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