1use crate::messages::Notification;
14use crate::protocol_generated::types::{Thread, ThreadItem, ThreadTokenUsage, Turn, TurnError};
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17
18#[allow(clippy::large_enum_variant)]
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28#[serde(tag = "type")]
29pub enum ExecEvent {
30 #[serde(rename = "thread.started")]
31 ThreadStarted { thread: Thread },
32 #[serde(rename = "turn.started")]
33 TurnStarted { thread_id: String, turn: Turn },
34 #[serde(rename = "turn.completed")]
35 TurnCompleted { thread_id: String, turn: Turn },
36 #[serde(rename = "turn.failed")]
39 TurnFailed {
40 thread_id: String,
41 turn_id: String,
42 error: TurnError,
43 },
44 #[serde(rename = "item.started")]
45 ItemStarted {
46 thread_id: String,
47 started_at_ms: i64,
48 item: ThreadItem,
49 },
50 #[serde(rename = "item.completed")]
51 ItemCompleted {
52 thread_id: String,
53 completed_at_ms: i64,
54 item: ThreadItem,
55 },
56 #[serde(rename = "item.agentMessage.delta")]
58 AgentMessageDelta {
59 thread_id: String,
60 item_id: String,
61 delta: String,
62 },
63 #[serde(rename = "thread.tokenUsage")]
65 TokenUsage {
66 thread_id: String,
67 turn_id: String,
68 token_usage: ThreadTokenUsage,
69 },
70 #[serde(rename = "raw")]
74 Raw {
75 method: String,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
77 params: Option<Value>,
78 },
79}
80
81impl ExecEvent {
82 pub fn from_notification(notification: Notification) -> ExecEvent {
86 match notification {
87 Notification::ThreadStarted(n) => ExecEvent::ThreadStarted { thread: n.thread },
88 Notification::TurnStarted(n) => ExecEvent::TurnStarted {
89 thread_id: n.thread_id,
90 turn: n.turn,
91 },
92 Notification::TurnCompleted(n) => ExecEvent::TurnCompleted {
93 thread_id: n.thread_id,
94 turn: n.turn,
95 },
96 Notification::Error(n) => ExecEvent::TurnFailed {
97 thread_id: n.thread_id,
98 turn_id: n.turn_id,
99 error: n.error,
100 },
101 Notification::ItemStarted(n) => ExecEvent::ItemStarted {
102 thread_id: n.thread_id,
103 started_at_ms: n.started_at_ms,
104 item: n.item,
105 },
106 Notification::ItemCompleted(n) => ExecEvent::ItemCompleted {
107 thread_id: n.thread_id,
108 completed_at_ms: n.completed_at_ms,
109 item: n.item,
110 },
111 Notification::AgentMessageDelta(n) => ExecEvent::AgentMessageDelta {
112 thread_id: n.thread_id,
113 item_id: n.item_id,
114 delta: n.delta,
115 },
116 Notification::ThreadTokenUsageUpdated(n) => ExecEvent::TokenUsage {
117 thread_id: n.thread_id,
118 turn_id: n.turn_id,
119 token_usage: n.token_usage,
120 },
121 other => {
122 let method = other.method().to_string();
123 match other.into_envelope() {
124 Ok((_, params)) => ExecEvent::Raw { method, params },
125 Err(_) => ExecEvent::Raw {
128 method,
129 params: None,
130 },
131 }
132 }
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
144 fn item_completed_maps_to_exec_tagged_json() {
145 let n = Notification::from_envelope(
146 "item/completed",
147 Some(serde_json::json!({
148 "threadId": "t-1",
149 "completedAtMs": 5,
150 "item": {"type": "agentMessage", "id": "i-1", "text": "hi"}
151 })),
152 )
153 .expect("typed notification");
154 let event = ExecEvent::from_notification(n);
155 let v = serde_json::to_value(&event).expect("serialize");
156 assert_eq!(v["type"], "item.completed");
157 assert_eq!(v["thread_id"], "t-1");
158 assert_eq!(v["item"]["type"], "agentMessage");
159 assert_eq!(v["item"]["text"], "hi");
160 }
161
162 #[test]
165 fn unknown_notification_passes_through_raw() {
166 let n = Notification::from_envelope("somefuture/thing", Some(serde_json::json!({"x": 1})))
167 .expect("unknown routes to Unknown without error");
168 let event = ExecEvent::from_notification(n);
169 match &event {
170 ExecEvent::Raw { method, params } => {
171 assert_eq!(method, "somefuture/thing");
172 assert_eq!(params.as_ref().unwrap()["x"], 1);
173 }
174 other => panic!("expected Raw, got {other:?}"),
175 }
176 let v = serde_json::to_value(&event).expect("serialize");
177 assert_eq!(v["type"], "raw");
178 }
179
180 #[test]
183 fn typed_but_unmapped_notification_rides_raw_with_its_method() {
184 let n = Notification::from_envelope(
185 "thread/reverted",
186 Some(serde_json::json!({"threadId": "t-9"})),
187 )
188 .expect("typed");
189 match ExecEvent::from_notification(n) {
190 ExecEvent::Raw { method, params } => {
191 assert_eq!(method, "thread/reverted");
192 assert_eq!(params.unwrap()["threadId"], "t-9");
193 }
194 other => panic!("expected Raw, got {other:?}"),
195 }
196 }
197}