1use router_protocol::{CallId, CanonicalEvent, ItemId, OutputItemKind, ResponseId};
4use serde_json::{Value, json};
5use std::collections::HashMap;
6use thiserror::Error;
7
8#[derive(Debug, Error, PartialEq, Eq)]
9pub enum CodexSseEncodeError {
10 #[error("canonical event sequence is invalid for Codex Responses SSE")]
11 InvalidSequence,
12 #[error("canonical event data could not be encoded for Codex Responses SSE")]
13 InvalidData,
14}
15
16#[derive(Clone, Debug)]
17enum ActiveItem {
18 Message {
19 id: ItemId,
20 text: String,
21 done: bool,
22 },
23 FunctionCall {
24 id: ItemId,
25 call_id: CallId,
26 name: String,
27 namespace: Option<String>,
28 arguments: String,
29 done: bool,
30 },
31}
32
33impl ActiveItem {
34 fn wire_value(&self, status: &'static str) -> Value {
35 match self {
36 Self::Message { id, text, .. } => json!({
37 "id": id.as_str(),
38 "type": "message",
39 "status": status,
40 "role": "assistant",
41 "content": if text.is_empty() {
42 Vec::<Value>::new()
43 } else {
44 vec![json!({
45 "type": "output_text",
46 "text": text,
47 "annotations": []
48 })]
49 }
50 }),
51 Self::FunctionCall {
52 id,
53 call_id,
54 name,
55 namespace,
56 arguments,
57 ..
58 } => {
59 let mut item = json!({
60 "id": id.as_str(),
61 "type": "function_call",
62 "status": status,
63 "call_id": call_id.as_str(),
64 "name": name,
65 "arguments": arguments
66 });
67 if let Some(namespace) = namespace {
68 item["namespace"] = Value::String(namespace.clone());
69 }
70 item
71 }
72 }
73 }
74
75 fn is_done(&self) -> bool {
76 match self {
77 Self::Message { done, .. } | Self::FunctionCall { done, .. } => *done,
78 }
79 }
80}
81
82pub struct CodexSseEncoder {
83 model: String,
84 response_id: Option<ResponseId>,
85 items: HashMap<ItemId, ActiveItem>,
86 item_order: Vec<ItemId>,
87}
88
89impl CodexSseEncoder {
90 pub fn new(model: impl Into<String>) -> Self {
91 Self {
92 model: model.into(),
93 response_id: None,
94 items: HashMap::new(),
95 item_order: Vec::new(),
96 }
97 }
98
99 pub fn encode(&mut self, event: &CanonicalEvent) -> Result<Value, CodexSseEncodeError> {
100 match event {
101 CanonicalEvent::ResponseCreated { response_id, .. } => {
102 if self.response_id.replace(response_id.clone()).is_some() {
103 return Err(CodexSseEncodeError::InvalidSequence);
104 }
105 Ok(json!({
106 "type": "response.created",
107 "sequence_number": event.sequence(),
108 "response": self.response_value(response_id, "in_progress", None)
109 }))
110 }
111 CanonicalEvent::ResponseInProgress {
112 response_id,
113 sequence,
114 } => {
115 self.ensure_response(response_id)?;
116 Ok(json!({
117 "type": "response.in_progress",
118 "sequence_number": sequence,
119 "response": self.response_value(response_id, "in_progress", None)
120 }))
121 }
122 CanonicalEvent::OutputItemAdded {
123 response_id,
124 item_id,
125 sequence,
126 item_kind,
127 call_id,
128 name,
129 namespace,
130 } => {
131 self.ensure_response(response_id)?;
132 if self.items.contains_key(item_id) {
133 return Err(CodexSseEncodeError::InvalidSequence);
134 }
135 let item = match item_kind {
136 OutputItemKind::Message
137 if call_id.is_none() && name.is_none() && namespace.is_none() =>
138 {
139 ActiveItem::Message {
140 id: item_id.clone(),
141 text: String::new(),
142 done: false,
143 }
144 }
145 OutputItemKind::FunctionCall => ActiveItem::FunctionCall {
146 id: item_id.clone(),
147 call_id: call_id.clone().ok_or(CodexSseEncodeError::InvalidData)?,
148 name: name.clone().ok_or(CodexSseEncodeError::InvalidData)?,
149 namespace: namespace.clone(),
150 arguments: String::new(),
151 done: false,
152 },
153 OutputItemKind::Message => return Err(CodexSseEncodeError::InvalidData),
154 };
155 let output_index = self.item_order.len();
156 let wire_item = item.wire_value("in_progress");
157 self.item_order.push(item_id.clone());
158 self.items.insert(item_id.clone(), item);
159 Ok(json!({
160 "type": "response.output_item.added",
161 "sequence_number": sequence,
162 "output_index": output_index,
163 "item": wire_item
164 }))
165 }
166 CanonicalEvent::ContentPartAdded {
167 response_id,
168 item_id,
169 sequence,
170 } => {
171 self.ensure_message(response_id, item_id)?;
172 Ok(json!({
173 "type": "response.content_part.added",
174 "sequence_number": sequence,
175 "item_id": item_id.as_str(),
176 "output_index": self.output_index(item_id)?,
177 "content_index": 0,
178 "part": {"type": "output_text", "text": "", "annotations": []}
179 }))
180 }
181 CanonicalEvent::OutputTextDelta {
182 response_id,
183 item_id,
184 sequence,
185 delta,
186 } => {
187 let output_index = self.output_index(item_id)?;
188 let item = self.ensure_message_mut(response_id, item_id)?;
189 let ActiveItem::Message { text, done, .. } = item else {
190 return Err(CodexSseEncodeError::InvalidSequence);
191 };
192 if *done {
193 return Err(CodexSseEncodeError::InvalidSequence);
194 }
195 text.push_str(delta);
196 Ok(json!({
197 "type": "response.output_text.delta",
198 "sequence_number": sequence,
199 "item_id": item_id.as_str(),
200 "output_index": output_index,
201 "content_index": 0,
202 "delta": delta
203 }))
204 }
205 CanonicalEvent::OutputTextDone {
206 response_id,
207 item_id,
208 sequence,
209 text,
210 } => {
211 let output_index = self.output_index(item_id)?;
212 let item = self.ensure_message_mut(response_id, item_id)?;
213 let ActiveItem::Message {
214 text: accumulated,
215 done,
216 ..
217 } = item
218 else {
219 return Err(CodexSseEncodeError::InvalidSequence);
220 };
221 if *done {
222 return Err(CodexSseEncodeError::InvalidSequence);
223 }
224 accumulated.clone_from(text);
225 Ok(json!({
226 "type": "response.output_text.done",
227 "sequence_number": sequence,
228 "item_id": item_id.as_str(),
229 "output_index": output_index,
230 "content_index": 0,
231 "text": text
232 }))
233 }
234 CanonicalEvent::ContentPartDone {
235 response_id,
236 item_id,
237 sequence,
238 } => {
239 let output_index = self.output_index(item_id)?;
240 let item = self.ensure_message(response_id, item_id)?;
241 let ActiveItem::Message { text, .. } = item else {
242 return Err(CodexSseEncodeError::InvalidSequence);
243 };
244 Ok(json!({
245 "type": "response.content_part.done",
246 "sequence_number": sequence,
247 "item_id": item_id.as_str(),
248 "output_index": output_index,
249 "content_index": 0,
250 "part": {"type": "output_text", "text": text, "annotations": []}
251 }))
252 }
253 CanonicalEvent::FunctionCallArgumentsDelta {
254 response_id,
255 item_id,
256 call_id,
257 sequence,
258 delta,
259 } => {
260 let output_index = self.output_index(item_id)?;
261 let item = self.ensure_function_mut(response_id, item_id, call_id)?;
262 let ActiveItem::FunctionCall {
263 arguments, done, ..
264 } = item
265 else {
266 return Err(CodexSseEncodeError::InvalidSequence);
267 };
268 if *done {
269 return Err(CodexSseEncodeError::InvalidSequence);
270 }
271 arguments.push_str(delta);
272 Ok(json!({
273 "type": "response.function_call_arguments.delta",
274 "sequence_number": sequence,
275 "item_id": item_id.as_str(),
276 "output_index": output_index,
277 "delta": delta
278 }))
279 }
280 CanonicalEvent::FunctionCallArgumentsDone {
281 response_id,
282 item_id,
283 call_id,
284 sequence,
285 arguments,
286 } => {
287 let output_index = self.output_index(item_id)?;
288 let encoded_arguments = serde_json::to_string(arguments)
289 .map_err(|_| CodexSseEncodeError::InvalidData)?;
290 let item = self.ensure_function_mut(response_id, item_id, call_id)?;
291 let ActiveItem::FunctionCall {
292 arguments, done, ..
293 } = item
294 else {
295 return Err(CodexSseEncodeError::InvalidSequence);
296 };
297 if *done {
298 return Err(CodexSseEncodeError::InvalidSequence);
299 }
300 arguments.clone_from(&encoded_arguments);
301 Ok(json!({
302 "type": "response.function_call_arguments.done",
303 "sequence_number": sequence,
304 "item_id": item_id.as_str(),
305 "output_index": output_index,
306 "arguments": encoded_arguments
307 }))
308 }
309 CanonicalEvent::OutputItemDone {
310 response_id,
311 item_id,
312 sequence,
313 call_id,
314 } => {
315 self.ensure_response(response_id)?;
316 let output_index = self.output_index(item_id)?;
317 let item = self
318 .items
319 .get_mut(item_id)
320 .ok_or(CodexSseEncodeError::InvalidSequence)?;
321 match item {
322 ActiveItem::Message { done, .. } if call_id.is_none() => *done = true,
323 ActiveItem::FunctionCall {
324 call_id: expected,
325 done,
326 ..
327 } if call_id.as_ref() == Some(expected) => *done = true,
328 ActiveItem::Message { .. } | ActiveItem::FunctionCall { .. } => {
329 return Err(CodexSseEncodeError::InvalidSequence);
330 }
331 }
332 Ok(json!({
333 "type": "response.output_item.done",
334 "sequence_number": sequence,
335 "output_index": output_index,
336 "item": item.wire_value("completed")
337 }))
338 }
339 CanonicalEvent::ResponseCompleted {
340 response_id,
341 sequence,
342 usage,
343 } => {
344 self.ensure_response(response_id)?;
345 if self.items.values().any(|item| !item.is_done()) {
346 return Err(CodexSseEncodeError::InvalidSequence);
347 }
348 let total_tokens = usage
349 .input_tokens
350 .checked_add(usage.output_tokens)
351 .ok_or(CodexSseEncodeError::InvalidData)?;
352 let usage = json!({
353 "input_tokens": usage.input_tokens,
354 "input_tokens_details": {
355 "cached_tokens": 0,
356 "cache_write_tokens": 0
357 },
358 "output_tokens": usage.output_tokens,
359 "output_tokens_details": {"reasoning_tokens": 0},
360 "total_tokens": total_tokens
361 });
362 Ok(json!({
363 "type": "response.completed",
364 "sequence_number": sequence,
365 "response": self.response_value(response_id, "completed", Some(usage))
366 }))
367 }
368 CanonicalEvent::ResponseFailed {
369 response_id,
370 sequence,
371 error,
372 } => {
373 self.ensure_response(response_id)?;
374 Ok(json!({
375 "type": "response.failed",
376 "sequence_number": sequence,
377 "response": {
378 "id": response_id.as_str(),
379 "object": "response",
380 "status": "failed",
381 "error": {
382 "type": "server_error",
383 "code": error.code.as_str(),
384 "message": error.message,
385 "details": error.details
386 }
387 }
388 }))
389 }
390 CanonicalEvent::Error { sequence, error } => Ok(json!({
391 "type": "error",
392 "sequence_number": sequence,
393 "error": {
394 "type": "server_error",
395 "code": error.code.as_str(),
396 "message": error.message,
397 "details": error.details
398 }
399 })),
400 }
401 }
402
403 fn response_value(
404 &self,
405 response_id: &ResponseId,
406 status: &str,
407 usage: Option<Value>,
408 ) -> Value {
409 let output = self
410 .item_order
411 .iter()
412 .filter_map(|item_id| self.items.get(item_id))
413 .map(|item| {
414 item.wire_value(if item.is_done() {
415 "completed"
416 } else {
417 "in_progress"
418 })
419 })
420 .collect::<Vec<_>>();
421 json!({
422 "id": response_id.as_str(),
423 "object": "response",
424 "created_at": 0,
425 "status": status,
426 "model": self.model,
427 "output": output,
428 "usage": usage,
429 "error": null,
430 "incomplete_details": null
431 })
432 }
433
434 fn ensure_response(&self, response_id: &ResponseId) -> Result<(), CodexSseEncodeError> {
435 if self.response_id.as_ref() == Some(response_id) {
436 Ok(())
437 } else {
438 Err(CodexSseEncodeError::InvalidSequence)
439 }
440 }
441
442 fn output_index(&self, item_id: &ItemId) -> Result<usize, CodexSseEncodeError> {
443 self.item_order
444 .iter()
445 .position(|candidate| candidate == item_id)
446 .ok_or(CodexSseEncodeError::InvalidSequence)
447 }
448
449 fn ensure_message(
450 &self,
451 response_id: &ResponseId,
452 item_id: &ItemId,
453 ) -> Result<&ActiveItem, CodexSseEncodeError> {
454 self.ensure_response(response_id)?;
455 match self.items.get(item_id) {
456 Some(item @ ActiveItem::Message { .. }) => Ok(item),
457 Some(ActiveItem::FunctionCall { .. }) | None => {
458 Err(CodexSseEncodeError::InvalidSequence)
459 }
460 }
461 }
462
463 fn ensure_message_mut(
464 &mut self,
465 response_id: &ResponseId,
466 item_id: &ItemId,
467 ) -> Result<&mut ActiveItem, CodexSseEncodeError> {
468 self.ensure_response(response_id)?;
469 match self.items.get_mut(item_id) {
470 Some(item @ ActiveItem::Message { .. }) => Ok(item),
471 Some(ActiveItem::FunctionCall { .. }) | None => {
472 Err(CodexSseEncodeError::InvalidSequence)
473 }
474 }
475 }
476
477 fn ensure_function_mut(
478 &mut self,
479 response_id: &ResponseId,
480 item_id: &ItemId,
481 call_id: &CallId,
482 ) -> Result<&mut ActiveItem, CodexSseEncodeError> {
483 self.ensure_response(response_id)?;
484 let item = self
485 .items
486 .get_mut(item_id)
487 .ok_or(CodexSseEncodeError::InvalidSequence)?;
488 if matches!(
489 &*item,
490 ActiveItem::FunctionCall {
491 call_id: expected,
492 ..
493 } if expected == call_id
494 ) {
495 Ok(item)
496 } else {
497 Err(CodexSseEncodeError::InvalidSequence)
498 }
499 }
500}
501
502trait CanonicalEventSequence {
503 fn sequence(&self) -> u64;
504}
505
506impl CanonicalEventSequence for CanonicalEvent {
507 fn sequence(&self) -> u64 {
508 match self {
509 Self::ResponseCreated { sequence, .. }
510 | Self::ResponseInProgress { sequence, .. }
511 | Self::OutputItemAdded { sequence, .. }
512 | Self::OutputItemDone { sequence, .. }
513 | Self::ContentPartAdded { sequence, .. }
514 | Self::ContentPartDone { sequence, .. }
515 | Self::OutputTextDelta { sequence, .. }
516 | Self::OutputTextDone { sequence, .. }
517 | Self::FunctionCallArgumentsDelta { sequence, .. }
518 | Self::FunctionCallArgumentsDone { sequence, .. }
519 | Self::ResponseCompleted { sequence, .. }
520 | Self::ResponseFailed { sequence, .. }
521 | Self::Error { sequence, .. } => *sequence,
522 }
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529 use router_protocol::{
530 InterruptionCause, PublicError, PublicErrorCode, RequestId, SafeResumeBoundary, Usage,
531 };
532
533 fn response_id() -> ResponseId {
534 ResponseId::parse("resp-sse").expect("response ID")
535 }
536
537 fn item_id(value: &str) -> ItemId {
538 ItemId::parse(value).expect("item ID")
539 }
540
541 fn call_id() -> CallId {
542 CallId::parse("call-sse").expect("call ID")
543 }
544
545 fn event_types(events: &[Value]) -> Vec<&str> {
546 events
547 .iter()
548 .map(|event| event["type"].as_str().expect("event type"))
549 .collect()
550 }
551
552 #[test]
553 fn encodes_exact_message_and_namespaced_function_lifecycle() {
554 let response_id = response_id();
555 let message_id = item_id("msg-sse");
556 let function_id = item_id("fc-sse");
557 let call_id = call_id();
558 let canonical = vec![
559 CanonicalEvent::ResponseCreated {
560 response_id: response_id.clone(),
561 request_id: RequestId::parse("request-sse").expect("request ID"),
562 sequence: 0,
563 },
564 CanonicalEvent::ResponseInProgress {
565 response_id: response_id.clone(),
566 sequence: 1,
567 },
568 CanonicalEvent::OutputItemAdded {
569 response_id: response_id.clone(),
570 item_id: message_id.clone(),
571 sequence: 2,
572 item_kind: OutputItemKind::Message,
573 call_id: None,
574 name: None,
575 namespace: None,
576 },
577 CanonicalEvent::ContentPartAdded {
578 response_id: response_id.clone(),
579 item_id: message_id.clone(),
580 sequence: 3,
581 },
582 CanonicalEvent::OutputTextDelta {
583 response_id: response_id.clone(),
584 item_id: message_id.clone(),
585 sequence: 4,
586 delta: "Checking. ".to_owned(),
587 },
588 CanonicalEvent::OutputTextDone {
589 response_id: response_id.clone(),
590 item_id: message_id.clone(),
591 sequence: 5,
592 text: "Checking. ".to_owned(),
593 },
594 CanonicalEvent::ContentPartDone {
595 response_id: response_id.clone(),
596 item_id: message_id.clone(),
597 sequence: 6,
598 },
599 CanonicalEvent::OutputItemDone {
600 response_id: response_id.clone(),
601 item_id: message_id,
602 sequence: 7,
603 call_id: None,
604 },
605 CanonicalEvent::OutputItemAdded {
606 response_id: response_id.clone(),
607 item_id: function_id.clone(),
608 sequence: 8,
609 item_kind: OutputItemKind::FunctionCall,
610 call_id: Some(call_id.clone()),
611 name: Some("spawn_agent".to_owned()),
612 namespace: Some("collaboration".to_owned()),
613 },
614 CanonicalEvent::FunctionCallArgumentsDelta {
615 response_id: response_id.clone(),
616 item_id: function_id.clone(),
617 call_id: call_id.clone(),
618 sequence: 9,
619 delta: "{\"task\":".to_owned(),
620 },
621 CanonicalEvent::FunctionCallArgumentsDelta {
622 response_id: response_id.clone(),
623 item_id: function_id.clone(),
624 call_id: call_id.clone(),
625 sequence: 10,
626 delta: "\"fixture\"}".to_owned(),
627 },
628 CanonicalEvent::FunctionCallArgumentsDone {
629 response_id: response_id.clone(),
630 item_id: function_id.clone(),
631 call_id: call_id.clone(),
632 sequence: 11,
633 arguments: json!({"task": "fixture"}),
634 },
635 CanonicalEvent::OutputItemDone {
636 response_id: response_id.clone(),
637 item_id: function_id,
638 sequence: 12,
639 call_id: Some(call_id),
640 },
641 CanonicalEvent::ResponseCompleted {
642 response_id,
643 sequence: 13,
644 usage: Usage {
645 input_tokens: 17,
646 output_tokens: 11,
647 },
648 },
649 ];
650
651 let mut encoder = CodexSseEncoder::new("router-auto");
652 let encoded = canonical
653 .iter()
654 .map(|event| encoder.encode(event).expect("valid event sequence"))
655 .collect::<Vec<_>>();
656 assert_eq!(
657 event_types(&encoded),
658 [
659 "response.created",
660 "response.in_progress",
661 "response.output_item.added",
662 "response.content_part.added",
663 "response.output_text.delta",
664 "response.output_text.done",
665 "response.content_part.done",
666 "response.output_item.done",
667 "response.output_item.added",
668 "response.function_call_arguments.delta",
669 "response.function_call_arguments.delta",
670 "response.function_call_arguments.done",
671 "response.output_item.done",
672 "response.completed",
673 ]
674 );
675 assert_eq!(
676 encoded[2]
677 .as_object()
678 .expect("added event object")
679 .keys()
680 .map(String::as_str)
681 .collect::<Vec<_>>(),
682 ["item", "output_index", "sequence_number", "type"]
683 );
684 assert_eq!(encoded[2]["item"]["type"], "message");
685 assert_eq!(encoded[2]["item"]["content"], json!([]));
686 assert_eq!(encoded[4]["item_id"], "msg-sse");
687 assert_eq!(encoded[7]["item"]["content"][0]["text"], "Checking. ");
688 assert_eq!(encoded[8]["item"]["type"], "function_call");
689 assert_eq!(encoded[8]["item"]["namespace"], "collaboration");
690 assert_eq!(encoded[8]["item"]["name"], "spawn_agent");
691 assert_eq!(encoded[12]["item"]["arguments"], "{\"task\":\"fixture\"}");
692 assert_eq!(encoded[13]["response"]["status"], "completed");
693 assert_eq!(encoded[13]["response"]["usage"]["total_tokens"], 28);
694 assert_eq!(
695 encoded[13]["response"]["output"].as_array().map(Vec::len),
696 Some(2)
697 );
698 let rendered = serde_json::to_string(&encoded).expect("render encoded events");
699 assert!(!rendered.contains("encrypted_content"));
700 assert!(!rendered.contains("reasoning_summary"));
701 }
702
703 #[test]
704 fn rejects_text_delta_without_an_active_message() {
705 let response_id = response_id();
706 let mut encoder = CodexSseEncoder::new("router-auto");
707 encoder
708 .encode(&CanonicalEvent::ResponseCreated {
709 response_id: response_id.clone(),
710 request_id: RequestId::parse("request-sse").expect("request ID"),
711 sequence: 0,
712 })
713 .expect("created event");
714 assert_eq!(
715 encoder.encode(&CanonicalEvent::OutputTextDelta {
716 response_id,
717 item_id: item_id("missing-item"),
718 sequence: 1,
719 delta: "must fail".to_owned(),
720 }),
721 Err(CodexSseEncodeError::InvalidSequence)
722 );
723 }
724
725 #[test]
726 fn rejects_terminal_usage_total_overflow() {
727 let response_id = response_id();
728 let mut encoder = CodexSseEncoder::new("router-auto");
729 encoder
730 .encode(&CanonicalEvent::ResponseCreated {
731 response_id: response_id.clone(),
732 request_id: RequestId::parse("request-sse").expect("request ID"),
733 sequence: 0,
734 })
735 .expect("created event");
736 assert_eq!(
737 encoder.encode(&CanonicalEvent::ResponseCompleted {
738 response_id,
739 sequence: 1,
740 usage: Usage {
741 input_tokens: u64::MAX,
742 output_tokens: 1,
743 },
744 }),
745 Err(CodexSseEncodeError::InvalidData)
746 );
747 }
748
749 #[test]
750 fn response_failure_is_terminal_and_redacted() {
751 let response_id = response_id();
752 let mut encoder = CodexSseEncoder::new("router-auto");
753 encoder
754 .encode(&CanonicalEvent::ResponseCreated {
755 response_id: response_id.clone(),
756 request_id: RequestId::parse("request-sse").expect("request ID"),
757 sequence: 0,
758 })
759 .expect("created event");
760 let error = PublicError::redacted(
761 PublicErrorCode::ProviderFailure,
762 RequestId::parse("request-sse").expect("request ID"),
763 false,
764 );
765 let event = encoder
766 .encode(&CanonicalEvent::ResponseFailed {
767 response_id,
768 sequence: 1,
769 error,
770 })
771 .expect("failed response");
772 assert_eq!(event["type"], "response.failed");
773 assert_eq!(event["response"]["status"], "failed");
774 assert_eq!(event["response"]["error"]["code"], "provider_failure");
775 assert_eq!(event["response"].get("output"), None);
776 }
777
778 #[test]
779 fn response_failure_preserves_fixed_safe_resume_details() {
780 let response_id = response_id();
781 let mut encoder = CodexSseEncoder::new("router-auto");
782 encoder
783 .encode(&CanonicalEvent::ResponseCreated {
784 response_id: response_id.clone(),
785 request_id: RequestId::parse("request-resume").expect("request ID"),
786 sequence: 0,
787 })
788 .expect("created event");
789 let event = encoder
790 .encode(&CanonicalEvent::ResponseFailed {
791 response_id,
792 sequence: 1,
793 error: PublicError::interrupted(
794 RequestId::parse("request-resume").expect("request ID"),
795 InterruptionCause::Timeout,
796 SafeResumeBoundary::ToolCall,
797 ),
798 })
799 .expect("failed response");
800 let interruption = &event["response"]["error"]["details"]["interruption"];
801 assert_eq!(interruption["cause"], "timeout");
802 assert_eq!(interruption["boundary"], "tool_call");
803 assert_eq!(interruption["automatic_replay"], false);
804 assert_eq!(interruption["safe_resume"]["action"], "start_new_turn");
805 assert_eq!(
806 interruption["safe_resume"]["repeat_completed_tool_calls"],
807 false
808 );
809 }
810}