Skip to main content

aivcs_core/domain/
run.rs

1//! Run and event tracking.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Status of a run.
8#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
9#[serde(rename_all = "UPPERCASE")]
10pub enum RunStatus {
11    Running,
12    Completed,
13    Failed,
14    Cancelled,
15}
16
17/// A single execution of an agent against an AgentSpec.
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19pub struct Run {
20    /// Unique identifier for this run.
21    pub run_id: Uuid,
22
23    /// Digest of the AgentSpec this run executed.
24    pub agent_spec_digest: String,
25
26    /// Git commit where execution occurred.
27    pub git_sha: String,
28
29    /// When execution started.
30    pub started_at: DateTime<Utc>,
31
32    /// When execution finished (None if still running).
33    pub finished_at: Option<DateTime<Utc>>,
34
35    /// Current execution status.
36    pub status: RunStatus,
37
38    /// Inputs provided to the agent.
39    pub inputs: serde_json::Value,
40
41    /// Outputs from the agent (available after completion).
42    pub outputs: Option<serde_json::Value>,
43
44    /// Digest of final agent state (for deduplication).
45    pub final_state_digest: Option<String>,
46}
47
48impl Run {
49    /// Create a new run.
50    pub fn new(agent_spec_digest: String, git_sha: String, inputs: serde_json::Value) -> Self {
51        Self {
52            run_id: Uuid::new_v4(),
53            agent_spec_digest,
54            git_sha,
55            started_at: Utc::now(),
56            finished_at: None,
57            status: RunStatus::Running,
58            inputs,
59            outputs: None,
60            final_state_digest: None,
61        }
62    }
63}
64
65/// Classification of an event in a run.
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
67#[serde(tag = "type", rename_all = "snake_case")]
68pub enum EventKind {
69    /// Graph execution started.
70    GraphStarted,
71
72    /// Graph execution completed successfully.
73    GraphCompleted { iterations: u32, duration_ms: u64 },
74
75    /// Graph execution failed.
76    GraphFailed { error: String },
77
78    /// Entered a graph node.
79    NodeEntered { node_id: String, iteration: u32 },
80
81    /// Exited a graph node.
82    NodeExited {
83        node_id: String,
84        next_node: Option<String>,
85        duration_ms: u64,
86    },
87
88    /// Graph node execution failed.
89    NodeFailed { node_id: String, error: String },
90
91    /// Tool was called.
92    ToolCalled { tool_name: String },
93
94    /// Tool returned a result.
95    ToolReturned { tool_name: String },
96
97    /// Tool execution failed.
98    ToolFailed { tool_name: String },
99
100    /// Checkpoint marker in execution.
101    CheckpointSaved {
102        checkpoint_id: String,
103        node_id: String,
104    },
105
106    /// Checkpoint restored.
107    CheckpointRestored {
108        checkpoint_id: String,
109        node_id: String,
110    },
111
112    /// Checkpoint deleted.
113    CheckpointDeleted { checkpoint_id: String },
114
115    /// State updated in a node.
116    StateUpdated {
117        node_id: String,
118        keys_changed: Vec<String>,
119    },
120
121    /// Message added to execution context.
122    MessageAdded { role: String, content_length: usize },
123
124    /// Graph execution interrupted.
125    GraphInterrupted { reason: String, node_id: String },
126
127    /// Node execution retrying.
128    NodeRetrying {
129        node_id: String,
130        attempt: u32,
131        delay_ms: u64,
132    },
133}
134
135/// A single event in a run's execution trace.
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
137pub struct Event {
138    /// Which run this event belongs to.
139    pub run_id: Uuid,
140
141    /// Monotonically increasing sequence number within the run.
142    pub seq: u64,
143
144    /// When the event occurred.
145    pub timestamp: DateTime<Utc>,
146
147    /// Event classification.
148    pub kind: EventKind,
149
150    /// Event-specific payload.
151    pub payload: serde_json::Value,
152}
153
154impl Event {
155    /// Create a new event.
156    pub fn new(run_id: Uuid, seq: u64, kind: EventKind, payload: serde_json::Value) -> Self {
157        Self {
158            run_id,
159            seq,
160            timestamp: Utc::now(),
161            kind,
162            payload,
163        }
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_run_serde_roundtrip() {
173        let run = Run::new(
174            "spec_digest_123".to_string(),
175            "git_sha_abc".to_string(),
176            serde_json::json!({"question": "What is 2+2?"}),
177        );
178
179        let json = serde_json::to_string(&run).expect("serialize");
180        let deserialized: Run = serde_json::from_str(&json).expect("deserialize");
181
182        assert_eq!(run, deserialized);
183    }
184
185    #[test]
186    fn test_run_status_serde() {
187        let statuses = [
188            (RunStatus::Running, "\"RUNNING\""),
189            (RunStatus::Completed, "\"COMPLETED\""),
190            (RunStatus::Failed, "\"FAILED\""),
191            (RunStatus::Cancelled, "\"CANCELLED\""),
192        ];
193
194        for (status, expected_json) in &statuses {
195            let json = serde_json::to_string(status).expect("serialize");
196            assert_eq!(json, *expected_json);
197            let deserialized: RunStatus = serde_json::from_str(&json).expect("deserialize");
198            assert_eq!(*status, deserialized);
199        }
200    }
201
202    #[test]
203    fn test_event_serde_roundtrip_graph_started() {
204        let run_id = Uuid::new_v4();
205        let event = Event::new(run_id, 1, EventKind::GraphStarted, serde_json::json!({}));
206
207        let json = serde_json::to_string(&event).expect("serialize");
208        let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
209
210        assert_eq!(event, deserialized);
211    }
212
213    #[test]
214    fn test_event_serde_roundtrip_node_entered() {
215        let run_id = Uuid::new_v4();
216        let event = Event::new(
217            run_id,
218            1,
219            EventKind::NodeEntered {
220                node_id: "node_42".to_string(),
221                iteration: 1,
222            },
223            serde_json::json!({"entry_time_ms": 100}),
224        );
225
226        let json = serde_json::to_string(&event).expect("serialize");
227        let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
228
229        assert_eq!(event, deserialized);
230    }
231
232    #[test]
233    fn test_event_serde_roundtrip_tool_called() {
234        let run_id = Uuid::new_v4();
235        let event = Event::new(
236            run_id,
237            5,
238            EventKind::ToolCalled {
239                tool_name: "search".to_string(),
240            },
241            serde_json::json!({"query": "llm models"}),
242        );
243
244        let json = serde_json::to_string(&event).expect("serialize");
245        let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
246
247        assert_eq!(event, deserialized);
248    }
249
250    #[test]
251    fn test_event_serde_roundtrip_checkpoint() {
252        let run_id = Uuid::new_v4();
253        let event = Event::new(
254            run_id,
255            10,
256            EventKind::CheckpointSaved {
257                checkpoint_id: "cp123".to_string(),
258                node_id: "node_x".to_string(),
259            },
260            serde_json::json!({"phase": 1, "duration_ms": 5000}),
261        );
262
263        let json = serde_json::to_string(&event).expect("serialize");
264        let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
265
266        assert_eq!(event, deserialized);
267    }
268
269    #[test]
270    fn test_run_new_defaults() {
271        let inputs = serde_json::json!({"test": "data"});
272        let run = Run::new(
273            "spec_digest".to_string(),
274            "git_sha".to_string(),
275            inputs.clone(),
276        );
277
278        assert_eq!(run.status, RunStatus::Running);
279        assert!(run.finished_at.is_none());
280        assert!(run.outputs.is_none());
281        assert!(run.final_state_digest.is_none());
282        assert_eq!(run.inputs, inputs);
283    }
284}