tirea_contract/io/event/stream/
definition.rs

1//! Single source of truth for all `AgentEvent` variants.
2//!
3//! The [`define_agent_events!`] macro generates from a single declaration:
4//! - `AgentEvent` enum
5//! - `AgentEventType` discriminant enum
6//! - `event_type()` mapping
7//! - Wire data structs (in private `data` module)
8//! - `Serialize` / `Deserialize` impls
9
10use super::envelope_meta::{
11    clear_serialize_run_hint_if_matches, serialize_run_hint, set_serialize_run_hint,
12    take_runtime_event_envelope_meta,
13};
14use super::wire::{from_data_value, to_data_value, EventEnvelope};
15use crate::runtime::inference::TokenUsage;
16use crate::runtime::tool_call::ToolResult;
17use crate::runtime::TerminationReason;
18use crate::runtime::ToolCallOutcome;
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21use tirea_state::TrackedPatch;
22
23const fn default_tool_call_outcome() -> ToolCallOutcome {
24    ToolCallOutcome::Succeeded
25}
26
27/// Dispatches run-hint set/clear for envelope events during serialization.
28macro_rules! run_hint_action {
29    (set, $hint:expr) => {
30        set_serialize_run_hint($hint.0, $hint.1);
31    };
32    (clear, $hint:expr) => {
33        clear_serialize_run_hint_if_matches(&$hint.0, &$hint.1);
34    };
35}
36
37/// Generates `AgentEvent`, `AgentEventType`, wire data structs, and serde impls
38/// from a single variant declaration.
39///
40/// # Sections
41///
42/// - **`envelope`** — Variants whose `thread_id`/`run_id` are promoted to the
43///   event envelope. Declared with `=> set` (sets run hint) or `=> clear` (clears
44///   run hint) for serialize-time tracking.
45/// - **`standard`** — Variants whose enum fields map 1:1 to wire data struct fields.
46/// - **`wire_only`** — Like standard, but the data struct has additional computed
47///   fields (specified in `wire_extra { ... }`) that exist on the wire but not in
48///   the enum.
49/// - **`no_data`** — Unit variants with no payload; serialized with `data: null`.
50macro_rules! define_agent_events {
51    (
52        data_imports { $($import:item)* }
53
54        envelope {
55            $(
56                $(#[$env_vattr:meta])*
57                $EnvVariant:ident {
58                    $($(#[$env_dattr:meta])* $env_dfield:ident : $env_dfty:ty),*
59                    $(,)?
60                } => $hint_action:ident
61            ),* $(,)?
62        }
63
64        standard {
65            $(
66                $(#[$std_vattr:meta])*
67                $StdVariant:ident {
68                    $($(#[$std_fattr:meta])* $std_field:ident : $std_fty:ty),*
69                    $(,)?
70                }
71            ),* $(,)?
72        }
73
74        wire_only {
75            $(
76                $(#[$wire_vattr:meta])*
77                $WireVariant:ident {
78                    $($(#[$wire_fattr:meta])* $wire_field:ident : $wire_fty:ty),*
79                    $(,)?
80                } wire_extra {
81                    $($(#[$wire_eattr:meta])* $wire_efield:ident : $wire_efty:ty = $wire_expr:expr),*
82                    $(,)?
83                }
84            ),* $(,)?
85        }
86
87        no_data {
88            $(
89                $(#[$nd_vattr:meta])*
90                $NoDataVariant:ident
91            ),* $(,)?
92        }
93    ) => {
94        // ======================== AgentEvent enum ========================
95
96        /// Agent loop events for streaming execution.
97        #[derive(Debug, Clone)]
98        pub enum AgentEvent {
99            $(
100                $(#[$env_vattr])*
101                $EnvVariant {
102                    thread_id: String,
103                    run_id: String,
104                    $($env_dfield: $env_dfty),*
105                },
106            )*
107            $(
108                $(#[$std_vattr])*
109                $StdVariant { $($std_field: $std_fty),* },
110            )*
111            $(
112                $(#[$wire_vattr])*
113                $WireVariant { $($wire_field: $wire_fty),* },
114            )*
115            $(
116                $(#[$nd_vattr])*
117                $NoDataVariant,
118            )*
119        }
120
121        // ======================== AgentEventType enum ========================
122
123        #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
124        #[serde(rename_all = "snake_case")]
125        pub(crate) enum AgentEventType {
126            $($EnvVariant,)*
127            $($StdVariant,)*
128            $($WireVariant,)*
129            $($NoDataVariant,)*
130        }
131
132        // ======================== event_type() ========================
133
134        impl AgentEvent {
135            pub(crate) fn event_type(&self) -> AgentEventType {
136                match self {
137                    $(Self::$EnvVariant { .. } => AgentEventType::$EnvVariant,)*
138                    $(Self::$StdVariant { .. } => AgentEventType::$StdVariant,)*
139                    $(Self::$WireVariant { .. } => AgentEventType::$WireVariant,)*
140                    $(Self::$NoDataVariant => AgentEventType::$NoDataVariant,)*
141                }
142            }
143        }
144
145        // ======================== Wire data structs ========================
146
147        mod data {
148            $($import)*
149
150            $(
151                #[derive(Debug, Clone, Serialize, Deserialize)]
152                pub(super) struct $EnvVariant {
153                    $($(#[$env_dattr])* pub $env_dfield: $env_dfty,)*
154                }
155            )*
156
157            $(
158                #[derive(Debug, Clone, Serialize, Deserialize)]
159                pub(super) struct $StdVariant {
160                    $($(#[$std_fattr])* pub $std_field: $std_fty,)*
161                }
162            )*
163
164            $(
165                #[derive(Debug, Clone, Serialize, Deserialize)]
166                pub(super) struct $WireVariant {
167                    $($(#[$wire_fattr])* pub $wire_field: $wire_fty,)*
168                    $($(#[$wire_eattr])* pub $wire_efield: $wire_efty,)*
169                }
170            )*
171        }
172
173        // ======================== Serialize ========================
174
175        impl Serialize for AgentEvent {
176            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
177            where
178                S: serde::Serializer,
179            {
180                let event_type = self.event_type();
181
182                let run_hint = match self {
183                    $(
184                        Self::$EnvVariant { run_id, thread_id, .. } => {
185                            Some((run_id.clone(), thread_id.clone()))
186                        }
187                    )*
188                    _ => serialize_run_hint(),
189                };
190
191                let runtime_meta = take_runtime_event_envelope_meta(
192                    event_type,
193                    run_hint
194                        .as_ref()
195                        .map(|(r, t)| (r.as_str(), t.as_str())),
196                );
197                let meta_run_id = || runtime_meta.as_ref().map(|m| m.run_id.clone());
198                let meta_thread_id = || runtime_meta.as_ref().map(|m| m.thread_id.clone());
199                let meta_seq = || runtime_meta.as_ref().map(|m| m.seq);
200                let meta_timestamp_ms = || runtime_meta.as_ref().map(|m| m.timestamp_ms);
201                let meta_step_id = || runtime_meta.as_ref().and_then(|m| m.step_id.clone());
202
203                let envelope = match self {
204                    $(
205                        Self::$EnvVariant { thread_id, run_id, $($env_dfield),* } => EventEnvelope {
206                            event_type: AgentEventType::$EnvVariant,
207                            run_id: meta_run_id().or_else(|| Some(run_id.clone())),
208                            thread_id: meta_thread_id().or_else(|| Some(thread_id.clone())),
209                            seq: meta_seq(),
210                            timestamp_ms: meta_timestamp_ms(),
211                            step_id: meta_step_id(),
212                            data: to_data_value(&data::$EnvVariant {
213                                $($env_dfield: $env_dfield.clone(),)*
214                            }).map_err(serde::ser::Error::custom)?,
215                        },
216                    )*
217                    $(
218                        Self::$StdVariant { $($std_field),* } => EventEnvelope {
219                            event_type: AgentEventType::$StdVariant,
220                            run_id: meta_run_id(),
221                            thread_id: meta_thread_id(),
222                            seq: meta_seq(),
223                            timestamp_ms: meta_timestamp_ms(),
224                            step_id: meta_step_id(),
225                            data: to_data_value(&data::$StdVariant {
226                                $($std_field: $std_field.clone(),)*
227                            }).map_err(serde::ser::Error::custom)?,
228                        },
229                    )*
230                    $(
231                        Self::$WireVariant { $($wire_field),* } => EventEnvelope {
232                            event_type: AgentEventType::$WireVariant,
233                            run_id: meta_run_id(),
234                            thread_id: meta_thread_id(),
235                            seq: meta_seq(),
236                            timestamp_ms: meta_timestamp_ms(),
237                            step_id: meta_step_id(),
238                            data: to_data_value(&data::$WireVariant {
239                                $($wire_field: $wire_field.clone(),)*
240                                $($wire_efield: $wire_expr,)*
241                            }).map_err(serde::ser::Error::custom)?,
242                        },
243                    )*
244                    $(
245                        Self::$NoDataVariant => EventEnvelope {
246                            event_type: AgentEventType::$NoDataVariant,
247                            run_id: meta_run_id(),
248                            thread_id: meta_thread_id(),
249                            seq: meta_seq(),
250                            timestamp_ms: meta_timestamp_ms(),
251                            step_id: meta_step_id(),
252                            data: None,
253                        },
254                    )*
255                };
256
257                // Run hint management for envelope events.
258                match self {
259                    $(
260                        Self::$EnvVariant { run_id, thread_id, .. } => {
261                            let hint = runtime_meta
262                                .as_ref()
263                                .map(|meta| (meta.run_id.clone(), meta.thread_id.clone()))
264                                .unwrap_or_else(|| (run_id.clone(), thread_id.clone()));
265                            run_hint_action!($hint_action, hint);
266                        }
267                    )*
268                    _ => {}
269                }
270
271                envelope.serialize(serializer)
272            }
273        }
274
275        // ======================== Deserialize ========================
276
277        impl<'de> Deserialize<'de> for AgentEvent {
278            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
279            where
280                D: serde::Deserializer<'de>,
281            {
282                let envelope = EventEnvelope::deserialize(deserializer)?;
283                match envelope.event_type {
284                    $(
285                        AgentEventType::$EnvVariant => {
286                            let d: data::$EnvVariant = from_data_value(envelope.data)?;
287                            let run_id = envelope.run_id.ok_or_else(|| {
288                                serde::de::Error::custom(concat!(
289                                    stringify!($EnvVariant), " missing run_id"
290                                ))
291                            })?;
292                            let thread_id = envelope.thread_id.ok_or_else(|| {
293                                serde::de::Error::custom(concat!(
294                                    stringify!($EnvVariant), " missing thread_id"
295                                ))
296                            })?;
297                            Ok(Self::$EnvVariant {
298                                thread_id,
299                                run_id,
300                                $($env_dfield: d.$env_dfield),*
301                            })
302                        }
303                    )*
304                    $(
305                        AgentEventType::$StdVariant => {
306                            let d: data::$StdVariant = from_data_value(envelope.data)?;
307                            Ok(Self::$StdVariant { $($std_field: d.$std_field),* })
308                        }
309                    )*
310                    $(
311                        AgentEventType::$WireVariant => {
312                            let d: data::$WireVariant = from_data_value(envelope.data)?;
313                            $(let _ = d.$wire_efield;)*
314                            Ok(Self::$WireVariant { $($wire_field: d.$wire_field),* })
315                        }
316                    )*
317                    $(
318                        AgentEventType::$NoDataVariant => Ok(Self::$NoDataVariant),
319                    )*
320                }
321            }
322        }
323    };
324}
325
326// ============================================================================
327// Event declarations — the single source of truth.
328//
329// To add a new event variant:
330//   1. Add the declaration below in the appropriate section
331//   2. That's it — the macro generates everything else
332// ============================================================================
333
334define_agent_events! {
335    data_imports {
336        use crate::runtime::TerminationReason;
337        use crate::runtime::ToolCallOutcome;
338        use crate::runtime::inference::TokenUsage;
339        use crate::runtime::tool_call::ToolResult;
340        use serde::{Deserialize, Serialize};
341        use serde_json::Value;
342        use tirea_state::TrackedPatch;
343    }
344
345    envelope {
346        /// Run started.
347        RunStart {
348            #[serde(skip_serializing_if = "Option::is_none")]
349            parent_run_id: Option<String>,
350        } => set,
351
352        /// Run finished.
353        RunFinish {
354            #[serde(skip_serializing_if = "Option::is_none")]
355            result: Option<Value>,
356            /// Why this run terminated.
357            termination: TerminationReason,
358        } => clear,
359    }
360
361    standard {
362        /// LLM text delta.
363        TextDelta { delta: String },
364
365        /// LLM reasoning delta.
366        ReasoningDelta { delta: String },
367
368        /// Opaque reasoning signature/token delta from provider.
369        ReasoningEncryptedValue { encrypted_value: String },
370
371        /// Tool call started.
372        ToolCallStart { id: String, name: String },
373
374        /// Tool call arguments delta.
375        ToolCallDelta { id: String, args_delta: String },
376
377        /// Tool call input is complete.
378        ToolCallReady { id: String, name: String, arguments: Value },
379
380        /// Step started.
381        StepStart {
382            /// Pre-generated ID for the assistant message this step will produce.
383            #[serde(default)]
384            message_id: String,
385        },
386
387        /// LLM inference completed with token usage data.
388        InferenceComplete {
389            /// Model used for this inference.
390            model: String,
391            /// Token usage.
392            #[serde(skip_serializing_if = "Option::is_none")]
393            usage: Option<TokenUsage>,
394            /// Duration of the LLM call in milliseconds.
395            duration_ms: u64,
396        },
397
398        /// State snapshot.
399        StateSnapshot { snapshot: Value },
400
401        /// State delta.
402        StateDelta { delta: Vec<Value> },
403
404        /// Messages snapshot.
405        MessagesSnapshot { messages: Vec<Value> },
406
407        /// Activity snapshot.
408        ActivitySnapshot {
409            message_id: String,
410            activity_type: String,
411            content: Value,
412            #[serde(skip_serializing_if = "Option::is_none")]
413            replace: Option<bool>,
414        },
415
416        /// Activity delta.
417        ActivityDelta {
418            message_id: String,
419            activity_type: String,
420            patch: Vec<Value>,
421        },
422
423        /// Suspension resolution received.
424        ToolCallResumed { target_id: String, result: Value },
425
426        /// Error occurred.
427        Error {
428            message: String,
429            #[serde(skip_serializing_if = "Option::is_none")]
430            code: Option<String>,
431        },
432    }
433
434    wire_only {
435        /// Tool call completed.
436        ToolCallDone {
437            id: String,
438            result: ToolResult,
439            #[serde(skip_serializing_if = "Option::is_none")]
440            patch: Option<TrackedPatch>,
441            /// Pre-generated ID for the stored tool result message.
442            #[serde(default)]
443            message_id: String,
444        } wire_extra {
445            #[serde(default = "super::default_tool_call_outcome")]
446            outcome: ToolCallOutcome = ToolCallOutcome::from_tool_result(result),
447        }
448    }
449
450    no_data {
451        /// Step completed.
452        StepEnd,
453    }
454}
455
456// ============================================================================
457// Hand-written impls that don't belong in the macro.
458// ============================================================================
459
460impl AgentEvent {
461    /// Extract the response text from a `RunFinish` result value.
462    pub fn extract_response(result: &Option<Value>) -> String {
463        result
464            .as_ref()
465            .and_then(|v| v.get("response"))
466            .and_then(|r| r.as_str())
467            .unwrap_or_default()
468            .to_string()
469    }
470}