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,
74
75    /// Graph execution failed.
76    GraphFailed,
77
78    /// Entered a graph node.
79    #[serde(rename = "node_entered")]
80    NodeEntered { node_id: String },
81
82    /// Exited a graph node.
83    #[serde(rename = "node_exited")]
84    NodeExited { node_id: String },
85
86    /// Graph node execution failed.
87    #[serde(rename = "node_failed")]
88    NodeFailed { node_id: String },
89
90    /// Tool was called.
91    #[serde(rename = "tool_called")]
92    ToolCalled { tool_name: String },
93
94    /// Tool returned a result.
95    #[serde(rename = "tool_returned")]
96    ToolReturned { tool_name: String },
97
98    /// Tool execution failed.
99    #[serde(rename = "tool_failed")]
100    ToolFailed { tool_name: String },
101
102    /// Checkpoint marker in execution.
103    #[serde(rename = "checkpoint")]
104    Checkpoint { label: String },
105}
106
107/// A single event in a run's execution trace.
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
109pub struct Event {
110    /// Which run this event belongs to.
111    pub run_id: Uuid,
112
113    /// Monotonically increasing sequence number within the run.
114    pub seq: u64,
115
116    /// When the event occurred.
117    pub timestamp: DateTime<Utc>,
118
119    /// Event classification.
120    pub kind: EventKind,
121
122    /// Event-specific payload.
123    pub payload: serde_json::Value,
124}
125
126impl Event {
127    /// Create a new event.
128    pub fn new(run_id: Uuid, seq: u64, kind: EventKind, payload: serde_json::Value) -> Self {
129        Self {
130            run_id,
131            seq,
132            timestamp: Utc::now(),
133            kind,
134            payload,
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_run_serde_roundtrip() {
145        let run = Run::new(
146            "spec_digest_123".to_string(),
147            "git_sha_abc".to_string(),
148            serde_json::json!({"question": "What is 2+2?"}),
149        );
150
151        let json = serde_json::to_string(&run).expect("serialize");
152        let deserialized: Run = serde_json::from_str(&json).expect("deserialize");
153
154        assert_eq!(run, deserialized);
155    }
156
157    #[test]
158    fn test_run_status_serde() {
159        let statuses = [
160            RunStatus::Running,
161            RunStatus::Completed,
162            RunStatus::Failed,
163            RunStatus::Cancelled,
164        ];
165
166        for status in &statuses {
167            let json = serde_json::to_string(status).expect("serialize");
168            let deserialized: RunStatus = serde_json::from_str(&json).expect("deserialize");
169            assert_eq!(*status, deserialized);
170        }
171    }
172
173    #[test]
174    fn test_event_serde_roundtrip_graph_started() {
175        let run_id = Uuid::new_v4();
176        let event = Event::new(run_id, 0, EventKind::GraphStarted, serde_json::json!({}));
177
178        let json = serde_json::to_string(&event).expect("serialize");
179        let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
180
181        assert_eq!(event, deserialized);
182    }
183
184    #[test]
185    fn test_event_serde_roundtrip_node_entered() {
186        let run_id = Uuid::new_v4();
187        let event = Event::new(
188            run_id,
189            1,
190            EventKind::NodeEntered {
191                node_id: "node_42".to_string(),
192            },
193            serde_json::json!({"entry_time_ms": 100}),
194        );
195
196        let json = serde_json::to_string(&event).expect("serialize");
197        let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
198
199        assert_eq!(event, deserialized);
200    }
201
202    #[test]
203    fn test_event_serde_roundtrip_tool_called() {
204        let run_id = Uuid::new_v4();
205        let event = Event::new(
206            run_id,
207            5,
208            EventKind::ToolCalled {
209                tool_name: "search".to_string(),
210            },
211            serde_json::json!({"query": "llm models"}),
212        );
213
214        let json = serde_json::to_string(&event).expect("serialize");
215        let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
216
217        assert_eq!(event, deserialized);
218    }
219
220    #[test]
221    fn test_event_serde_roundtrip_checkpoint() {
222        let run_id = Uuid::new_v4();
223        let event = Event::new(
224            run_id,
225            10,
226            EventKind::Checkpoint {
227                label: "phase_1_complete".to_string(),
228            },
229            serde_json::json!({"phase": 1, "duration_ms": 5000}),
230        );
231
232        let json = serde_json::to_string(&event).expect("serialize");
233        let deserialized: Event = serde_json::from_str(&json).expect("deserialize");
234
235        assert_eq!(event, deserialized);
236    }
237
238    #[test]
239    fn test_run_new_defaults() {
240        let inputs = serde_json::json!({"test": "data"});
241        let run = Run::new(
242            "spec_digest".to_string(),
243            "git_sha".to_string(),
244            inputs.clone(),
245        );
246
247        assert_eq!(run.status, RunStatus::Running);
248        assert!(run.finished_at.is_none());
249        assert!(run.outputs.is_none());
250        assert!(run.final_state_digest.is_none());
251        assert_eq!(run.inputs, inputs);
252    }
253}