Skip to main content

vv_agent/
events.rs

1use std::time::{SystemTime, UNIX_EPOCH};
2
3use serde::{de::Error as _, ser::Error as _, Deserialize, Deserializer, Serialize, Serializer};
4use serde_json::{Number, Value};
5
6use crate::budget::{BudgetEnforcementBoundary, BudgetExhaustion, BudgetUsageSnapshot};
7use crate::checkpoint::{
8    OperationKind, OperationState, ReconciliationDecisionKind, ResumeObservation, ToolIdempotency,
9};
10use crate::types::{AgentStatus, CompletionReason, Metadata};
11
12mod wire;
13
14use wire::{
15    add_default_supplemental_fields, supplemental_wire_fields, validate_budget_wire_fields,
16    validate_checkpoint_wire_fields, validate_completion_wire_fields,
17};
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(transparent)]
21pub struct RunEventVersion(String);
22
23impl RunEventVersion {
24    pub fn v1() -> Self {
25        Self("v1".to_string())
26    }
27
28    pub fn as_str(&self) -> &str {
29        &self.0
30    }
31}
32impl Default for RunEventVersion {
33    fn default() -> Self {
34        Self::v1()
35    }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(transparent)]
40pub struct EventId(String);
41
42impl EventId {
43    pub fn new() -> Self {
44        Self(format!("evt_{}", uuid::Uuid::new_v4().simple()))
45    }
46
47    pub fn as_str(&self) -> &str {
48        &self.0
49    }
50
51    pub fn stable(value: impl Into<String>) -> Result<Self, String> {
52        let value = value.into();
53        if value.trim().is_empty() {
54            return Err("event id must be non-empty".to_string());
55        }
56        Ok(Self(value))
57    }
58}
59
60impl Default for EventId {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66#[derive(Debug, Clone, PartialEq)]
67pub struct RunEvent {
68    pub version: RunEventVersion,
69    pub event_id: EventId,
70    pub run_id: String,
71    pub trace_id: String,
72    pub session_id: Option<String>,
73    pub parent_event_id: Option<String>,
74    pub parent_run_id: Option<String>,
75    pub created_at: f64,
76    created_at_wire: CreatedAtWire,
77    pub cycle_index: Option<u32>,
78    pub agent_name: Option<String>,
79    pub metadata: Metadata,
80    pub payload: RunEventPayload,
81    extra_fields: Metadata,
82}
83
84#[derive(Debug, Clone, Default)]
85struct CreatedAtWire(Option<Number>);
86
87impl PartialEq for CreatedAtWire {
88    fn eq(&self, _other: &Self) -> bool {
89        true
90    }
91}
92
93impl Serialize for RunEvent {
94    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
95    where
96        S: Serializer,
97    {
98        let mut object = serde_json::to_value(&self.payload)
99            .map_err(S::Error::custom)?
100            .as_object()
101            .cloned()
102            .ok_or_else(|| S::Error::custom("run event payload must serialize as an object"))?;
103        object.extend(self.extra_fields.clone());
104        object.insert(
105            "version".to_string(),
106            serde_json::to_value(&self.version).map_err(S::Error::custom)?,
107        );
108        object.insert(
109            "event_id".to_string(),
110            serde_json::to_value(&self.event_id).map_err(S::Error::custom)?,
111        );
112        object.insert("run_id".to_string(), Value::String(self.run_id.clone()));
113        object.insert("trace_id".to_string(), Value::String(self.trace_id.clone()));
114        if let Some(session_id) = &self.session_id {
115            object.insert("session_id".to_string(), Value::String(session_id.clone()));
116        }
117        if let Some(parent_event_id) = &self.parent_event_id {
118            object.insert(
119                "parent_event_id".to_string(),
120                Value::String(parent_event_id.clone()),
121            );
122        }
123        if let Some(parent_run_id) = &self.parent_run_id {
124            object.insert(
125                "parent_run_id".to_string(),
126                Value::String(parent_run_id.clone()),
127            );
128        }
129        let created_at = self
130            .created_at_wire
131            .0
132            .clone()
133            .or_else(|| Number::from_f64(self.created_at))
134            .ok_or_else(|| S::Error::custom("created_at must be finite"))?;
135        object.insert("created_at".to_string(), Value::Number(created_at));
136        if let Some(cycle_index) = self.cycle_index {
137            object.insert("cycle_index".to_string(), Value::from(cycle_index));
138        }
139        if let Some(agent_name) = &self.agent_name {
140            object.insert("agent_name".to_string(), Value::String(agent_name.clone()));
141        }
142        if !self.metadata.is_empty() {
143            object.insert(
144                "metadata".to_string(),
145                Value::Object(self.metadata.clone().into_iter().collect()),
146            );
147        }
148        Value::Object(object).serialize(serializer)
149    }
150}
151
152#[derive(Deserialize)]
153struct RunEventWire {
154    version: RunEventVersion,
155    event_id: EventId,
156    run_id: String,
157    trace_id: String,
158    #[serde(default)]
159    session_id: Option<String>,
160    #[serde(default)]
161    parent_event_id: Option<String>,
162    #[serde(default)]
163    parent_run_id: Option<String>,
164    #[serde(default)]
165    created_at: Option<f64>,
166    #[serde(default)]
167    created_at_ms: Option<f64>,
168    #[serde(default)]
169    cycle_index: Option<u32>,
170    #[serde(default)]
171    agent_name: Option<String>,
172    #[serde(default)]
173    metadata: Metadata,
174    #[serde(flatten)]
175    payload: RunEventPayload,
176}
177
178impl<'de> Deserialize<'de> for RunEvent {
179    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
180    where
181        D: Deserializer<'de>,
182    {
183        let value = Value::deserialize(deserializer)?;
184        let mut wire: RunEventWire =
185            serde_json::from_value(value.clone()).map_err(D::Error::custom)?;
186        if wire.version.as_str() != "v1" {
187            return Err(D::Error::custom(format!(
188                "unsupported run event version `{}`",
189                wire.version.as_str()
190            )));
191        }
192        for (field, value) in [
193            ("event_id", wire.event_id.as_str()),
194            ("run_id", wire.run_id.as_str()),
195            ("trace_id", wire.trace_id.as_str()),
196        ] {
197            if value.trim().is_empty() {
198                return Err(D::Error::custom(format!(
199                    "run event {field} must be a non-empty string"
200                )));
201            }
202        }
203        validate_completion_wire_fields(&value).map_err(D::Error::custom)?;
204        validate_budget_wire_fields(&value).map_err(D::Error::custom)?;
205        validate_checkpoint_wire_fields(&wire.payload, wire.cycle_index)
206            .map_err(D::Error::custom)?;
207        if matches!(
208            &wire.payload,
209            RunEventPayload::ToolCallStarted { arguments, .. } if !arguments.is_object()
210        ) {
211            return Err(D::Error::custom(
212                "run event tool arguments must be an object",
213            ));
214        }
215        if let RunEventPayload::ApprovalResolved { approved, .. } = &mut wire.payload {
216            let action = match value.get("action") {
217                Some(Value::String(action)) => ApprovalAction::parse(action).ok_or_else(|| {
218                    D::Error::custom(format!("unsupported approval action `{action}`"))
219                })?,
220                Some(_) => return Err(D::Error::custom("approval action must be a string")),
221                None if value.get("approved").is_some() => ApprovalAction::from_approved(*approved),
222                None => {
223                    return Err(D::Error::custom(
224                        "approval_resolved requires action or approved",
225                    ))
226                }
227            };
228            if value.get("action").is_some()
229                && value.get("approved").is_some()
230                && *approved != action.is_approved()
231            {
232                return Err(D::Error::custom(format!(
233                    "approval action `{}` conflicts with approved={approved}",
234                    action.as_str()
235                )));
236            }
237            *approved = action.is_approved();
238        }
239        if let RunEventPayload::BudgetExhausted {
240            enforcement_boundary,
241            budget_exhaustion,
242            ..
243        } = &wire.payload
244        {
245            if budget_exhaustion.enforcement_boundary != *enforcement_boundary {
246                return Err(D::Error::custom(
247                    "run event budget exhaustion boundaries must match",
248                ));
249            }
250        }
251        let created_at_wire =
252            CreatedAtWire(value.get("created_at").and_then(Value::as_number).cloned());
253        let created_at = match (wire.created_at, wire.created_at_ms) {
254            (Some(seconds), _) => seconds,
255            (None, Some(milliseconds)) => milliseconds / 1000.0,
256            (None, None) => return Err(D::Error::custom("missing created_at")),
257        };
258        if !created_at.is_finite() || created_at < 0.0 {
259            return Err(D::Error::custom(
260                "created_at must be a finite non-negative number",
261            ));
262        }
263
264        let mut extra_fields = supplemental_wire_fields(&value, &wire.payload);
265        add_default_supplemental_fields(&wire.payload, &mut extra_fields);
266        Ok(Self {
267            version: wire.version,
268            event_id: wire.event_id,
269            run_id: wire.run_id,
270            trace_id: wire.trace_id,
271            session_id: wire.session_id,
272            parent_event_id: wire.parent_event_id,
273            parent_run_id: wire.parent_run_id,
274            created_at,
275            created_at_wire,
276            cycle_index: wire.cycle_index,
277            agent_name: wire.agent_name,
278            metadata: wire.metadata,
279            payload: wire.payload,
280            extra_fields,
281        })
282    }
283}
284
285impl RunEvent {
286    pub fn new(
287        run_id: impl Into<String>,
288        trace_id: impl Into<String>,
289        agent_name: impl Into<String>,
290        cycle_index: Option<u32>,
291        payload: RunEventPayload,
292    ) -> Self {
293        let mut extra_fields = Metadata::new();
294        add_default_supplemental_fields(&payload, &mut extra_fields);
295        Self {
296            version: RunEventVersion::v1(),
297            event_id: EventId::new(),
298            run_id: run_id.into(),
299            trace_id: trace_id.into(),
300            session_id: None,
301            parent_event_id: None,
302            parent_run_id: None,
303            created_at: timestamp_seconds(),
304            created_at_wire: CreatedAtWire::default(),
305            cycle_index,
306            agent_name: Some(agent_name.into()),
307            metadata: Metadata::new(),
308            payload,
309            extra_fields,
310        }
311    }
312
313    pub fn run_started(
314        run_id: impl Into<String>,
315        trace_id: impl Into<String>,
316        agent_name: impl Into<String>,
317        input: impl Into<String>,
318    ) -> Self {
319        Self::new(
320            run_id,
321            trace_id,
322            agent_name,
323            None,
324            RunEventPayload::RunStarted {
325                input: input.into(),
326            },
327        )
328    }
329
330    pub fn cycle_started(
331        run_id: impl Into<String>,
332        trace_id: impl Into<String>,
333        agent_name: impl Into<String>,
334        cycle_index: u32,
335    ) -> Self {
336        Self::new(
337            run_id,
338            trace_id,
339            agent_name,
340            Some(cycle_index),
341            RunEventPayload::CycleStarted,
342        )
343    }
344
345    pub fn assistant_delta(
346        run_id: impl Into<String>,
347        trace_id: impl Into<String>,
348        agent_name: impl Into<String>,
349        cycle_index: u32,
350        delta: impl Into<String>,
351    ) -> Self {
352        Self::new(
353            run_id,
354            trace_id,
355            agent_name,
356            Some(cycle_index),
357            RunEventPayload::AssistantDelta {
358                delta: delta.into(),
359            },
360        )
361    }
362
363    pub fn tool_call_started(
364        run_id: impl Into<String>,
365        trace_id: impl Into<String>,
366        agent_name: impl Into<String>,
367        cycle_index: u32,
368        tool_call_id: impl Into<String>,
369        tool_name: impl Into<String>,
370        arguments: Value,
371    ) -> Self {
372        Self::new(
373            run_id,
374            trace_id,
375            agent_name,
376            Some(cycle_index),
377            RunEventPayload::ToolCallStarted {
378                tool_call_id: tool_call_id.into(),
379                tool_name: tool_name.into(),
380                arguments,
381            },
382        )
383    }
384
385    pub fn tool_call_completed(
386        run_id: impl Into<String>,
387        trace_id: impl Into<String>,
388        agent_name: impl Into<String>,
389        cycle_index: Option<u32>,
390        tool_call_id: impl Into<String>,
391        tool_name: impl Into<String>,
392        status: ToolStatus,
393    ) -> Self {
394        Self::new(
395            run_id,
396            trace_id,
397            agent_name,
398            cycle_index,
399            RunEventPayload::ToolCallCompleted {
400                tool_call_id: tool_call_id.into(),
401                tool_name: tool_name.into(),
402                status,
403            },
404        )
405    }
406
407    pub fn approval_requested(
408        run_id: impl Into<String>,
409        trace_id: impl Into<String>,
410        agent_name: impl Into<String>,
411        request_id: impl Into<String>,
412        tool_call_id: impl Into<String>,
413        tool_name: impl Into<String>,
414        message: impl Into<String>,
415    ) -> Self {
416        Self::new(
417            run_id,
418            trace_id,
419            agent_name,
420            None,
421            RunEventPayload::ApprovalRequested {
422                request_id: request_id.into(),
423                tool_call_id: tool_call_id.into(),
424                tool_name: tool_name.into(),
425                message: message.into(),
426            },
427        )
428    }
429
430    pub fn memory_compact_started(
431        run_id: impl Into<String>,
432        trace_id: impl Into<String>,
433        agent_name: impl Into<String>,
434        cycle_index: u32,
435        message_count: usize,
436        estimated_tokens: Option<u64>,
437    ) -> Self {
438        Self::new(
439            run_id,
440            trace_id,
441            agent_name,
442            Some(cycle_index),
443            RunEventPayload::MemoryCompactStarted {
444                message_count,
445                estimated_tokens,
446            },
447        )
448    }
449
450    pub fn memory_compact_completed(
451        run_id: impl Into<String>,
452        trace_id: impl Into<String>,
453        agent_name: impl Into<String>,
454        cycle_index: u32,
455        before_count: usize,
456        after_count: usize,
457        summary_tokens: Option<u64>,
458    ) -> Self {
459        Self::new(
460            run_id,
461            trace_id,
462            agent_name,
463            Some(cycle_index),
464            RunEventPayload::MemoryCompactCompleted {
465                before_count,
466                after_count,
467                summary_tokens,
468            },
469        )
470    }
471
472    pub fn handoff_completed(
473        run_id: impl Into<String>,
474        trace_id: impl Into<String>,
475        source_agent: impl Into<String>,
476        target_agent: impl Into<String>,
477        tool_call_id: impl Into<String>,
478    ) -> Self {
479        let source_agent = source_agent.into();
480        Self::new(
481            run_id,
482            trace_id,
483            source_agent.clone(),
484            None,
485            RunEventPayload::HandoffCompleted {
486                source_agent,
487                target_agent: target_agent.into(),
488                tool_call_id: tool_call_id.into(),
489            },
490        )
491    }
492
493    pub fn run_completed(
494        run_id: impl Into<String>,
495        trace_id: impl Into<String>,
496        agent_name: impl Into<String>,
497        status: AgentStatus,
498    ) -> Self {
499        Self::new(
500            run_id,
501            trace_id,
502            agent_name,
503            None,
504            RunEventPayload::RunCompleted { status },
505        )
506    }
507
508    pub fn run_failed(
509        run_id: impl Into<String>,
510        trace_id: impl Into<String>,
511        agent_name: impl Into<String>,
512        error: AgentErrorPayload,
513    ) -> Self {
514        let AgentErrorPayload { message, code } = error;
515        let mut event = Self::new(
516            run_id,
517            trace_id,
518            agent_name,
519            None,
520            RunEventPayload::RunFailed { error: message },
521        );
522        if let Some(code) = code {
523            event
524                .metadata
525                .insert("error_code".to_string(), Value::String(code));
526        }
527        event
528    }
529
530    pub fn version(&self) -> &RunEventVersion {
531        &self.version
532    }
533
534    pub fn event_id(&self) -> &EventId {
535        &self.event_id
536    }
537
538    pub fn run_id(&self) -> &str {
539        &self.run_id
540    }
541
542    pub fn trace_id(&self) -> &str {
543        &self.trace_id
544    }
545
546    pub fn session_id(&self) -> Option<&str> {
547        self.session_id.as_deref()
548    }
549
550    pub fn parent_event_id(&self) -> Option<&str> {
551        self.parent_event_id.as_deref()
552    }
553
554    pub fn parent_run_id(&self) -> Option<&str> {
555        self.parent_run_id.as_deref()
556    }
557
558    pub fn created_at(&self) -> f64 {
559        self.created_at
560    }
561
562    pub fn created_at_ms(&self) -> u128 {
563        (self.created_at * 1000.0).round() as u128
564    }
565
566    pub fn cycle_index(&self) -> Option<u32> {
567        self.cycle_index
568    }
569
570    pub fn agent_name(&self) -> Option<&str> {
571        self.agent_name.as_deref()
572    }
573
574    pub fn payload(&self) -> &RunEventPayload {
575        &self.payload
576    }
577
578    pub fn metadata(&self) -> &Metadata {
579        &self.metadata
580    }
581
582    pub fn approval_action(&self) -> Option<ApprovalAction> {
583        let RunEventPayload::ApprovalResolved { approved, .. } = &self.payload else {
584            return None;
585        };
586        Some(
587            self.extra_fields
588                .get("action")
589                .and_then(Value::as_str)
590                .and_then(ApprovalAction::parse)
591                .unwrap_or_else(|| ApprovalAction::from_approved(*approved)),
592        )
593    }
594
595    pub fn completion_reason(&self) -> Option<CompletionReason> {
596        self.extra_fields
597            .get("completion_reason")
598            .and_then(Value::as_str)
599            .and_then(CompletionReason::parse)
600    }
601
602    pub fn completion_tool_name(&self) -> Option<&str> {
603        self.extra_fields
604            .get("completion_tool_name")
605            .and_then(Value::as_str)
606    }
607
608    pub fn partial_output(&self) -> Option<&str> {
609        self.extra_fields
610            .get("partial_output")
611            .and_then(Value::as_str)
612    }
613
614    pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
615        self.session_id = Some(session_id.into());
616        self
617    }
618
619    pub fn with_event_id(mut self, event_id: impl Into<String>) -> Result<Self, String> {
620        self.event_id = EventId::stable(event_id)?;
621        Ok(self)
622    }
623
624    pub fn with_parent_event_id(mut self, parent_event_id: impl Into<String>) -> Self {
625        self.parent_event_id = Some(parent_event_id.into());
626        self
627    }
628
629    pub fn with_parent_run_id(mut self, parent_run_id: impl Into<String>) -> Self {
630        self.parent_run_id = Some(parent_run_id.into());
631        self
632    }
633
634    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
635        self.metadata.insert(key.into(), value);
636        self
637    }
638
639    pub(crate) fn with_handoff_lifecycle(
640        mut self,
641        status: impl Into<String>,
642        child_session_id: Option<&str>,
643        child_run_id: Option<&str>,
644    ) -> Self {
645        debug_assert!(matches!(
646            &self.payload,
647            RunEventPayload::HandoffStarted { .. } | RunEventPayload::HandoffCompleted { .. }
648        ));
649        self.extra_fields
650            .insert("status".to_string(), Value::String(status.into()));
651        if let Some(child_session_id) = child_session_id {
652            self.extra_fields.insert(
653                "child_session_id".to_string(),
654                Value::String(child_session_id.to_string()),
655            );
656        }
657        if let Some(child_run_id) = child_run_id {
658            self.extra_fields.insert(
659                "child_run_id".to_string(),
660                Value::String(child_run_id.to_string()),
661            );
662        }
663        self
664    }
665
666    pub(crate) fn with_sub_run_details(
667        mut self,
668        child_session_id: Option<&str>,
669        task_id: Option<&str>,
670        wait_reason: Option<&str>,
671        error: Option<&str>,
672        token_usage: Option<Value>,
673    ) -> Self {
674        debug_assert!(matches!(
675            &self.payload,
676            RunEventPayload::SubRunCompleted { .. }
677        ));
678        for (key, value) in [
679            ("child_session_id", child_session_id),
680            ("task_id", task_id),
681            ("wait_reason", wait_reason),
682            ("error", error),
683        ] {
684            if let Some(value) = value {
685                self.extra_fields
686                    .insert(key.to_string(), Value::String(value.to_string()));
687            }
688        }
689        if let Some(token_usage) = token_usage {
690            self.extra_fields
691                .insert("token_usage".to_string(), token_usage);
692        }
693        self
694    }
695
696    pub fn with_approval_action(mut self, action: ApprovalAction) -> Self {
697        if let RunEventPayload::ApprovalResolved { approved, .. } = &mut self.payload {
698            *approved = action.is_approved();
699            self.extra_fields.insert(
700                "action".to_string(),
701                Value::String(action.as_str().to_string()),
702            );
703        }
704        self
705    }
706
707    pub fn with_final_output(mut self, final_output: Option<String>) -> Self {
708        self.extra_fields.insert(
709            "final_output".to_string(),
710            final_output.map_or(Value::Null, Value::String),
711        );
712        self
713    }
714
715    pub fn with_completion_details(
716        mut self,
717        completion_reason: Option<CompletionReason>,
718        completion_tool_name: Option<&str>,
719        partial_output: Option<&str>,
720    ) -> Self {
721        if let Some(reason) = completion_reason {
722            self.extra_fields.insert(
723                "completion_reason".to_string(),
724                Value::String(reason.as_str().to_string()),
725            );
726        }
727        for (key, value) in [
728            ("completion_tool_name", completion_tool_name),
729            ("partial_output", partial_output),
730        ] {
731            if let Some(value) = value {
732                self.extra_fields
733                    .insert(key.to_string(), Value::String(value.to_string()));
734            }
735        }
736        self
737    }
738
739    pub fn with_budget_details(
740        mut self,
741        budget_usage: Option<&BudgetUsageSnapshot>,
742        budget_exhaustion: Option<&BudgetExhaustion>,
743    ) -> Self {
744        if let Some(budget_usage) = budget_usage {
745            self.extra_fields.insert(
746                "budget_usage".to_string(),
747                serde_json::to_value(budget_usage).expect("budget usage serializes"),
748            );
749            self.extra_fields
750                .entry("completion_tool_name".to_string())
751                .or_insert(Value::Null);
752            self.extra_fields
753                .entry("partial_output".to_string())
754                .or_insert(Value::Null);
755            if matches!(self.payload, RunEventPayload::RunFailed { .. }) {
756                self.extra_fields
757                    .entry("status".to_string())
758                    .or_insert_with(|| Value::String("failed".to_string()));
759            }
760        }
761        if let Some(budget_exhaustion) = budget_exhaustion {
762            self.extra_fields.insert(
763                "budget_exhaustion".to_string(),
764                serde_json::to_value(budget_exhaustion).expect("budget exhaustion serializes"),
765            );
766        }
767        self
768    }
769
770    pub fn final_output(&self) -> Option<&str> {
771        self.extra_fields
772            .get("final_output")
773            .and_then(Value::as_str)
774    }
775}
776
777#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
778#[serde(tag = "type", rename_all = "snake_case")]
779pub enum RunEventPayload {
780    RunStarted {
781        input: String,
782    },
783    RunStateChanged {
784        state: String,
785    },
786    AgentStarted,
787    CycleStarted,
788    LlmStarted {
789        model: String,
790    },
791    AssistantDelta {
792        delta: String,
793    },
794    ToolCallStarted {
795        tool_call_id: String,
796        tool_name: String,
797        arguments: Value,
798    },
799    ApprovalRequested {
800        request_id: String,
801        tool_call_id: String,
802        tool_name: String,
803        #[serde(alias = "preview")]
804        message: String,
805    },
806    ApprovalResolved {
807        request_id: String,
808        tool_name: String,
809        tool_call_id: String,
810        #[serde(default)]
811        approved: bool,
812    },
813    ToolCallCompleted {
814        tool_call_id: String,
815        tool_name: String,
816        status: ToolStatus,
817    },
818    MemoryCompactStarted {
819        message_count: usize,
820        #[serde(default, skip_serializing_if = "Option::is_none")]
821        estimated_tokens: Option<u64>,
822    },
823    MemoryCompactCompleted {
824        before_count: usize,
825        after_count: usize,
826        #[serde(default, skip_serializing_if = "Option::is_none")]
827        summary_tokens: Option<u64>,
828    },
829    SubRunStarted {
830        parent_tool_call_id: String,
831        #[serde(default, skip_serializing_if = "Option::is_none")]
832        child_session_id: Option<String>,
833        #[serde(default, skip_serializing_if = "Option::is_none")]
834        task_id: Option<String>,
835    },
836    SubRunCompleted {
837        parent_tool_call_id: String,
838        status: AgentStatus,
839        #[serde(default, skip_serializing_if = "Option::is_none")]
840        final_output: Option<String>,
841    },
842    Handoff {
843        source_agent: String,
844        target_agent: String,
845        tool_call_id: String,
846    },
847    HandoffStarted {
848        source_agent: String,
849        target_agent: String,
850        tool_call_id: String,
851    },
852    HandoffCompleted {
853        source_agent: String,
854        target_agent: String,
855        tool_call_id: String,
856    },
857    SessionPersisted,
858    BudgetSnapshot {
859        enforcement_boundary: BudgetEnforcementBoundary,
860        budget_usage: BudgetUsageSnapshot,
861    },
862    BudgetExhausted {
863        enforcement_boundary: BudgetEnforcementBoundary,
864        budget_usage: BudgetUsageSnapshot,
865        budget_exhaustion: BudgetExhaustion,
866    },
867    CheckpointCreated {
868        checkpoint_key: String,
869        resume_attempt: u64,
870    },
871    CheckpointResumed {
872        checkpoint_key: String,
873        resume_attempt: u64,
874    },
875    OperationReplayed {
876        checkpoint_key: String,
877        operation_id: String,
878        operation_kind: OperationKind,
879        receipt_state: OperationState,
880    },
881    OperationAmbiguous {
882        checkpoint_key: String,
883        operation_id: String,
884        operation_kind: OperationKind,
885        risk: String,
886        idempotency_support: Option<ToolIdempotency>,
887    },
888    ReconciliationRequired {
889        checkpoint_key: String,
890        operation_id: String,
891        operation_kind: OperationKind,
892        interruption_reason: String,
893        resume_observation: ResumeObservation,
894    },
895    ModelRetryDuplicateRisk {
896        checkpoint_key: String,
897        operation_id: String,
898        operation_kind: OperationKind,
899        risk: String,
900    },
901    ReconciliationResolved {
902        checkpoint_key: String,
903        operation_id: String,
904        operation_kind: OperationKind,
905        decision: ReconciliationDecisionKind,
906    },
907    RunCompleted {
908        status: AgentStatus,
909    },
910    RunFailed {
911        error: String,
912    },
913    RunCancelled {
914        reason: String,
915    },
916}
917
918#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
919#[serde(rename_all = "snake_case")]
920pub enum ApprovalAction {
921    #[default]
922    Allow,
923    AllowSession,
924    Deny,
925    Timeout,
926}
927
928impl ApprovalAction {
929    pub fn from_approved(approved: bool) -> Self {
930        if approved {
931            Self::Allow
932        } else {
933            Self::Deny
934        }
935    }
936
937    pub fn parse(value: &str) -> Option<Self> {
938        match value {
939            "allow" => Some(Self::Allow),
940            "allow_session" => Some(Self::AllowSession),
941            "deny" => Some(Self::Deny),
942            "timeout" => Some(Self::Timeout),
943            _ => None,
944        }
945    }
946
947    pub fn as_str(self) -> &'static str {
948        match self {
949            Self::Allow => "allow",
950            Self::AllowSession => "allow_session",
951            Self::Deny => "deny",
952            Self::Timeout => "timeout",
953        }
954    }
955
956    pub fn is_approved(self) -> bool {
957        matches!(self, Self::Allow | Self::AllowSession)
958    }
959}
960
961#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
962#[serde(rename_all = "snake_case")]
963pub enum ToolStatus {
964    Started,
965    Success,
966    Error,
967    WaitResponse,
968}
969
970#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
971pub struct AgentErrorPayload {
972    pub message: String,
973    pub code: Option<String>,
974}
975
976impl AgentErrorPayload {
977    pub fn new(message: impl Into<String>) -> Self {
978        Self {
979            message: message.into(),
980            code: None,
981        }
982    }
983}
984
985fn timestamp_seconds() -> f64 {
986    SystemTime::now()
987        .duration_since(UNIX_EPOCH)
988        .map(|duration| duration.as_micros() as f64 / 1_000_000.0)
989        .unwrap_or_default()
990}