oris_kernel/kernel/
timeline.rs1use serde::{Deserialize, Serialize};
6
7use crate::kernel::event::{Event, EventStore};
8use crate::kernel::identity::{RunId, Seq};
9use crate::kernel::KernelError;
10
11#[derive(Clone, Debug, Serialize, Deserialize)]
13pub struct TimelineEntry {
14 pub seq: Seq,
15 pub kind: String,
17 #[serde(skip_serializing_if = "Option::is_none")]
18 pub step_id: Option<String>,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub action_id: Option<String>,
21}
22
23#[derive(Clone, Debug, Serialize, Deserialize)]
25pub struct RunTimeline {
26 pub run_id: String,
27 pub events: Vec<TimelineEntry>,
28 pub final_status: RunStatusSummary,
29}
30
31#[derive(Clone, Debug, Serialize, Deserialize)]
33#[serde(tag = "status")]
34pub enum RunStatusSummary {
35 Completed,
36 Blocked { interrupt: bool },
37 Failed { recoverable: bool },
38}
39
40pub fn run_timeline(events: &dyn EventStore, run_id: &RunId) -> Result<RunTimeline, KernelError> {
43 const FROM_SEQ: Seq = 1;
44 let sequenced = events.scan(run_id, FROM_SEQ)?;
45 let mut entries = Vec::new();
46 let mut final_status = RunStatusSummary::Completed;
47
48 for se in sequenced {
49 let (kind, step_id, action_id) = match &se.event {
50 Event::StateUpdated { step_id, .. } => {
51 ("StateUpdated".to_string(), step_id.clone(), None)
52 }
53 Event::ActionRequested { action_id, .. } => {
54 ("ActionRequested".to_string(), None, Some(action_id.clone()))
55 }
56 Event::ActionSucceeded { action_id, .. } => {
57 ("ActionSucceeded".to_string(), None, Some(action_id.clone()))
58 }
59 Event::ActionFailed { action_id, .. } => {
60 final_status = RunStatusSummary::Failed { recoverable: false };
61 ("ActionFailed".to_string(), None, Some(action_id.clone()))
62 }
63 Event::Interrupted { .. } => {
64 final_status = RunStatusSummary::Blocked { interrupt: true };
65 ("Interrupted".to_string(), None, None)
66 }
67 Event::Resumed { .. } => ("Resumed".to_string(), None, None),
68 Event::Completed => {
69 final_status = RunStatusSummary::Completed;
70 ("Completed".to_string(), None, None)
71 }
72 };
73 entries.push(TimelineEntry {
74 seq: se.seq,
75 kind,
76 step_id,
77 action_id,
78 });
79 }
80
81 Ok(RunTimeline {
82 run_id: run_id.clone(),
83 events: entries,
84 final_status,
85 })
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::kernel::event::Event;
92 use crate::kernel::event_store::InMemoryEventStore;
93
94 #[test]
95 fn timeline_from_events_matches_order_and_final_status() {
96 let store = InMemoryEventStore::new();
97 let run_id = "timeline-test".to_string();
98 store
99 .append(
100 &run_id,
101 &[
102 Event::StateUpdated {
103 step_id: Some("n1".into()),
104 payload: serde_json::json!({"x": 1}),
105 },
106 Event::Completed,
107 ],
108 )
109 .unwrap();
110 let tl = run_timeline(&store, &run_id).unwrap();
111 assert_eq!(tl.run_id, run_id);
112 assert_eq!(tl.events.len(), 2);
113 assert_eq!(tl.events[0].seq, 1);
114 assert_eq!(tl.events[0].kind, "StateUpdated");
115 assert_eq!(tl.events[0].step_id.as_deref(), Some("n1"));
116 assert_eq!(tl.events[1].kind, "Completed");
117 assert!(matches!(tl.final_status, RunStatusSummary::Completed));
118 }
119
120 #[test]
121 fn timeline_json_roundtrip() {
122 let store = InMemoryEventStore::new();
123 let run_id = "json-test".to_string();
124 store
125 .append(
126 &run_id,
127 &[
128 Event::ActionRequested {
129 action_id: "a1".into(),
130 payload: serde_json::json!({"tool": "t1"}),
131 },
132 Event::ActionSucceeded {
133 action_id: "a1".into(),
134 output: serde_json::json!("ok"),
135 },
136 Event::Completed,
137 ],
138 )
139 .unwrap();
140 let tl = run_timeline(&store, &run_id).unwrap();
141 let json = serde_json::to_string(&tl).unwrap();
142 let _: RunTimeline = serde_json::from_str(&json).unwrap();
143 }
144}