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