1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use super::approval::ApprovalRequest;
5use super::checkpoint::CheckpointData;
6use super::plan_update::PlanItem;
7use super::session::SessionId;
8
9#[derive(Clone, Debug, Serialize, Deserialize)]
19#[serde(tag = "userEventType", rename_all = "camelCase")]
20pub enum UserEvent {
21 Progress { text: String },
23 Structured { event_type: String, data: Value },
25 ToolPartialResult {
28 tool_call_id: String,
29 content: String,
30 is_partial: bool,
31 },
32}
33
34#[derive(Clone, Debug, Serialize, Deserialize)]
41#[serde(tag = "runtimeEventType", rename_all = "camelCase")]
42pub enum RuntimeEvent {
43 TextDelta {
45 session_id: SessionId,
46 text: String,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 agent_id: Option<String>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 trace_id: Option<String>,
53 },
54 ThoughtDelta {
55 session_id: SessionId,
56 text: String,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 agent_id: Option<String>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 trace_id: Option<String>,
61 },
62 ToolCallStarted {
63 session_id: SessionId,
64 tool_name: String,
65 args_json: String,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 agent_id: Option<String>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 trace_id: Option<String>,
70 },
71 ToolCallFinished {
72 session_id: SessionId,
73 tool_name: String,
74 summary: String,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 agent_id: Option<String>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 trace_id: Option<String>,
79 #[serde(default)]
82 denied: bool,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
86 details: Option<Value>,
87 },
88 AwaitingApproval {
89 session_id: SessionId,
90 request: ApprovalRequest,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 agent_id: Option<String>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 trace_id: Option<String>,
95 },
96 Checkpoint {
97 session_id: SessionId,
98 checkpoint: CheckpointData,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 agent_id: Option<String>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 trace_id: Option<String>,
103 },
104 RunFinished {
105 session_id: SessionId,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 agent_id: Option<String>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
109 trace_id: Option<String>,
110 },
111 RunCancelled {
112 session_id: SessionId,
113 #[serde(default, skip_serializing_if = "Option::is_none")]
114 agent_id: Option<String>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
116 trace_id: Option<String>,
117 },
118 PlanUpdated {
120 session_id: SessionId,
121 objective: String,
122 explanation: Option<String>,
123 plan: Vec<PlanItem>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 agent_id: Option<String>,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 trace_id: Option<String>,
128 },
129 UserEvent {
132 session_id: SessionId,
133 event: UserEvent,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 agent_id: Option<String>,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
137 trace_id: Option<String>,
138 },
139}
140
141impl RuntimeEvent {
142 pub fn session_id(&self) -> &SessionId {
144 match self {
145 RuntimeEvent::TextDelta { session_id, .. } => session_id,
146 RuntimeEvent::ThoughtDelta { session_id, .. } => session_id,
147 RuntimeEvent::ToolCallStarted { session_id, .. } => session_id,
148 RuntimeEvent::ToolCallFinished { session_id, .. } => session_id,
149 RuntimeEvent::AwaitingApproval { session_id, .. } => session_id,
150 RuntimeEvent::Checkpoint { session_id, .. } => session_id,
151 RuntimeEvent::RunFinished { session_id, .. } => session_id,
152 RuntimeEvent::RunCancelled { session_id, .. } => session_id,
153 RuntimeEvent::PlanUpdated { session_id, .. } => session_id,
154 RuntimeEvent::UserEvent { session_id, .. } => session_id,
155 }
156 }
157
158 pub fn agent_id(&self) -> Option<&str> {
160 match self {
161 RuntimeEvent::TextDelta { agent_id, .. } => agent_id.as_deref(),
162 RuntimeEvent::ThoughtDelta { agent_id, .. } => agent_id.as_deref(),
163 RuntimeEvent::ToolCallStarted { agent_id, .. } => agent_id.as_deref(),
164 RuntimeEvent::ToolCallFinished { agent_id, .. } => agent_id.as_deref(),
165 RuntimeEvent::AwaitingApproval { agent_id, .. } => agent_id.as_deref(),
166 RuntimeEvent::Checkpoint { agent_id, .. } => agent_id.as_deref(),
167 RuntimeEvent::RunFinished { agent_id, .. } => agent_id.as_deref(),
168 RuntimeEvent::RunCancelled { agent_id, .. } => agent_id.as_deref(),
169 RuntimeEvent::PlanUpdated { agent_id, .. } => agent_id.as_deref(),
170 RuntimeEvent::UserEvent { agent_id, .. } => agent_id.as_deref(),
171 }
172 }
173
174 pub fn trace_id(&self) -> Option<&str> {
176 match self {
177 RuntimeEvent::TextDelta { trace_id, .. } => trace_id.as_deref(),
178 RuntimeEvent::ThoughtDelta { trace_id, .. } => trace_id.as_deref(),
179 RuntimeEvent::ToolCallStarted { trace_id, .. } => trace_id.as_deref(),
180 RuntimeEvent::ToolCallFinished { trace_id, .. } => trace_id.as_deref(),
181 RuntimeEvent::AwaitingApproval { trace_id, .. } => trace_id.as_deref(),
182 RuntimeEvent::Checkpoint { trace_id, .. } => trace_id.as_deref(),
183 RuntimeEvent::RunFinished { trace_id, .. } => trace_id.as_deref(),
184 RuntimeEvent::RunCancelled { trace_id, .. } => trace_id.as_deref(),
185 RuntimeEvent::PlanUpdated { trace_id, .. } => trace_id.as_deref(),
186 RuntimeEvent::UserEvent { trace_id, .. } => trace_id.as_deref(),
187 }
188 }
189
190 pub fn with_agent_id(mut self, id: impl Into<String>) -> Self {
192 let id = id.into();
193 match &mut self {
194 RuntimeEvent::TextDelta { agent_id, .. }
195 | RuntimeEvent::ThoughtDelta { agent_id, .. }
196 | RuntimeEvent::ToolCallStarted { agent_id, .. }
197 | RuntimeEvent::ToolCallFinished { agent_id, .. }
198 | RuntimeEvent::AwaitingApproval { agent_id, .. }
199 | RuntimeEvent::Checkpoint { agent_id, .. }
200 | RuntimeEvent::RunFinished { agent_id, .. }
201 | RuntimeEvent::RunCancelled { agent_id, .. }
202 | RuntimeEvent::PlanUpdated { agent_id, .. }
203 | RuntimeEvent::UserEvent { agent_id, .. } => *agent_id = Some(id),
204 }
205 self
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::types::{CheckpointStep, PlanStepStatus, RiskLevel};
213
214 fn sid(id: u64) -> SessionId {
215 SessionId::new(id)
216 }
217
218 fn approval_request() -> ApprovalRequest {
219 ApprovalRequest {
220 title: "title".to_string(),
221 message: "message".to_string(),
222 action_key: None,
223 risk_level: RiskLevel::Safe,
224 raw: None,
225 source: None,
226 }
227 }
228
229 fn checkpoint() -> CheckpointData {
230 CheckpointData {
231 session_id: sid(42),
232 user_input: "input".to_string(),
233 step: CheckpointStep::AfterUserInput,
234 turn_count: 0,
235 }
236 }
237
238 #[test]
239 fn accessors_return_embedded_ids_for_all_variants() {
240 let events: Vec<RuntimeEvent> = vec![
241 RuntimeEvent::TextDelta {
242 session_id: sid(1),
243 text: "hi".into(),
244 agent_id: Some("a".into()),
245 trace_id: Some("t".into()),
246 },
247 RuntimeEvent::ThoughtDelta {
248 session_id: sid(2),
249 text: "hmm".into(),
250 agent_id: Some("a".into()),
251 trace_id: Some("t".into()),
252 },
253 RuntimeEvent::ToolCallStarted {
254 session_id: sid(3),
255 tool_name: "read".into(),
256 args_json: "{}".into(),
257 agent_id: Some("a".into()),
258 trace_id: Some("t".into()),
259 },
260 RuntimeEvent::ToolCallFinished {
261 session_id: sid(4),
262 tool_name: "read".into(),
263 summary: "ok".into(),
264 agent_id: Some("a".into()),
265 trace_id: Some("t".into()),
266 denied: false,
267 details: None,
268 },
269 RuntimeEvent::AwaitingApproval {
270 session_id: sid(5),
271 request: approval_request(),
272 agent_id: Some("a".into()),
273 trace_id: Some("t".into()),
274 },
275 RuntimeEvent::Checkpoint {
276 session_id: sid(6),
277 checkpoint: checkpoint(),
278 agent_id: Some("a".into()),
279 trace_id: Some("t".into()),
280 },
281 RuntimeEvent::RunFinished {
282 session_id: sid(7),
283 agent_id: Some("a".into()),
284 trace_id: Some("t".into()),
285 },
286 RuntimeEvent::RunCancelled {
287 session_id: sid(8),
288 agent_id: Some("a".into()),
289 trace_id: Some("t".into()),
290 },
291 RuntimeEvent::PlanUpdated {
292 session_id: sid(9),
293 objective: "goal".into(),
294 explanation: None,
295 plan: vec![PlanItem {
296 step: "s".into(),
297 status: PlanStepStatus::Pending,
298 }],
299 agent_id: Some("a".into()),
300 trace_id: Some("t".into()),
301 },
302 RuntimeEvent::UserEvent {
303 session_id: sid(10),
304 event: UserEvent::Progress { text: "p".into() },
305 agent_id: Some("a".into()),
306 trace_id: Some("t".into()),
307 },
308 ];
309
310 for (i, ev) in events.iter().enumerate() {
311 let expected = i as u64 + 1;
312 assert_eq!(ev.session_id(), &sid(expected), "variant {i}");
313 assert_eq!(ev.agent_id(), Some("a"), "variant {i}");
314 assert_eq!(ev.trace_id(), Some("t"), "variant {i}");
315 }
316 }
317
318 #[test]
319 fn with_agent_id_sets_id_on_all_variants() {
320 let events: Vec<RuntimeEvent> = vec![
321 RuntimeEvent::TextDelta {
322 session_id: sid(1),
323 text: "hi".into(),
324 agent_id: None,
325 trace_id: None,
326 },
327 RuntimeEvent::ThoughtDelta {
328 session_id: sid(2),
329 text: "hmm".into(),
330 agent_id: None,
331 trace_id: None,
332 },
333 RuntimeEvent::ToolCallStarted {
334 session_id: sid(3),
335 tool_name: "read".into(),
336 args_json: "{}".into(),
337 agent_id: None,
338 trace_id: None,
339 },
340 RuntimeEvent::ToolCallFinished {
341 session_id: sid(4),
342 tool_name: "read".into(),
343 summary: "ok".into(),
344 agent_id: None,
345 trace_id: None,
346 denied: false,
347 details: None,
348 },
349 RuntimeEvent::AwaitingApproval {
350 session_id: sid(5),
351 request: approval_request(),
352 agent_id: None,
353 trace_id: None,
354 },
355 RuntimeEvent::Checkpoint {
356 session_id: sid(6),
357 checkpoint: checkpoint(),
358 agent_id: None,
359 trace_id: None,
360 },
361 RuntimeEvent::RunFinished {
362 session_id: sid(7),
363 agent_id: None,
364 trace_id: None,
365 },
366 RuntimeEvent::RunCancelled {
367 session_id: sid(8),
368 agent_id: None,
369 trace_id: None,
370 },
371 RuntimeEvent::PlanUpdated {
372 session_id: sid(9),
373 objective: "goal".into(),
374 explanation: None,
375 plan: vec![PlanItem {
376 step: "s".into(),
377 status: PlanStepStatus::Pending,
378 }],
379 agent_id: None,
380 trace_id: None,
381 },
382 RuntimeEvent::UserEvent {
383 session_id: sid(10),
384 event: UserEvent::Progress { text: "p".into() },
385 agent_id: None,
386 trace_id: None,
387 },
388 ];
389
390 for (i, ev) in events.into_iter().enumerate() {
391 let tagged = ev.with_agent_id("sub/1");
392 assert_eq!(tagged.agent_id(), Some("sub/1"), "variant {i}");
393 assert_eq!(tagged.session_id(), &sid(i as u64 + 1), "variant {i}");
394 }
395 }
396
397 #[test]
398 fn accessors_return_none_when_ids_absent() {
399 let ev = RuntimeEvent::RunFinished {
400 session_id: sid(1),
401 agent_id: None,
402 trace_id: None,
403 };
404 assert_eq!(ev.session_id(), &sid(1));
405 assert_eq!(ev.agent_id(), None);
406 assert_eq!(ev.trace_id(), None);
407 }
408
409 #[test]
410 fn runtime_event_serde_uses_camel_case_tag() {
411 let ev = RuntimeEvent::ToolCallStarted {
412 session_id: SessionId::with_external_id(7, "ext"),
413 tool_name: "read".into(),
414 args_json: "{}".into(),
415 agent_id: Some("a".into()),
416 trace_id: None,
417 };
418 let v = serde_json::to_value(&ev).unwrap();
419 assert_eq!(v["runtimeEventType"], "toolCallStarted");
420 assert_eq!(v["session_id"]["id"], serde_json::json!(7));
421 assert_eq!(v["session_id"]["external_id"], "ext");
422 assert!(v.get("trace_id").is_none());
424
425 let back: RuntimeEvent = serde_json::from_value(v).unwrap();
426 assert_eq!(back.session_id(), &SessionId::with_external_id(7, "ext"));
427 assert_eq!(back.agent_id(), Some("a"));
428 assert_eq!(back.trace_id(), None);
429 }
430
431 #[test]
432 fn user_event_serde_tag() {
433 let v = serde_json::to_value(UserEvent::Progress {
434 text: "working".into(),
435 })
436 .unwrap();
437 assert_eq!(v["userEventType"], "progress");
438 assert_eq!(v["text"], "working");
439
440 let v = serde_json::to_value(UserEvent::Structured {
441 event_type: "custom".into(),
442 data: serde_json::json!({"k": 1}),
443 })
444 .unwrap();
445 assert_eq!(v["userEventType"], "structured");
446 assert_eq!(v["data"]["k"], serde_json::json!(1));
447 }
448}