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}