1use chrono::{DateTime, Utc};
10
11mod surreal_datetime {
13 use chrono::{DateTime, Utc};
14 use serde::{self, Deserialize, Deserializer, Serializer};
15 use surrealdb::sql::Datetime as SurrealDatetime;
16
17 pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
18 where
19 S: Serializer,
20 {
21 let sd = SurrealDatetime::from(*date);
22 serde::Serialize::serialize(&sd, serializer)
23 }
24
25 pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
26 where
27 D: Deserializer<'de>,
28 {
29 let sd = SurrealDatetime::deserialize(deserializer)?;
30 Ok(DateTime::from(sd))
31 }
32}
33
34mod surreal_datetime_opt {
36 use chrono::{DateTime, Utc};
37 use serde::{self, Deserialize, Deserializer, Serializer};
38 use surrealdb::sql::Datetime as SurrealDatetime;
39
40 pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
41 where
42 S: Serializer,
43 {
44 match date {
45 Some(d) => {
46 let sd = SurrealDatetime::from(*d);
47 serde::Serialize::serialize(&Some(sd), serializer)
48 }
49 None => serde::Serialize::serialize(&None::<SurrealDatetime>, serializer),
50 }
51 }
52
53 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
54 where
55 D: Deserializer<'de>,
56 {
57 let sd = Option::<SurrealDatetime>::deserialize(deserializer)?;
58 Ok(sd.map(DateTime::from))
59 }
60}
61use serde::{Deserialize, Serialize};
62use sha2::{Digest, Sha256};
63use uuid::Uuid;
64
65#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
72pub struct CommitId {
73 pub hash: String,
75 pub logic_hash: Option<String>,
77 pub state_hash: String,
79 pub env_hash: Option<String>,
81}
82
83impl CommitId {
84 pub fn from_state(state: &[u8]) -> Self {
86 let mut hasher = Sha256::new();
87 hasher.update(state);
88 let state_hash = hex::encode(hasher.finalize());
89
90 CommitId {
92 hash: state_hash.clone(),
93 logic_hash: None,
94 state_hash,
95 env_hash: None,
96 }
97 }
98
99 pub fn new(logic_hash: Option<&str>, state_hash: &str, env_hash: Option<&str>) -> Self {
101 let mut hasher = Sha256::new();
102 if let Some(lh) = logic_hash {
103 hasher.update(lh.as_bytes());
104 }
105 hasher.update(state_hash.as_bytes());
106 if let Some(eh) = env_hash {
107 hasher.update(eh.as_bytes());
108 }
109 let composite = hex::encode(hasher.finalize());
110
111 CommitId {
112 hash: composite,
113 logic_hash: logic_hash.map(String::from),
114 state_hash: state_hash.to_string(),
115 env_hash: env_hash.map(String::from),
116 }
117 }
118
119 pub fn short(&self) -> &str {
121 &self.hash[..8.min(self.hash.len())]
122 }
123}
124
125impl std::fmt::Display for CommitId {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 write!(f, "{}", self.hash)
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct CommitRecord {
134 pub id: Option<surrealdb::sql::Thing>,
136 pub commit_id: CommitId,
138 pub parent_ids: Vec<String>,
140 pub message: String,
142 pub author: String,
144 #[serde(with = "surreal_datetime")]
146 pub created_at: DateTime<Utc>,
147 pub branch: Option<String>,
149}
150
151impl CommitRecord {
152 pub fn new(commit_id: CommitId, parent_ids: Vec<String>, message: &str, author: &str) -> Self {
154 CommitRecord {
155 id: None,
156 commit_id,
157 parent_ids,
158 message: message.to_string(),
159 author: author.to_string(),
160 created_at: Utc::now(),
161 branch: None,
162 }
163 }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct SnapshotRecord {
169 pub id: Option<surrealdb::sql::Thing>,
171 pub commit_id: String,
173 pub state: serde_json::Value,
175 pub size_bytes: u64,
177 #[serde(with = "surreal_datetime")]
179 pub created_at: DateTime<Utc>,
180}
181
182impl SnapshotRecord {
183 pub fn new(commit_id: &str, state: serde_json::Value) -> Self {
185 let size = serde_json::to_string(&state)
186 .map(|s| s.len() as u64)
187 .unwrap_or(0);
188
189 SnapshotRecord {
190 id: None,
191 commit_id: commit_id.to_string(),
192 state,
193 size_bytes: size,
194 created_at: Utc::now(),
195 }
196 }
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct BranchRecord {
202 pub id: Option<surrealdb::sql::Thing>,
204 pub name: String,
206 pub head_commit_id: String,
208 pub is_default: bool,
210 #[serde(with = "surreal_datetime")]
212 pub created_at: DateTime<Utc>,
213 #[serde(with = "surreal_datetime")]
215 pub updated_at: DateTime<Utc>,
216}
217
218impl BranchRecord {
219 pub fn new(name: &str, head_commit_id: &str, is_default: bool) -> Self {
221 let now = Utc::now();
222 BranchRecord {
223 id: None,
224 name: name.to_string(),
225 head_commit_id: head_commit_id.to_string(),
226 is_default,
227 created_at: now,
228 updated_at: now,
229 }
230 }
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct AgentRecord {
236 pub id: Option<surrealdb::sql::Thing>,
238 pub agent_id: Uuid,
240 pub name: String,
242 pub agent_type: String,
244 pub config: serde_json::Value,
246 #[serde(with = "surreal_datetime")]
248 pub created_at: DateTime<Utc>,
249}
250
251impl AgentRecord {
252 pub fn new(name: &str, agent_type: &str, config: serde_json::Value) -> Self {
254 AgentRecord {
255 id: None,
256 agent_id: Uuid::new_v4(),
257 name: name.to_string(),
258 agent_type: agent_type.to_string(),
259 config,
260 created_at: Utc::now(),
261 }
262 }
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct MemoryRecord {
268 pub id: Option<surrealdb::sql::Thing>,
270 pub commit_id: String,
272 pub key: String,
274 pub content: String,
276 pub embedding: Option<Vec<f32>>,
278 pub metadata: serde_json::Value,
280 #[serde(with = "surreal_datetime")]
282 pub created_at: DateTime<Utc>,
283}
284
285impl MemoryRecord {
286 pub fn new(commit_id: &str, key: &str, content: &str) -> Self {
288 MemoryRecord {
289 id: None,
290 commit_id: commit_id.to_string(),
291 key: key.to_string(),
292 content: content.to_string(),
293 embedding: None,
294 metadata: serde_json::json!({}),
295 created_at: Utc::now(),
296 }
297 }
298
299 pub fn with_embedding(mut self, embedding: Vec<f32>) -> Self {
301 self.embedding = Some(embedding);
302 self
303 }
304
305 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
307 self.metadata = metadata;
308 self
309 }
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct GraphEdge {
315 pub child_id: String,
317 pub parent_id: String,
319 pub edge_type: EdgeType,
321 #[serde(with = "surreal_datetime")]
323 pub created_at: DateTime<Utc>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
328#[serde(rename_all = "lowercase")]
329pub enum EdgeType {
330 Normal,
332 Merge,
334 Fork,
336}
337
338impl GraphEdge {
339 pub fn new(child_id: &str, parent_id: &str) -> Self {
341 GraphEdge {
342 child_id: child_id.to_string(),
343 parent_id: parent_id.to_string(),
344 edge_type: EdgeType::Normal,
345 created_at: Utc::now(),
346 }
347 }
348
349 pub fn merge(child_id: &str, parent_id: &str) -> Self {
351 GraphEdge {
352 child_id: child_id.to_string(),
353 parent_id: parent_id.to_string(),
354 edge_type: EdgeType::Merge,
355 created_at: Utc::now(),
356 }
357 }
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct RunRecord {
367 pub id: Option<surrealdb::sql::Thing>,
369 pub run_id: String,
371 pub spec_digest: String,
373 pub git_sha: Option<String>,
375 pub agent_name: String,
377 pub tags: serde_json::Value,
379 pub status: String,
381 pub total_events: u64,
383 pub final_state_digest: Option<String>,
385 pub duration_ms: u64,
387 pub success: bool,
389 #[serde(with = "surreal_datetime")]
391 pub created_at: DateTime<Utc>,
392 #[serde(default, with = "surreal_datetime_opt")]
394 pub completed_at: Option<DateTime<Utc>>,
395}
396
397impl RunRecord {
398 pub fn new(
400 run_id: String,
401 spec_digest: String,
402 git_sha: Option<String>,
403 agent_name: String,
404 tags: serde_json::Value,
405 ) -> Self {
406 RunRecord {
407 id: None,
408 run_id,
409 spec_digest,
410 git_sha,
411 agent_name,
412 tags,
413 status: "running".to_string(),
414 total_events: 0,
415 final_state_digest: None,
416 duration_ms: 0,
417 success: false,
418 created_at: Utc::now(),
419 completed_at: None,
420 }
421 }
422
423 pub fn complete(
425 mut self,
426 total_events: u64,
427 final_state_digest: Option<String>,
428 duration_ms: u64,
429 ) -> Self {
430 self.status = "completed".to_string();
431 self.total_events = total_events;
432 self.final_state_digest = final_state_digest;
433 self.duration_ms = duration_ms;
434 self.success = true;
435 self.completed_at = Some(Utc::now());
436 self
437 }
438
439 pub fn fail(mut self, total_events: u64, duration_ms: u64) -> Self {
441 self.status = "failed".to_string();
442 self.total_events = total_events;
443 self.duration_ms = duration_ms;
444 self.success = false;
445 self.completed_at = Some(Utc::now());
446 self
447 }
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct RunEventRecord {
453 pub id: Option<surrealdb::sql::Thing>,
455 pub run_id: String,
457 pub seq: u64,
459 pub kind: String,
461 pub payload: serde_json::Value,
463 #[serde(with = "surreal_datetime")]
465 pub timestamp: DateTime<Utc>,
466}
467
468impl RunEventRecord {
469 pub fn new(run_id: String, seq: u64, kind: String, payload: serde_json::Value) -> Self {
471 RunEventRecord {
472 id: None,
473 run_id,
474 seq,
475 kind,
476 payload,
477 timestamp: Utc::now(),
478 }
479 }
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
484pub struct ReleaseRecordSchema {
485 pub id: Option<surrealdb::sql::Thing>,
487 pub agent_name: String,
489 pub spec_digest: String,
491 pub version_label: Option<String>,
493 pub promoted_by: String,
495 pub notes: Option<String>,
497 #[serde(with = "surreal_datetime")]
499 pub created_at: DateTime<Utc>,
500}
501
502impl ReleaseRecordSchema {
503 pub fn new(
505 agent_name: String,
506 spec_digest: String,
507 version_label: Option<String>,
508 promoted_by: String,
509 notes: Option<String>,
510 ) -> Self {
511 ReleaseRecordSchema {
512 id: None,
513 agent_name,
514 spec_digest,
515 version_label,
516 promoted_by,
517 notes,
518 created_at: Utc::now(),
519 }
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526
527 #[test]
528 fn test_commit_id_from_state() {
529 let state = b"test state data";
530 let commit_id = CommitId::from_state(state);
531
532 assert!(!commit_id.hash.is_empty());
533 assert_eq!(commit_id.hash.len(), 64); assert!(commit_id.logic_hash.is_none());
535 assert!(commit_id.env_hash.is_none());
536 }
537
538 #[test]
539 fn test_commit_id_deterministic() {
540 let state = b"same state";
541 let id1 = CommitId::from_state(state);
542 let id2 = CommitId::from_state(state);
543
544 assert_eq!(id1.hash, id2.hash);
545 }
546
547 #[test]
548 fn test_commit_id_different_states() {
549 let id1 = CommitId::from_state(b"state 1");
550 let id2 = CommitId::from_state(b"state 2");
551
552 assert_ne!(id1.hash, id2.hash);
553 }
554
555 #[test]
556 fn test_commit_id_short() {
557 let commit_id = CommitId::from_state(b"test");
558 assert_eq!(commit_id.short().len(), 8);
559 }
560
561 #[test]
562 fn test_composite_commit_id() {
563 let commit_id = CommitId::new(Some("logic-hash"), "state-hash", Some("env-hash"));
564
565 assert!(!commit_id.hash.is_empty());
566 assert_eq!(commit_id.logic_hash, Some("logic-hash".to_string()));
567 assert_eq!(commit_id.env_hash, Some("env-hash".to_string()));
568 }
569
570 #[test]
571 fn test_snapshot_record_size() {
572 let state = serde_json::json!({"key": "value", "nested": {"a": 1}});
573 let snapshot = SnapshotRecord::new("commit-123", state);
574
575 assert!(snapshot.size_bytes > 0);
576 }
577
578 #[test]
579 fn test_run_record_new() {
580 let run = RunRecord::new(
581 "run-123".to_string(),
582 "spec-digest-abc".to_string(),
583 Some("abc123".to_string()),
584 "test-agent".to_string(),
585 serde_json::json!({"env": "test"}),
586 );
587
588 assert_eq!(run.run_id, "run-123");
589 assert_eq!(run.status, "running");
590 assert_eq!(run.total_events, 0);
591 assert!(!run.success);
592 }
593
594 #[test]
595 fn test_run_record_complete() {
596 let run = RunRecord::new(
597 "run-123".to_string(),
598 "spec-digest-abc".to_string(),
599 Some("abc123".to_string()),
600 "test-agent".to_string(),
601 serde_json::json!({}),
602 )
603 .complete(5, Some("state-digest-xyz".to_string()), 1000);
604
605 assert_eq!(run.status, "completed");
606 assert_eq!(run.total_events, 5);
607 assert!(run.success);
608 assert!(run.completed_at.is_some());
609 }
610
611 #[test]
612 fn test_run_record_fail() {
613 let run = RunRecord::new(
614 "run-123".to_string(),
615 "spec-digest-abc".to_string(),
616 None,
617 "test-agent".to_string(),
618 serde_json::json!({}),
619 )
620 .fail(2, 500);
621
622 assert_eq!(run.status, "failed");
623 assert_eq!(run.total_events, 2);
624 assert!(!run.success);
625 assert!(run.completed_at.is_some());
626 }
627
628 #[test]
629 fn test_run_event_record() {
630 let event = RunEventRecord::new(
631 "run-123".to_string(),
632 1,
633 "GraphStarted".to_string(),
634 serde_json::json!({"graph_id": "g1"}),
635 );
636
637 assert_eq!(event.run_id, "run-123");
638 assert_eq!(event.seq, 1);
639 assert_eq!(event.kind, "GraphStarted");
640 }
641
642 #[test]
643 fn test_release_record() {
644 let release = ReleaseRecordSchema::new(
645 "my-agent".to_string(),
646 "spec-digest-abc".to_string(),
647 Some("v1.0.0".to_string()),
648 "alice".to_string(),
649 Some("Initial release".to_string()),
650 );
651
652 assert_eq!(release.agent_name, "my-agent");
653 assert_eq!(release.version_label, Some("v1.0.0".to_string()));
654 }
655}