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}