Skip to main content

harness_codex/
sse.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3use 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}