use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "UPPERCASE")]
pub enum RunStatus {
Running,
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Run {
pub run_id: Uuid,
pub agent_spec_digest: String,
pub git_sha: String,
pub started_at: DateTime<Utc>,
pub finished_at: Option<DateTime<Utc>>,
pub status: RunStatus,
pub inputs: serde_json::Value,
pub outputs: Option<serde_json::Value>,
pub final_state_digest: Option<String>,
}
impl Run {
pub fn new(agent_spec_digest: String, git_sha: String, inputs: serde_json::Value) -> Self {
Self {
run_id: Uuid::new_v4(),
agent_spec_digest,
git_sha,
started_at: Utc::now(),
finished_at: None,
status: RunStatus::Running,
inputs,
outputs: None,
final_state_digest: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum EventKind {
GraphStarted,
GraphCompleted { iterations: u32, duration_ms: u64 },
GraphFailed { error: String },
NodeEntered { node_id: String, iteration: u32 },
NodeExited {
node_id: String,
next_node: Option<String>,
duration_ms: u64,
},
NodeFailed { node_id: String, error: String },
ToolCalled { tool_name: String },
ToolReturned { tool_name: String },
ToolFailed { tool_name: String },
CheckpointSaved {
checkpoint_id: String,
node_id: String,
},
CheckpointRestored {
checkpoint_id: String,
node_id: String,
},
CheckpointDeleted { checkpoint_id: String },
StateUpdated {
node_id: String,
keys_changed: Vec<String>,
},
MessageAdded { role: String, content_length: usize },
GraphInterrupted { reason: String, node_id: String },
NodeRetrying {
node_id: String,
attempt: u32,
delay_ms: u64,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Event {
pub run_id: Uuid,
pub seq: u64,
pub timestamp: DateTime<Utc>,
pub kind: EventKind,
pub payload: serde_json::Value,
}
impl Event {
pub fn new(run_id: Uuid, seq: u64, kind: EventKind, payload: serde_json::Value) -> Self {
Self {
run_id,
seq,
timestamp: Utc::now(),
kind,
payload,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_run_serde_roundtrip() {
let run = Run::new(
"spec_digest_123".to_string(),
"git_sha_abc".to_string(),
serde_json::json!({"question": "What is 2+2?"}),
);
let json = serde_json::to_string(&run).expect("serialize");
let deserialized: Run = serde_json::from_str(&json).expect("deserialize");
assert_eq!(run, deserialized);
}
#[test]
fn test_run_status_serde() {
let statuses = [
(RunStatus::Running, "\"RUNNING\""),
(RunStatus::Completed, "\"COMPLETED\""),
(RunStatus::Failed, "\"FAILED\""),
(RunStatus::Cancelled, "\"CANCELLED\""),
];
for (status, expected_json) in &statuses {
let json = serde_json::to_string(status).expect("serialize");
assert_eq!(json, *expected_json);
let deserialized: RunStatus = serde_json::from_str(&json).expect("deserialize");
assert_eq!(*status, deserialized);
}
}
#[test]
fn test_event_serde_roundtrip_graph_started() {
let run_id = Uuid::new_v4();
let event = Event::new(run_id, 1, EventKind::GraphStarted, serde_json::json!({}));
let json = serde_json::to_string(&event).expect("serialize");
let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
assert_eq!(event, deserialized);
}
#[test]
fn test_event_serde_roundtrip_node_entered() {
let run_id = Uuid::new_v4();
let event = Event::new(
run_id,
1,
EventKind::NodeEntered {
node_id: "node_42".to_string(),
iteration: 1,
},
serde_json::json!({"entry_time_ms": 100}),
);
let json = serde_json::to_string(&event).expect("serialize");
let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
assert_eq!(event, deserialized);
}
#[test]
fn test_event_serde_roundtrip_tool_called() {
let run_id = Uuid::new_v4();
let event = Event::new(
run_id,
5,
EventKind::ToolCalled {
tool_name: "search".to_string(),
},
serde_json::json!({"query": "llm models"}),
);
let json = serde_json::to_string(&event).expect("serialize");
let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
assert_eq!(event, deserialized);
}
#[test]
fn test_event_serde_roundtrip_checkpoint() {
let run_id = Uuid::new_v4();
let event = Event::new(
run_id,
10,
EventKind::CheckpointSaved {
checkpoint_id: "cp123".to_string(),
node_id: "node_x".to_string(),
},
serde_json::json!({"phase": 1, "duration_ms": 5000}),
);
let json = serde_json::to_string(&event).expect("serialize");
let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
assert_eq!(event, deserialized);
}
#[test]
fn test_run_new_defaults() {
let inputs = serde_json::json!({"test": "data"});
let run = Run::new(
"spec_digest".to_string(),
"git_sha".to_string(),
inputs.clone(),
);
assert_eq!(run.status, RunStatus::Running);
assert!(run.finished_at.is_none());
assert!(run.outputs.is_none());
assert!(run.final_state_digest.is_none());
assert_eq!(run.inputs, inputs);
}
}