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 v1() -> Self {
33        Self("v1".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::v1()
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() != "v1" {
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::v1(),
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        model_context_window: u64,
542        model_max_output_tokens: Option<u64>,
543        reserved_output_tokens: u64,
544        reserved_output_source: ReservedOutputSource,
545        autocompact_buffer_tokens: u64,
546    ) -> Self {
547        Self::new(
548            run_id,
549            trace_id,
550            agent_name,
551            Some(cycle_index),
552            RunEventPayload::MemoryCompactStarted {
553                message_count,
554                estimated_tokens,
555                trigger,
556                configured_threshold,
557                effective_threshold,
558                microcompact_threshold,
559                model_context_window,
560                model_max_output_tokens,
561                reserved_output_tokens,
562                reserved_output_source,
563                autocompact_buffer_tokens,
564            },
565        )
566    }
567
568    #[allow(clippy::too_many_arguments)]
569    pub fn memory_compact_completed(
570        run_id: impl Into<String>,
571        trace_id: impl Into<String>,
572        agent_name: impl Into<String>,
573        cycle_index: u32,
574        before_count: usize,
575        after_count: usize,
576        summary_tokens: Option<u64>,
577        mode: MemoryCompactMode,
578        changed: bool,
579    ) -> Self {
580        Self::new(
581            run_id,
582            trace_id,
583            agent_name,
584            Some(cycle_index),
585            RunEventPayload::MemoryCompactCompleted {
586                before_count,
587                after_count,
588                summary_tokens,
589                mode,
590                changed,
591            },
592        )
593    }
594
595    pub fn handoff_completed(
596        run_id: impl Into<String>,
597        trace_id: impl Into<String>,
598        source_agent: impl Into<String>,
599        target_agent: impl Into<String>,
600        tool_call_id: impl Into<String>,
601    ) -> Self {
602        let source_agent = source_agent.into();
603        Self::new(
604            run_id,
605            trace_id,
606            source_agent.clone(),
607            None,
608            RunEventPayload::HandoffCompleted {
609                source_agent,
610                target_agent: target_agent.into(),
611                tool_call_id: tool_call_id.into(),
612            },
613        )
614    }
615
616    pub fn run_completed(
617        run_id: impl Into<String>,
618        trace_id: impl Into<String>,
619        agent_name: impl Into<String>,
620        status: AgentStatus,
621    ) -> Self {
622        Self::new(
623            run_id,
624            trace_id,
625            agent_name,
626            None,
627            RunEventPayload::RunCompleted { status },
628        )
629    }
630
631    pub fn run_failed(
632        run_id: impl Into<String>,
633        trace_id: impl Into<String>,
634        agent_name: impl Into<String>,
635        error: AgentErrorPayload,
636    ) -> Self {
637        let AgentErrorPayload { message, code } = error;
638        let mut event = Self::new(
639            run_id,
640            trace_id,
641            agent_name,
642            None,
643            RunEventPayload::RunFailed { error: message },
644        );
645        if let Some(code) = code {
646            event
647                .metadata
648                .insert("error_code".to_string(), Value::String(code));
649        }
650        event
651    }
652
653    pub fn version(&self) -> &RunEventVersion {
654        &self.version
655    }
656
657    pub fn event_id(&self) -> &EventId {
658        &self.event_id
659    }
660
661    pub fn run_id(&self) -> &str {
662        &self.run_id
663    }
664
665    pub fn trace_id(&self) -> &str {
666        &self.trace_id
667    }
668
669    pub fn session_id(&self) -> Option<&str> {
670        self.session_id.as_deref()
671    }
672
673    pub fn parent_event_id(&self) -> Option<&str> {
674        self.parent_event_id.as_deref()
675    }
676
677    pub fn parent_run_id(&self) -> Option<&str> {
678        self.parent_run_id.as_deref()
679    }
680
681    pub fn created_at(&self) -> f64 {
682        self.created_at
683    }
684
685    pub fn cycle_index(&self) -> Option<u32> {
686        self.cycle_index
687    }
688
689    pub fn agent_name(&self) -> Option<&str> {
690        self.agent_name.as_deref()
691    }
692
693    pub fn payload(&self) -> &RunEventPayload {
694        &self.payload
695    }
696
697    pub fn metadata(&self) -> &Metadata {
698        &self.metadata
699    }
700
701    pub fn tool_metadata(&self) -> Option<ToolMetadata> {
702        self.extra_fields
703            .get("tool_metadata")
704            .and_then(|value| serde_json::from_value(value.clone()).ok())
705    }
706
707    pub fn tool_directive(&self) -> Option<ToolDirective> {
708        match &self.payload {
709            RunEventPayload::ToolCallCompleted { directive, .. } => Some(*directive),
710            _ => None,
711        }
712    }
713
714    pub fn tool_error_code(&self) -> Option<&str> {
715        match &self.payload {
716            RunEventPayload::ToolCallCompleted { error_code, .. } => error_code.as_deref(),
717            _ => None,
718        }
719    }
720
721    pub fn tool_execution_started(&self) -> Option<bool> {
722        match &self.payload {
723            RunEventPayload::ToolCallCompleted {
724                execution_started, ..
725            } => Some(*execution_started),
726            _ => None,
727        }
728    }
729
730    pub fn tool_duration_ms(&self) -> Option<u64> {
731        match &self.payload {
732            RunEventPayload::ToolCallCompleted { duration_ms, .. } => *duration_ms,
733            _ => None,
734        }
735    }
736
737    pub fn approval_action(&self) -> Option<ApprovalAction> {
738        match &self.payload {
739            RunEventPayload::ApprovalResolved { action, .. } => Some(*action),
740            _ => None,
741        }
742    }
743
744    pub fn completion_reason(&self) -> Option<CompletionReason> {
745        self.extra_fields
746            .get("completion_reason")
747            .and_then(Value::as_str)
748            .and_then(CompletionReason::parse)
749    }
750
751    pub fn completion_tool_name(&self) -> Option<&str> {
752        self.extra_fields
753            .get("completion_tool_name")
754            .and_then(Value::as_str)
755    }
756
757    pub fn partial_output(&self) -> Option<&str> {
758        self.extra_fields
759            .get("partial_output")
760            .and_then(Value::as_str)
761    }
762
763    pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
764        self.session_id = Some(session_id.into());
765        self
766    }
767
768    pub fn with_event_id(mut self, event_id: impl Into<String>) -> Result<Self, String> {
769        self.event_id = EventId::stable(event_id)?;
770        Ok(self)
771    }
772
773    pub fn with_parent_event_id(mut self, parent_event_id: impl Into<String>) -> Self {
774        self.parent_event_id = Some(parent_event_id.into());
775        self
776    }
777
778    pub fn with_parent_run_id(mut self, parent_run_id: impl Into<String>) -> Self {
779        self.parent_run_id = Some(parent_run_id.into());
780        self
781    }
782
783    pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
784        self.metadata.insert(key.into(), value);
785        self
786    }
787
788    pub fn with_tool_metadata(mut self, tool_metadata: Option<&ToolMetadata>) -> Self {
789        debug_assert!(matches!(
790            self.payload,
791            RunEventPayload::ToolCallPlanned { .. }
792                | RunEventPayload::ToolCallStarted { .. }
793                | RunEventPayload::ToolCallCompleted { .. }
794        ));
795        if let Some(tool_metadata) = tool_metadata {
796            self.extra_fields.insert(
797                "tool_metadata".to_string(),
798                serde_json::to_value(tool_metadata).expect("tool metadata serializes"),
799            );
800        }
801        self
802    }
803
804    pub(crate) fn with_handoff_lifecycle(
805        mut self,
806        status: impl Into<String>,
807        child_session_id: Option<&str>,
808        child_run_id: Option<&str>,
809    ) -> Self {
810        debug_assert!(matches!(
811            &self.payload,
812            RunEventPayload::HandoffStarted { .. } | RunEventPayload::HandoffCompleted { .. }
813        ));
814        self.extra_fields
815            .insert("status".to_string(), Value::String(status.into()));
816        if let Some(child_session_id) = child_session_id {
817            self.extra_fields.insert(
818                "child_session_id".to_string(),
819                Value::String(child_session_id.to_string()),
820            );
821        }
822        if let Some(child_run_id) = child_run_id {
823            self.extra_fields.insert(
824                "child_run_id".to_string(),
825                Value::String(child_run_id.to_string()),
826            );
827        }
828        self
829    }
830
831    pub(crate) fn with_sub_run_details(
832        mut self,
833        child_session_id: Option<&str>,
834        task_id: Option<&str>,
835        wait_reason: Option<&str>,
836        error: Option<&str>,
837        token_usage: Option<Value>,
838    ) -> Self {
839        debug_assert!(matches!(
840            &self.payload,
841            RunEventPayload::SubRunCompleted { .. }
842        ));
843        for (key, value) in [
844            ("child_session_id", child_session_id),
845            ("task_id", task_id),
846            ("wait_reason", wait_reason),
847            ("error", error),
848        ] {
849            if let Some(value) = value {
850                self.extra_fields
851                    .insert(key.to_string(), Value::String(value.to_string()));
852            }
853        }
854        if let Some(token_usage) = token_usage {
855            self.extra_fields
856                .insert("token_usage".to_string(), token_usage);
857        }
858        self
859    }
860
861    pub fn with_final_output(mut self, final_output: Option<String>) -> Self {
862        self.extra_fields.insert(
863            "final_output".to_string(),
864            final_output.map_or(Value::Null, Value::String),
865        );
866        self
867    }
868
869    pub fn with_completion_details(
870        mut self,
871        completion_reason: Option<CompletionReason>,
872        completion_tool_name: Option<&str>,
873        partial_output: Option<&str>,
874    ) -> Self {
875        if let Some(reason) = completion_reason {
876            self.extra_fields.insert(
877                "completion_reason".to_string(),
878                Value::String(reason.as_str().to_string()),
879            );
880        }
881        for (key, value) in [
882            ("completion_tool_name", completion_tool_name),
883            ("partial_output", partial_output),
884        ] {
885            if let Some(value) = value {
886                self.extra_fields
887                    .insert(key.to_string(), Value::String(value.to_string()));
888            }
889        }
890        self
891    }
892
893    pub fn with_budget_details(
894        mut self,
895        budget_usage: Option<&BudgetUsageSnapshot>,
896        budget_exhaustion: Option<&BudgetExhaustion>,
897    ) -> Self {
898        if let Some(budget_usage) = budget_usage {
899            self.extra_fields.insert(
900                "budget_usage".to_string(),
901                serde_json::to_value(budget_usage).expect("budget usage serializes"),
902            );
903            self.extra_fields
904                .entry("completion_tool_name".to_string())
905                .or_insert(Value::Null);
906            self.extra_fields
907                .entry("partial_output".to_string())
908                .or_insert(Value::Null);
909            if matches!(self.payload, RunEventPayload::RunFailed { .. }) {
910                self.extra_fields
911                    .entry("status".to_string())
912                    .or_insert_with(|| Value::String("failed".to_string()));
913            }
914        }
915        if let Some(budget_exhaustion) = budget_exhaustion {
916            self.extra_fields.insert(
917                "budget_exhaustion".to_string(),
918                serde_json::to_value(budget_exhaustion).expect("budget exhaustion serializes"),
919            );
920        }
921        self
922    }
923
924    pub fn final_output(&self) -> Option<&str> {
925        self.extra_fields
926            .get("final_output")
927            .and_then(Value::as_str)
928    }
929}
930
931fn timestamp_seconds() -> f64 {
932    SystemTime::now()
933        .duration_since(UNIX_EPOCH)
934        .map(|duration| duration.as_micros() as f64 / 1_000_000.0)
935        .unwrap_or_default()
936}