tirea_protocol_ag_ui/
events.rs

1use crate::types::Role;
2use serde::{Deserialize, Serialize};
3use serde_json::{json, Value};
4use std::collections::HashMap;
5use tirea_contract::Suspension;
6use tracing::warn;
7
8// Base Event Fields
9// ============================================================================
10
11/// Common fields for all AG-UI events (BaseEvent).
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
13pub struct BaseEvent {
14    /// Event timestamp in milliseconds since epoch.
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub timestamp: Option<u64>,
17    /// Raw event data from external systems.
18    #[serde(rename = "rawEvent", skip_serializing_if = "Option::is_none")]
19    pub raw_event: Option<Value>,
20}
21
22/// Entity kind for `REASONING_ENCRYPTED_VALUE`.
23#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24#[serde(rename_all = "kebab-case")]
25pub enum ReasoningEncryptedValueSubtype {
26    ToolCall,
27    Message,
28}
29
30// ============================================================================
31// AG-UI Event Types
32// ============================================================================
33
34/// AG-UI Protocol Event Types.
35///
36/// These events follow the AG-UI specification for agent-to-frontend communication.
37/// See: <https://docs.ag-ui.com/concepts/events>
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39#[serde(tag = "type")]
40pub enum Event {
41    // ========================================================================
42    // Lifecycle Events
43    // ========================================================================
44    /// Signals the start of an agent run.
45    #[serde(rename = "RUN_STARTED")]
46    RunStarted {
47        #[serde(rename = "threadId")]
48        thread_id: String,
49        #[serde(rename = "runId")]
50        run_id: String,
51        #[serde(rename = "parentRunId", skip_serializing_if = "Option::is_none")]
52        parent_run_id: Option<String>,
53        #[serde(skip_serializing_if = "Option::is_none")]
54        input: Option<Value>,
55        #[serde(flatten)]
56        base: BaseEvent,
57    },
58
59    /// Signals successful completion of an agent run.
60    #[serde(rename = "RUN_FINISHED")]
61    RunFinished {
62        #[serde(rename = "threadId")]
63        thread_id: String,
64        #[serde(rename = "runId")]
65        run_id: String,
66        #[serde(skip_serializing_if = "Option::is_none")]
67        result: Option<Value>,
68        #[serde(flatten)]
69        base: BaseEvent,
70    },
71
72    /// Indicates an error occurred during the run.
73    #[serde(rename = "RUN_ERROR")]
74    RunError {
75        message: String,
76        #[serde(skip_serializing_if = "Option::is_none")]
77        code: Option<String>,
78        #[serde(flatten)]
79        base: BaseEvent,
80    },
81
82    /// Marks the beginning of a step within a run.
83    #[serde(rename = "STEP_STARTED")]
84    StepStarted {
85        #[serde(rename = "stepName")]
86        step_name: String,
87        #[serde(flatten)]
88        base: BaseEvent,
89    },
90
91    /// Marks the completion of a step.
92    #[serde(rename = "STEP_FINISHED")]
93    StepFinished {
94        #[serde(rename = "stepName")]
95        step_name: String,
96        #[serde(flatten)]
97        base: BaseEvent,
98    },
99
100    // ========================================================================
101    // Text Message Events
102    // ========================================================================
103    /// Indicates the beginning of a text message stream.
104    #[serde(rename = "TEXT_MESSAGE_START")]
105    TextMessageStart {
106        #[serde(rename = "messageId")]
107        message_id: String,
108        /// Role is always "assistant" for TEXT_MESSAGE_START.
109        role: Role,
110        #[serde(flatten)]
111        base: BaseEvent,
112    },
113
114    /// Contains incremental text content.
115    #[serde(rename = "TEXT_MESSAGE_CONTENT")]
116    TextMessageContent {
117        #[serde(rename = "messageId")]
118        message_id: String,
119        delta: String,
120        #[serde(flatten)]
121        base: BaseEvent,
122    },
123
124    /// Indicates the end of a text message stream.
125    #[serde(rename = "TEXT_MESSAGE_END")]
126    TextMessageEnd {
127        #[serde(rename = "messageId")]
128        message_id: String,
129        #[serde(flatten)]
130        base: BaseEvent,
131    },
132
133    /// Combined chunk event for text messages (alternative to Start/Content/End).
134    #[serde(rename = "TEXT_MESSAGE_CHUNK")]
135    TextMessageChunk {
136        #[serde(rename = "messageId", skip_serializing_if = "Option::is_none")]
137        message_id: Option<String>,
138        #[serde(skip_serializing_if = "Option::is_none")]
139        role: Option<Role>,
140        #[serde(skip_serializing_if = "Option::is_none")]
141        delta: Option<String>,
142        #[serde(flatten)]
143        base: BaseEvent,
144    },
145
146    // ========================================================================
147    // Reasoning Events
148    // ========================================================================
149    /// Marks the start of a reasoning phase for a message.
150    #[serde(rename = "REASONING_START")]
151    ReasoningStart {
152        #[serde(rename = "messageId")]
153        message_id: String,
154        #[serde(flatten)]
155        base: BaseEvent,
156    },
157
158    /// Marks the start of a streamed reasoning message.
159    #[serde(rename = "REASONING_MESSAGE_START")]
160    ReasoningMessageStart {
161        #[serde(rename = "messageId")]
162        message_id: String,
163        role: Role,
164        #[serde(flatten)]
165        base: BaseEvent,
166    },
167
168    /// Contains incremental reasoning text.
169    #[serde(rename = "REASONING_MESSAGE_CONTENT")]
170    ReasoningMessageContent {
171        #[serde(rename = "messageId")]
172        message_id: String,
173        delta: String,
174        #[serde(flatten)]
175        base: BaseEvent,
176    },
177
178    /// Marks the end of a streamed reasoning message.
179    #[serde(rename = "REASONING_MESSAGE_END")]
180    ReasoningMessageEnd {
181        #[serde(rename = "messageId")]
182        message_id: String,
183        #[serde(flatten)]
184        base: BaseEvent,
185    },
186
187    /// Combined reasoning chunk event (alternative to Start/Content/End).
188    #[serde(rename = "REASONING_MESSAGE_CHUNK")]
189    ReasoningMessageChunk {
190        #[serde(rename = "messageId", skip_serializing_if = "Option::is_none")]
191        message_id: Option<String>,
192        #[serde(skip_serializing_if = "Option::is_none")]
193        delta: Option<String>,
194        #[serde(flatten)]
195        base: BaseEvent,
196    },
197
198    /// Marks the end of a reasoning phase for a message.
199    #[serde(rename = "REASONING_END")]
200    ReasoningEnd {
201        #[serde(rename = "messageId")]
202        message_id: String,
203        #[serde(flatten)]
204        base: BaseEvent,
205    },
206
207    /// Attaches an encrypted reasoning value to a message or tool call.
208    #[serde(rename = "REASONING_ENCRYPTED_VALUE")]
209    ReasoningEncryptedValue {
210        subtype: ReasoningEncryptedValueSubtype,
211        #[serde(rename = "entityId")]
212        entity_id: String,
213        #[serde(rename = "encryptedValue")]
214        encrypted_value: String,
215        #[serde(flatten)]
216        base: BaseEvent,
217    },
218
219    // ========================================================================
220    // Tool Call Events
221    // ========================================================================
222    /// Signals the start of a tool call.
223    #[serde(rename = "TOOL_CALL_START")]
224    ToolCallStart {
225        #[serde(rename = "toolCallId")]
226        tool_call_id: String,
227        #[serde(rename = "toolCallName")]
228        tool_call_name: String,
229        #[serde(rename = "parentMessageId", skip_serializing_if = "Option::is_none")]
230        parent_message_id: Option<String>,
231        #[serde(flatten)]
232        base: BaseEvent,
233    },
234
235    /// Contains incremental tool arguments.
236    #[serde(rename = "TOOL_CALL_ARGS")]
237    ToolCallArgs {
238        #[serde(rename = "toolCallId")]
239        tool_call_id: String,
240        delta: String,
241        #[serde(flatten)]
242        base: BaseEvent,
243    },
244
245    /// Signals the end of tool argument streaming.
246    #[serde(rename = "TOOL_CALL_END")]
247    ToolCallEnd {
248        #[serde(rename = "toolCallId")]
249        tool_call_id: String,
250        #[serde(flatten)]
251        base: BaseEvent,
252    },
253
254    /// Contains the result of a tool execution.
255    #[serde(rename = "TOOL_CALL_RESULT")]
256    ToolCallResult {
257        #[serde(rename = "messageId")]
258        message_id: String,
259        #[serde(rename = "toolCallId")]
260        tool_call_id: String,
261        content: String,
262        #[serde(skip_serializing_if = "Option::is_none")]
263        role: Option<Role>,
264        #[serde(flatten)]
265        base: BaseEvent,
266    },
267
268    /// Combined chunk event for tool calls (alternative to Start/Args/End).
269    #[serde(rename = "TOOL_CALL_CHUNK")]
270    ToolCallChunk {
271        #[serde(rename = "toolCallId", skip_serializing_if = "Option::is_none")]
272        tool_call_id: Option<String>,
273        #[serde(rename = "toolCallName", skip_serializing_if = "Option::is_none")]
274        tool_call_name: Option<String>,
275        #[serde(rename = "parentMessageId", skip_serializing_if = "Option::is_none")]
276        parent_message_id: Option<String>,
277        #[serde(skip_serializing_if = "Option::is_none")]
278        delta: Option<String>,
279        #[serde(flatten)]
280        base: BaseEvent,
281    },
282
283    // ========================================================================
284    // State Management Events
285    // ========================================================================
286    /// Provides a complete state snapshot.
287    #[serde(rename = "STATE_SNAPSHOT")]
288    StateSnapshot {
289        snapshot: Value,
290        #[serde(flatten)]
291        base: BaseEvent,
292    },
293
294    /// Contains incremental state changes (RFC 6902 JSON Patch).
295    #[serde(rename = "STATE_DELTA")]
296    StateDelta {
297        /// Array of JSON Patch operations.
298        delta: Vec<Value>,
299        #[serde(flatten)]
300        base: BaseEvent,
301    },
302
303    /// Provides a complete message history snapshot.
304    #[serde(rename = "MESSAGES_SNAPSHOT")]
305    MessagesSnapshot {
306        messages: Vec<Value>,
307        #[serde(flatten)]
308        base: BaseEvent,
309    },
310
311    // ========================================================================
312    // Activity Events
313    // ========================================================================
314    /// Provides an activity snapshot.
315    #[serde(rename = "ACTIVITY_SNAPSHOT")]
316    ActivitySnapshot {
317        #[serde(rename = "messageId")]
318        message_id: String,
319        #[serde(rename = "activityType")]
320        activity_type: String,
321        content: HashMap<String, Value>,
322        #[serde(skip_serializing_if = "Option::is_none")]
323        replace: Option<bool>,
324        #[serde(flatten)]
325        base: BaseEvent,
326    },
327
328    /// Contains incremental activity changes (RFC 6902 JSON Patch).
329    #[serde(rename = "ACTIVITY_DELTA")]
330    ActivityDelta {
331        #[serde(rename = "messageId")]
332        message_id: String,
333        #[serde(rename = "activityType")]
334        activity_type: String,
335        /// Array of JSON Patch operations.
336        patch: Vec<Value>,
337        #[serde(flatten)]
338        base: BaseEvent,
339    },
340
341    // ========================================================================
342    // Special Events
343    // ========================================================================
344    /// Wraps events from external systems.
345    #[serde(rename = "RAW")]
346    Raw {
347        event: Value,
348        #[serde(skip_serializing_if = "Option::is_none")]
349        source: Option<String>,
350        #[serde(flatten)]
351        base: BaseEvent,
352    },
353
354    /// Custom application-defined event.
355    #[serde(rename = "CUSTOM")]
356    Custom {
357        name: String,
358        value: Value,
359        #[serde(flatten)]
360        base: BaseEvent,
361    },
362}
363
364impl Event {
365    // ========================================================================
366    // Factory Methods - Lifecycle
367    // ========================================================================
368
369    /// Create a run-started event.
370    pub fn run_started(
371        thread_id: impl Into<String>,
372        run_id: impl Into<String>,
373        parent_run_id: Option<String>,
374    ) -> Self {
375        Self::RunStarted {
376            thread_id: thread_id.into(),
377            run_id: run_id.into(),
378            parent_run_id,
379            input: None,
380            base: BaseEvent::default(),
381        }
382    }
383
384    /// Create a run-started event with input.
385    pub fn run_started_with_input(
386        thread_id: impl Into<String>,
387        run_id: impl Into<String>,
388        parent_run_id: Option<String>,
389        input: Value,
390    ) -> Self {
391        Self::RunStarted {
392            thread_id: thread_id.into(),
393            run_id: run_id.into(),
394            parent_run_id,
395            input: Some(input),
396            base: BaseEvent::default(),
397        }
398    }
399
400    /// Create a run-finished event.
401    pub fn run_finished(
402        thread_id: impl Into<String>,
403        run_id: impl Into<String>,
404        result: Option<Value>,
405    ) -> Self {
406        Self::RunFinished {
407            thread_id: thread_id.into(),
408            run_id: run_id.into(),
409            result,
410            base: BaseEvent::default(),
411        }
412    }
413
414    /// Create a run-error event.
415    pub fn run_error(message: impl Into<String>, code: Option<String>) -> Self {
416        Self::RunError {
417            message: message.into(),
418            code,
419            base: BaseEvent::default(),
420        }
421    }
422
423    /// Create a step-started event.
424    pub fn step_started(step_name: impl Into<String>) -> Self {
425        Self::StepStarted {
426            step_name: step_name.into(),
427            base: BaseEvent::default(),
428        }
429    }
430
431    /// Create a step-finished event.
432    pub fn step_finished(step_name: impl Into<String>) -> Self {
433        Self::StepFinished {
434            step_name: step_name.into(),
435            base: BaseEvent::default(),
436        }
437    }
438
439    // ========================================================================
440    // Factory Methods - Text Message
441    // ========================================================================
442
443    /// Create a text-message-start event.
444    pub fn text_message_start(message_id: impl Into<String>) -> Self {
445        Self::TextMessageStart {
446            message_id: message_id.into(),
447            role: Role::Assistant,
448            base: BaseEvent::default(),
449        }
450    }
451
452    /// Create a text-message-content event.
453    pub fn text_message_content(message_id: impl Into<String>, delta: impl Into<String>) -> Self {
454        Self::TextMessageContent {
455            message_id: message_id.into(),
456            delta: delta.into(),
457            base: BaseEvent::default(),
458        }
459    }
460
461    /// Create a text-message-end event.
462    pub fn text_message_end(message_id: impl Into<String>) -> Self {
463        Self::TextMessageEnd {
464            message_id: message_id.into(),
465            base: BaseEvent::default(),
466        }
467    }
468
469    /// Create a text-message-chunk event.
470    pub fn text_message_chunk(
471        message_id: Option<String>,
472        role: Option<Role>,
473        delta: Option<String>,
474    ) -> Self {
475        Self::TextMessageChunk {
476            message_id,
477            role,
478            delta,
479            base: BaseEvent::default(),
480        }
481    }
482
483    // ========================================================================
484    // Factory Methods - Reasoning
485    // ========================================================================
486
487    /// Create a reasoning-start event.
488    pub fn reasoning_start(message_id: impl Into<String>) -> Self {
489        Self::ReasoningStart {
490            message_id: message_id.into(),
491            base: BaseEvent::default(),
492        }
493    }
494
495    /// Create a reasoning-message-start event.
496    pub fn reasoning_message_start(message_id: impl Into<String>) -> Self {
497        Self::ReasoningMessageStart {
498            message_id: message_id.into(),
499            role: Role::Assistant,
500            base: BaseEvent::default(),
501        }
502    }
503
504    /// Create a reasoning-message-content event.
505    pub fn reasoning_message_content(
506        message_id: impl Into<String>,
507        delta: impl Into<String>,
508    ) -> Self {
509        Self::ReasoningMessageContent {
510            message_id: message_id.into(),
511            delta: delta.into(),
512            base: BaseEvent::default(),
513        }
514    }
515
516    /// Create a reasoning-message-end event.
517    pub fn reasoning_message_end(message_id: impl Into<String>) -> Self {
518        Self::ReasoningMessageEnd {
519            message_id: message_id.into(),
520            base: BaseEvent::default(),
521        }
522    }
523
524    /// Create a reasoning-message-chunk event.
525    pub fn reasoning_message_chunk(message_id: Option<String>, delta: Option<String>) -> Self {
526        Self::ReasoningMessageChunk {
527            message_id,
528            delta,
529            base: BaseEvent::default(),
530        }
531    }
532
533    /// Create a reasoning-end event.
534    pub fn reasoning_end(message_id: impl Into<String>) -> Self {
535        Self::ReasoningEnd {
536            message_id: message_id.into(),
537            base: BaseEvent::default(),
538        }
539    }
540
541    /// Create a reasoning-encrypted-value event.
542    pub fn reasoning_encrypted_value(
543        subtype: ReasoningEncryptedValueSubtype,
544        entity_id: impl Into<String>,
545        encrypted_value: impl Into<String>,
546    ) -> Self {
547        Self::ReasoningEncryptedValue {
548            subtype,
549            entity_id: entity_id.into(),
550            encrypted_value: encrypted_value.into(),
551            base: BaseEvent::default(),
552        }
553    }
554
555    // ========================================================================
556    // Factory Methods - Tool Call
557    // ========================================================================
558
559    /// Create a tool-call-start event.
560    pub fn tool_call_start(
561        tool_call_id: impl Into<String>,
562        tool_call_name: impl Into<String>,
563        parent_message_id: Option<String>,
564    ) -> Self {
565        Self::ToolCallStart {
566            tool_call_id: tool_call_id.into(),
567            tool_call_name: tool_call_name.into(),
568            parent_message_id,
569            base: BaseEvent::default(),
570        }
571    }
572
573    /// Create a tool-call-args event.
574    pub fn tool_call_args(tool_call_id: impl Into<String>, delta: impl Into<String>) -> Self {
575        Self::ToolCallArgs {
576            tool_call_id: tool_call_id.into(),
577            delta: delta.into(),
578            base: BaseEvent::default(),
579        }
580    }
581
582    /// Create a tool-call-end event.
583    pub fn tool_call_end(tool_call_id: impl Into<String>) -> Self {
584        Self::ToolCallEnd {
585            tool_call_id: tool_call_id.into(),
586            base: BaseEvent::default(),
587        }
588    }
589
590    /// Create a tool-call-result event.
591    pub fn tool_call_result(
592        message_id: impl Into<String>,
593        tool_call_id: impl Into<String>,
594        content: impl Into<String>,
595    ) -> Self {
596        Self::ToolCallResult {
597            message_id: message_id.into(),
598            tool_call_id: tool_call_id.into(),
599            content: content.into(),
600            role: Some(Role::Tool),
601            base: BaseEvent::default(),
602        }
603    }
604
605    /// Create a tool-call-chunk event.
606    pub fn tool_call_chunk(
607        tool_call_id: Option<String>,
608        tool_call_name: Option<String>,
609        parent_message_id: Option<String>,
610        delta: Option<String>,
611    ) -> Self {
612        Self::ToolCallChunk {
613            tool_call_id,
614            tool_call_name,
615            parent_message_id,
616            delta,
617            base: BaseEvent::default(),
618        }
619    }
620
621    // ========================================================================
622    // Factory Methods - State
623    // ========================================================================
624
625    /// Create a state-snapshot event.
626    pub fn state_snapshot(snapshot: Value) -> Self {
627        Self::StateSnapshot {
628            snapshot,
629            base: BaseEvent::default(),
630        }
631    }
632
633    /// Create a state-delta event.
634    pub fn state_delta(delta: Vec<Value>) -> Self {
635        Self::StateDelta {
636            delta,
637            base: BaseEvent::default(),
638        }
639    }
640
641    /// Create a messages-snapshot event.
642    pub fn messages_snapshot(messages: Vec<Value>) -> Self {
643        Self::MessagesSnapshot {
644            messages,
645            base: BaseEvent::default(),
646        }
647    }
648
649    // ========================================================================
650    // Factory Methods - Activity
651    // ========================================================================
652
653    /// Create an activity-snapshot event.
654    pub fn activity_snapshot(
655        message_id: impl Into<String>,
656        activity_type: impl Into<String>,
657        content: HashMap<String, Value>,
658        replace: Option<bool>,
659    ) -> Self {
660        Self::ActivitySnapshot {
661            message_id: message_id.into(),
662            activity_type: activity_type.into(),
663            content,
664            replace,
665            base: BaseEvent::default(),
666        }
667    }
668
669    /// Create an activity-delta event.
670    pub fn activity_delta(
671        message_id: impl Into<String>,
672        activity_type: impl Into<String>,
673        patch: Vec<Value>,
674    ) -> Self {
675        Self::ActivityDelta {
676            message_id: message_id.into(),
677            activity_type: activity_type.into(),
678            patch,
679            base: BaseEvent::default(),
680        }
681    }
682
683    // ========================================================================
684    // Factory Methods - Special
685    // ========================================================================
686
687    /// Create a raw event.
688    pub fn raw(event: Value, source: Option<String>) -> Self {
689        Self::Raw {
690            event,
691            source,
692            base: BaseEvent::default(),
693        }
694    }
695
696    /// Create a custom event.
697    pub fn custom(name: impl Into<String>, value: Value) -> Self {
698        Self::Custom {
699            name: name.into(),
700            value,
701            base: BaseEvent::default(),
702        }
703    }
704
705    // ========================================================================
706    // Utility Methods
707    // ========================================================================
708
709    /// Set timestamp on the event.
710    pub fn with_timestamp(mut self, timestamp: u64) -> Self {
711        match &mut self {
712            Self::RunStarted { base, .. }
713            | Self::RunFinished { base, .. }
714            | Self::RunError { base, .. }
715            | Self::StepStarted { base, .. }
716            | Self::StepFinished { base, .. }
717            | Self::TextMessageStart { base, .. }
718            | Self::TextMessageContent { base, .. }
719            | Self::TextMessageEnd { base, .. }
720            | Self::TextMessageChunk { base, .. }
721            | Self::ReasoningStart { base, .. }
722            | Self::ReasoningMessageStart { base, .. }
723            | Self::ReasoningMessageContent { base, .. }
724            | Self::ReasoningMessageEnd { base, .. }
725            | Self::ReasoningMessageChunk { base, .. }
726            | Self::ReasoningEnd { base, .. }
727            | Self::ReasoningEncryptedValue { base, .. }
728            | Self::ToolCallStart { base, .. }
729            | Self::ToolCallArgs { base, .. }
730            | Self::ToolCallEnd { base, .. }
731            | Self::ToolCallResult { base, .. }
732            | Self::ToolCallChunk { base, .. }
733            | Self::StateSnapshot { base, .. }
734            | Self::StateDelta { base, .. }
735            | Self::MessagesSnapshot { base, .. }
736            | Self::ActivitySnapshot { base, .. }
737            | Self::ActivityDelta { base, .. }
738            | Self::Raw { base, .. }
739            | Self::Custom { base, .. } => {
740                base.timestamp = Some(timestamp);
741            }
742        }
743        self
744    }
745
746    /// Get current timestamp in milliseconds.
747    pub fn now_millis() -> u64 {
748        use std::time::{SystemTime, UNIX_EPOCH};
749        SystemTime::now()
750            .duration_since(UNIX_EPOCH)
751            .map(|d| d.as_millis() as u64)
752            .unwrap_or(0)
753    }
754}
755
756// ============================================================================
757// Suspension to AG-UI Conversion
758// ============================================================================
759
760/// Convert one interaction to AG-UI tool call events.
761pub fn interaction_to_ag_ui_events(interaction: &Suspension) -> Vec<Event> {
762    let args = json!({
763        "id": interaction.id,
764        "message": interaction.message,
765        "parameters": interaction.parameters,
766        "response_schema": interaction.response_schema,
767    });
768
769    // Strip "tool:" prefix from action to produce the AG-UI toolCallName.
770    // Internally, interactions use "tool:addTask" convention for routing,
771    // but the AG-UI protocol expects the bare tool name "addTask".
772    let tool_name = interaction
773        .action
774        .strip_prefix("tool:")
775        .unwrap_or(&interaction.action);
776
777    vec![
778        Event::tool_call_start(&interaction.id, tool_name, None),
779        Event::tool_call_args(
780            &interaction.id,
781            match serde_json::to_string(&args) {
782                Ok(value) => value,
783                Err(err) => {
784                    warn!(
785                        error = %err,
786                        target_id = %interaction.id,
787                        "failed to serialize interaction arguments for AG-UI"
788                    );
789                    "{}".to_string()
790                }
791            },
792        ),
793        Event::tool_call_end(&interaction.id),
794    ]
795}
796
797// ============================================================================