1use crate::llm::ContentBlock;
16use crate::types::{BudgetLimitKind, ThreadId, TokenUsage, ToolResult, ToolTier};
17use serde::{Deserialize, Serialize};
18use std::sync::Arc;
19use std::sync::atomic::{AtomicU64, Ordering};
20use std::time::Duration;
21use time::OffsetDateTime;
22
23mod duration_ms_serde {
34 use serde::{Deserialize, Deserializer, Serializer};
35 use std::time::Duration;
36
37 #[derive(Deserialize)]
39 #[serde(untagged)]
40 enum DurationRepr {
41 Millis(u64),
43 Legacy { secs: u64, nanos: u32 },
45 }
46
47 pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
48 where
49 S: Serializer,
50 {
51 let ms = u64::try_from(duration.as_millis()).unwrap_or(u64::MAX);
52 serializer.serialize_u64(ms)
53 }
54
55 pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
56 where
57 D: Deserializer<'de>,
58 {
59 match DurationRepr::deserialize(deserializer)? {
60 DurationRepr::Millis(ms) => Ok(Duration::from_millis(ms)),
61 DurationRepr::Legacy { secs, nanos } => Ok(Duration::new(secs, nanos)),
62 }
63 }
64}
65
66#[derive(Clone, Debug, Serialize, Deserialize)]
69#[serde(tag = "type", rename_all = "snake_case")]
70#[non_exhaustive]
71pub enum AgentEvent {
72 Start { thread_id: ThreadId, turn: usize },
74
75 UserInput {
95 thread_id: ThreadId,
96 content: Vec<ContentBlock>,
103 },
104
105 Thinking { message_id: String, text: String },
107
108 ThinkingDelta { message_id: String, delta: String },
110
111 TextDelta { message_id: String, delta: String },
113
114 Text { message_id: String, text: String },
116
117 ToolCallStart {
119 id: String,
120 name: String,
121 display_name: String,
122 input: serde_json::Value,
123 tier: ToolTier,
124 },
125
126 ToolCallEnd {
128 id: String,
129 name: String,
130 display_name: String,
131 result: ToolResult,
132 },
133
134 ToolProgress {
136 id: String,
138 name: String,
140 display_name: String,
142 stage: String,
144 message: String,
146 data: Option<serde_json::Value>,
148 },
149
150 ToolRequiresConfirmation {
153 id: String,
154 name: String,
155 display_name: String,
156 input: serde_json::Value,
157 description: String,
158 },
159
160 TurnComplete { turn: usize, usage: TokenUsage },
162
163 Done {
165 thread_id: ThreadId,
166 total_turns: usize,
167 total_usage: TokenUsage,
168 #[serde(rename = "duration_ms", alias = "duration", with = "duration_ms_serde")]
176 duration: Duration,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
182 estimated_cost_usd: Option<f64>,
183 },
184
185 BudgetExceeded {
192 thread_id: ThreadId,
193 total_turns: usize,
194 total_usage: TokenUsage,
195 #[serde(rename = "duration_ms", alias = "duration", with = "duration_ms_serde")]
200 duration: Duration,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
204 estimated_cost_usd: Option<f64>,
205 limit: BudgetLimitKind,
207 },
208
209 Error { message: String, recoverable: bool },
211
212 AutoRetryStart {
218 attempt: u32,
220 max_attempts: u32,
222 delay_ms: u64,
224 error_message: String,
226 },
227
228 AutoRetryEnd {
232 attempt: u32,
235 success: bool,
237 final_error: Option<String>,
239 },
240
241 Refusal {
243 message_id: String,
244 text: Option<String>,
245 },
246
247 Cancelled { turn: usize, usage: TokenUsage },
263
264 ContextCompacted {
266 original_count: usize,
268 new_count: usize,
270 original_tokens: usize,
272 new_tokens: usize,
274 },
275
276 SubagentProgress {
278 subagent_id: String,
280 subagent_name: String,
282 nickname: Option<String>,
284 child_thread_id: Option<ThreadId>,
286 child_root_task_id: Option<String>,
288 subagent_task_id: Option<String>,
290 max_turns: Option<u32>,
292 current_turn: Option<u32>,
294 model: Option<String>,
296 tool_name: String,
298 tool_context: String,
300 completed: bool,
302 success: bool,
304 tool_count: u32,
306 total_tokens: u64,
308 },
309}
310
311impl AgentEvent {
312 #[must_use]
313 pub const fn start(thread_id: ThreadId, turn: usize) -> Self {
314 Self::Start { thread_id, turn }
315 }
316
317 #[must_use]
318 pub const fn user_input(thread_id: ThreadId, content: Vec<ContentBlock>) -> Self {
319 Self::UserInput { thread_id, content }
320 }
321
322 #[must_use]
323 pub fn thinking(message_id: impl Into<String>, text: impl Into<String>) -> Self {
324 Self::Thinking {
325 message_id: message_id.into(),
326 text: text.into(),
327 }
328 }
329
330 #[must_use]
331 pub fn thinking_delta(message_id: impl Into<String>, delta: impl Into<String>) -> Self {
332 Self::ThinkingDelta {
333 message_id: message_id.into(),
334 delta: delta.into(),
335 }
336 }
337
338 #[must_use]
339 pub fn text_delta(message_id: impl Into<String>, delta: impl Into<String>) -> Self {
340 Self::TextDelta {
341 message_id: message_id.into(),
342 delta: delta.into(),
343 }
344 }
345
346 #[must_use]
347 pub fn text(message_id: impl Into<String>, text: impl Into<String>) -> Self {
348 Self::Text {
349 message_id: message_id.into(),
350 text: text.into(),
351 }
352 }
353
354 #[must_use]
355 pub fn tool_call_start(
356 id: impl Into<String>,
357 name: impl Into<String>,
358 display_name: impl Into<String>,
359 input: serde_json::Value,
360 tier: ToolTier,
361 ) -> Self {
362 Self::ToolCallStart {
363 id: id.into(),
364 name: name.into(),
365 display_name: display_name.into(),
366 input,
367 tier,
368 }
369 }
370
371 #[must_use]
372 pub fn tool_call_end(
373 id: impl Into<String>,
374 name: impl Into<String>,
375 display_name: impl Into<String>,
376 result: ToolResult,
377 ) -> Self {
378 Self::ToolCallEnd {
379 id: id.into(),
380 name: name.into(),
381 display_name: display_name.into(),
382 result,
383 }
384 }
385
386 #[must_use]
387 pub fn tool_progress(
388 id: impl Into<String>,
389 name: impl Into<String>,
390 display_name: impl Into<String>,
391 stage: impl Into<String>,
392 message: impl Into<String>,
393 data: Option<serde_json::Value>,
394 ) -> Self {
395 Self::ToolProgress {
396 id: id.into(),
397 name: name.into(),
398 display_name: display_name.into(),
399 stage: stage.into(),
400 message: message.into(),
401 data,
402 }
403 }
404
405 #[must_use]
406 pub fn tool_requires_confirmation(
407 id: impl Into<String>,
408 name: impl Into<String>,
409 display_name: impl Into<String>,
410 input: serde_json::Value,
411 description: impl Into<String>,
412 ) -> Self {
413 Self::ToolRequiresConfirmation {
414 id: id.into(),
415 name: name.into(),
416 display_name: display_name.into(),
417 input,
418 description: description.into(),
419 }
420 }
421
422 #[must_use]
423 pub const fn done(
424 thread_id: ThreadId,
425 total_turns: usize,
426 total_usage: TokenUsage,
427 duration: Duration,
428 ) -> Self {
429 Self::Done {
430 thread_id,
431 total_turns,
432 total_usage,
433 duration,
434 estimated_cost_usd: None,
435 }
436 }
437
438 #[must_use]
439 pub const fn done_with_cost(
440 thread_id: ThreadId,
441 total_turns: usize,
442 total_usage: TokenUsage,
443 duration: Duration,
444 estimated_cost_usd: Option<f64>,
445 ) -> Self {
446 Self::Done {
447 thread_id,
448 total_turns,
449 total_usage,
450 duration,
451 estimated_cost_usd,
452 }
453 }
454
455 #[must_use]
456 pub const fn budget_exceeded(
457 thread_id: ThreadId,
458 total_turns: usize,
459 total_usage: TokenUsage,
460 duration: Duration,
461 estimated_cost_usd: Option<f64>,
462 limit: BudgetLimitKind,
463 ) -> Self {
464 Self::BudgetExceeded {
465 thread_id,
466 total_turns,
467 total_usage,
468 duration,
469 estimated_cost_usd,
470 limit,
471 }
472 }
473
474 #[must_use]
475 pub fn error(message: impl Into<String>, recoverable: bool) -> Self {
476 Self::Error {
477 message: message.into(),
478 recoverable,
479 }
480 }
481
482 #[must_use]
483 pub fn refusal(message_id: impl Into<String>, text: Option<String>) -> Self {
484 Self::Refusal {
485 message_id: message_id.into(),
486 text,
487 }
488 }
489
490 #[must_use]
491 pub const fn cancelled(turn: usize, usage: TokenUsage) -> Self {
492 Self::Cancelled { turn, usage }
493 }
494
495 #[must_use]
496 pub const fn context_compacted(
497 original_count: usize,
498 new_count: usize,
499 original_tokens: usize,
500 new_tokens: usize,
501 ) -> Self {
502 Self::ContextCompacted {
503 original_count,
504 new_count,
505 original_tokens,
506 new_tokens,
507 }
508 }
509}
510
511#[derive(Clone, Debug)]
520pub struct SequenceCounter(Arc<AtomicU64>);
521
522impl SequenceCounter {
523 #[must_use]
525 pub fn new() -> Self {
526 Self(Arc::new(AtomicU64::new(0)))
527 }
528
529 #[must_use]
535 pub fn with_offset(start: u64) -> Self {
536 Self(Arc::new(AtomicU64::new(start)))
537 }
538
539 #[must_use]
541 pub fn next(&self) -> u64 {
542 self.0.fetch_add(1, Ordering::Relaxed)
543 }
544}
545
546impl Default for SequenceCounter {
547 fn default() -> Self {
548 Self::new()
549 }
550}
551
552#[derive(Clone, Debug, Serialize, Deserialize)]
560pub struct AgentEventEnvelope {
561 pub event_id: uuid::Uuid,
566 pub sequence: u64,
568 #[serde(with = "time::serde::rfc3339")]
570 pub timestamp: OffsetDateTime,
571 #[serde(flatten)]
573 pub event: AgentEvent,
574}
575
576impl AgentEventEnvelope {
577 #[must_use]
580 pub fn wrap(event: AgentEvent, seq: &SequenceCounter) -> Self {
581 Self {
582 event_id: uuid::Uuid::new_v4(),
583 sequence: seq.next(),
584 timestamp: OffsetDateTime::now_utc(),
585 event,
586 }
587 }
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593 use std::collections::HashSet;
594
595 #[test]
600 fn sequence_counter_starts_at_zero() {
601 let seq = SequenceCounter::new();
602 assert_eq!(seq.next(), 0);
603 }
604
605 #[test]
606 fn sequence_counter_increments_monotonically() {
607 let seq = SequenceCounter::new();
608 for expected in 0..100 {
609 assert_eq!(seq.next(), expected);
610 }
611 }
612
613 #[test]
614 fn sequence_counter_no_gaps() {
615 let seq = SequenceCounter::new();
616 let values: Vec<u64> = (0..50).map(|_| seq.next()).collect();
617 let expected: Vec<u64> = (0..50).collect();
618 assert_eq!(values, expected);
619 }
620
621 #[test]
622 fn sequence_counter_clones_share_state() {
623 let seq = SequenceCounter::new();
624 let clone = seq.clone();
625
626 assert_eq!(seq.next(), 0);
627 assert_eq!(clone.next(), 1);
628 assert_eq!(seq.next(), 2);
629 }
630
631 #[test]
632 fn sequence_counter_default_starts_at_zero() {
633 let seq = SequenceCounter::default();
634 assert_eq!(seq.next(), 0);
635 }
636
637 #[test]
638 fn sequence_counter_with_offset_starts_at_given_value() {
639 let seq = SequenceCounter::with_offset(42);
640 assert_eq!(seq.next(), 42);
641 assert_eq!(seq.next(), 43);
642 assert_eq!(seq.next(), 44);
643 }
644
645 #[test]
646 fn sequence_counter_with_offset_zero_same_as_new() {
647 let seq = SequenceCounter::with_offset(0);
648 assert_eq!(seq.next(), 0);
649 assert_eq!(seq.next(), 1);
650 }
651
652 #[tokio::test]
653 async fn sequence_counter_unique_across_concurrent_tasks() {
654 let seq = SequenceCounter::new();
655 let n = 1000;
656
657 let mut handles = Vec::new();
658 for _ in 0..n {
659 let seq_clone = seq.clone();
660 handles.push(tokio::spawn(async move { seq_clone.next() }));
661 }
662
663 let mut values = HashSet::new();
664 for handle in handles {
665 let val = handle.await.unwrap();
666 assert!(values.insert(val), "duplicate sequence number: {val}");
667 }
668
669 assert_eq!(values.len(), n);
670 for v in &values {
672 assert!(*v < n as u64);
673 }
674 }
675
676 fn sample_event() -> AgentEvent {
681 AgentEvent::text("msg_1", "hello")
682 }
683
684 #[test]
685 fn wrap_assigns_unique_event_ids() {
686 let seq = SequenceCounter::new();
687 let ids: HashSet<uuid::Uuid> = (0..100)
688 .map(|_| AgentEventEnvelope::wrap(sample_event(), &seq).event_id)
689 .collect();
690 assert_eq!(ids.len(), 100);
691 }
692
693 #[test]
694 fn wrap_event_id_is_valid_uuid_v4() {
695 let seq = SequenceCounter::new();
696 let envelope = AgentEventEnvelope::wrap(sample_event(), &seq);
697 assert_eq!(envelope.event_id.get_version(), Some(uuid::Version::Random));
698 }
699
700 #[test]
701 fn wrap_assigns_incrementing_sequences() {
702 let seq = SequenceCounter::new();
703 let envelopes: Vec<AgentEventEnvelope> = (0..10)
704 .map(|_| AgentEventEnvelope::wrap(sample_event(), &seq))
705 .collect();
706
707 for (i, env) in envelopes.iter().enumerate() {
708 assert_eq!(env.sequence, i as u64);
709 }
710 }
711
712 #[test]
713 fn wrap_timestamps_are_non_decreasing() {
714 let seq = SequenceCounter::new();
715 let envelopes: Vec<AgentEventEnvelope> = (0..20)
716 .map(|_| AgentEventEnvelope::wrap(sample_event(), &seq))
717 .collect();
718
719 for pair in envelopes.windows(2) {
720 assert!(pair[1].timestamp >= pair[0].timestamp);
721 }
722 }
723
724 #[test]
725 fn wrap_preserves_inner_event() {
726 let seq = SequenceCounter::new();
727 let envelope = AgentEventEnvelope::wrap(AgentEvent::text("msg_42", "content"), &seq);
728 match &envelope.event {
729 AgentEvent::Text { message_id, text } => {
730 assert_eq!(message_id, "msg_42");
731 assert_eq!(text, "content");
732 }
733 other => panic!("expected Text, got {other:?}"),
734 }
735 }
736
737 #[test]
738 fn separate_counters_produce_independent_sequences() {
739 let seq_a = SequenceCounter::new();
740 let seq_b = SequenceCounter::new();
741
742 let a0 = AgentEventEnvelope::wrap(sample_event(), &seq_a);
743 let b0 = AgentEventEnvelope::wrap(sample_event(), &seq_b);
744 let a1 = AgentEventEnvelope::wrap(sample_event(), &seq_a);
745 let b1 = AgentEventEnvelope::wrap(sample_event(), &seq_b);
746
747 assert_eq!(a0.sequence, 0);
749 assert_eq!(b0.sequence, 0);
750 assert_eq!(a1.sequence, 1);
751 assert_eq!(b1.sequence, 1);
752
753 let ids: HashSet<uuid::Uuid> = [&a0, &b0, &a1, &b1].iter().map(|e| e.event_id).collect();
755 assert_eq!(ids.len(), 4);
756 }
757
758 #[test]
763 fn envelope_serializes_flat_json() {
764 let seq = SequenceCounter::new();
765 let envelope = AgentEventEnvelope::wrap(AgentEvent::text("msg_1", "hi"), &seq);
766 let json: serde_json::Value = serde_json::to_value(&envelope).expect("serialize");
767
768 assert!(json.get("event_id").is_some());
770 assert!(json.get("sequence").is_some());
771 assert!(json.get("timestamp").is_some());
772
773 assert_eq!(json.get("type").and_then(|v| v.as_str()), Some("text"));
775 assert_eq!(
776 json.get("message_id").and_then(|v| v.as_str()),
777 Some("msg_1")
778 );
779 assert_eq!(json.get("text").and_then(|v| v.as_str()), Some("hi"));
780
781 assert!(json.get("event").is_none());
783 }
784
785 #[test]
786 fn envelope_event_id_does_not_collide_with_tool_id() {
787 let seq = SequenceCounter::new();
788 let envelope = AgentEventEnvelope::wrap(
789 AgentEvent::tool_call_start(
790 "tool_123",
791 "bash",
792 "Bash",
793 serde_json::json!({}),
794 ToolTier::Observe,
795 ),
796 &seq,
797 );
798 let json: serde_json::Value = serde_json::to_value(&envelope).expect("serialize");
799
800 let event_id = json.get("event_id").and_then(|v| v.as_str()).unwrap();
802 let tool_id = json.get("id").and_then(|v| v.as_str()).unwrap();
803 assert_ne!(event_id, tool_id);
804 assert_eq!(tool_id, "tool_123");
805 }
806
807 #[test]
808 fn envelope_roundtrip_serde() {
809 let seq = SequenceCounter::new();
810 let original = AgentEventEnvelope::wrap(AgentEvent::text("msg_1", "hello"), &seq);
811
812 let json_str = serde_json::to_string(&original).expect("serialize");
813 let restored: AgentEventEnvelope = serde_json::from_str(&json_str).expect("deserialize");
814
815 assert_eq!(restored.event_id, original.event_id);
816 assert_eq!(restored.sequence, original.sequence);
817 assert_eq!(restored.timestamp, original.timestamp);
818 match &restored.event {
819 AgentEvent::Text { message_id, text } => {
820 assert_eq!(message_id, "msg_1");
821 assert_eq!(text, "hello");
822 }
823 other => panic!("expected Text, got {other:?}"),
824 }
825 }
826
827 #[test]
828 fn envelope_sequence_is_u64_in_json() {
829 let seq = SequenceCounter::new();
830 let envelope = AgentEventEnvelope::wrap(sample_event(), &seq);
831 let json: serde_json::Value = serde_json::to_value(&envelope).expect("serialize");
832
833 assert!(json.get("sequence").unwrap().is_u64());
834 assert_eq!(json.get("sequence").unwrap().as_u64(), Some(0));
835 }
836
837 #[test]
838 fn envelope_timestamp_is_rfc3339_string() {
839 let seq = SequenceCounter::new();
840 let envelope = AgentEventEnvelope::wrap(sample_event(), &seq);
841 let json: serde_json::Value = serde_json::to_value(&envelope).expect("serialize");
842
843 let ts_str = json.get("timestamp").unwrap().as_str().unwrap();
844 time::OffsetDateTime::parse(ts_str, &time::format_description::well_known::Rfc3339)
846 .expect("timestamp should be valid RFC 3339");
847 }
848
849 #[test]
850 fn done_event_serializes_duration_as_millis() -> serde_json::Result<()> {
851 let seq = SequenceCounter::new();
852 let envelope = AgentEventEnvelope::wrap(
853 AgentEvent::done(
854 ThreadId::from_string("t"),
855 3,
856 TokenUsage::default(),
857 Duration::from_millis(2500),
858 ),
859 &seq,
860 );
861 let json = serde_json::to_value(&envelope)?;
862
863 assert_eq!(
866 json.get("duration_ms").and_then(serde_json::Value::as_u64),
867 Some(2500)
868 );
869 assert!(
870 json.get("duration").is_none(),
871 "old `duration` key must be gone: {json}"
872 );
873
874 let restored: AgentEventEnvelope = serde_json::from_value(json)?;
875 match restored.event {
876 AgentEvent::Done { duration, .. } => {
877 assert_eq!(duration, Duration::from_millis(2500));
878 }
879 other => panic!("expected Done, got {other:?}"),
880 }
881 Ok(())
882 }
883
884 #[test]
885 fn done_event_deserializes_legacy_duration_object() -> serde_json::Result<()> {
886 let legacy = serde_json::json!({
891 "type": "done",
892 "thread_id": "t-legacy",
893 "total_turns": 3,
894 "total_usage": TokenUsage::default(),
895 "duration": { "secs": 2, "nanos": 500_000_000 },
896 });
897 let event: AgentEvent = serde_json::from_value(legacy)?;
898 match event {
899 AgentEvent::Done {
900 duration,
901 total_turns,
902 ..
903 } => {
904 assert_eq!(duration, Duration::from_millis(2500));
905 assert_eq!(total_turns, 3);
906 }
907 other => panic!("expected Done, got {other:?}"),
908 }
909
910 let current = serde_json::json!({
912 "type": "done",
913 "thread_id": "t-current",
914 "total_turns": 3,
915 "total_usage": TokenUsage::default(),
916 "duration_ms": 2500,
917 });
918 let event: AgentEvent = serde_json::from_value(current)?;
919 let AgentEvent::Done { duration, .. } = event else {
920 panic!("expected Done");
921 };
922 assert_eq!(duration, Duration::from_millis(2500));
923
924 let legacy_event: AgentEvent = serde_json::from_value(serde_json::json!({
926 "type": "done",
927 "thread_id": "t-roundtrip",
928 "total_turns": 1,
929 "total_usage": TokenUsage::default(),
930 "duration": { "secs": 1, "nanos": 0 },
931 }))?;
932 let reserialized = serde_json::to_value(&legacy_event)?;
933 assert_eq!(
934 reserialized
935 .get("duration_ms")
936 .and_then(serde_json::Value::as_u64),
937 Some(1000),
938 "round-trips must write the millis form: {reserialized}"
939 );
940 assert!(reserialized.get("duration").is_none());
941 Ok(())
942 }
943
944 #[test]
945 fn budget_exceeded_event_deserializes_legacy_duration_object() -> serde_json::Result<()> {
946 let legacy = serde_json::json!({
949 "type": "budget_exceeded",
950 "thread_id": "t-legacy",
951 "total_turns": 2,
952 "total_usage": TokenUsage::default(),
953 "duration": { "secs": 1, "nanos": 250_000_000 },
954 "limit": "total_tokens",
955 });
956 let event: AgentEvent = serde_json::from_value(legacy)?;
957 let AgentEvent::BudgetExceeded { duration, .. } = event else {
958 panic!("expected BudgetExceeded");
959 };
960 assert_eq!(duration, Duration::from_millis(1250));
961 Ok(())
962 }
963
964 #[test]
965 fn budget_exceeded_event_serializes_duration_as_millis() -> serde_json::Result<()> {
966 let seq = SequenceCounter::new();
967 let envelope = AgentEventEnvelope::wrap(
968 AgentEvent::budget_exceeded(
969 ThreadId::from_string("t"),
970 2,
971 TokenUsage::default(),
972 Duration::from_millis(1200),
973 Some(0.5),
974 BudgetLimitKind::TotalTokens,
975 ),
976 &seq,
977 );
978 let json = serde_json::to_value(&envelope)?;
979
980 assert_eq!(
982 json.get("duration_ms").and_then(serde_json::Value::as_u64),
983 Some(1200)
984 );
985 assert!(
986 json.get("duration").is_none(),
987 "no nested `duration` key expected: {json}"
988 );
989
990 let restored: AgentEventEnvelope = serde_json::from_value(json)?;
991 match restored.event {
992 AgentEvent::BudgetExceeded { duration, .. } => {
993 assert_eq!(duration, Duration::from_millis(1200));
994 }
995 other => panic!("expected BudgetExceeded, got {other:?}"),
996 }
997 Ok(())
998 }
999
1000 fn sample_all_variants() -> Vec<AgentEvent> {
1007 let thread = ThreadId::from_string("thread-1");
1008 let usage = TokenUsage::default();
1009 let mut events = session_open_events(&thread);
1010 events.extend(streamed_content_events());
1011 events.extend(tool_call_events());
1012 events.extend(turn_completion_events(&thread, &usage));
1013 events.extend(failure_and_retry_events());
1014 events.extend(auxiliary_events(&usage));
1015 events
1016 }
1017
1018 fn session_open_events(thread: &ThreadId) -> Vec<AgentEvent> {
1020 vec![
1021 AgentEvent::Start {
1022 thread_id: thread.clone(),
1023 turn: 1,
1024 },
1025 AgentEvent::UserInput {
1026 thread_id: thread.clone(),
1027 content: vec![ContentBlock::Text { text: "hi".into() }],
1028 },
1029 ]
1030 }
1031
1032 fn streamed_content_events() -> Vec<AgentEvent> {
1035 vec![
1036 AgentEvent::Thinking {
1037 message_id: "m".into(),
1038 text: "t".into(),
1039 },
1040 AgentEvent::ThinkingDelta {
1041 message_id: "m".into(),
1042 delta: "d".into(),
1043 },
1044 AgentEvent::TextDelta {
1045 message_id: "m".into(),
1046 delta: "d".into(),
1047 },
1048 AgentEvent::Text {
1049 message_id: "m".into(),
1050 text: "t".into(),
1051 },
1052 ]
1053 }
1054
1055 fn tool_call_events() -> Vec<AgentEvent> {
1057 vec![
1058 AgentEvent::ToolCallStart {
1059 id: "id".into(),
1060 name: "n".into(),
1061 display_name: "N".into(),
1062 input: serde_json::json!({}),
1063 tier: ToolTier::Observe,
1064 },
1065 AgentEvent::ToolCallEnd {
1066 id: "id".into(),
1067 name: "n".into(),
1068 display_name: "N".into(),
1069 result: ToolResult::success("ok"),
1070 },
1071 AgentEvent::ToolProgress {
1072 id: "id".into(),
1073 name: "n".into(),
1074 display_name: "N".into(),
1075 stage: "s".into(),
1076 message: "m".into(),
1077 data: None,
1078 },
1079 AgentEvent::ToolRequiresConfirmation {
1080 id: "id".into(),
1081 name: "n".into(),
1082 display_name: "N".into(),
1083 input: serde_json::json!({}),
1084 description: "d".into(),
1085 },
1086 ]
1087 }
1088
1089 fn turn_completion_events(thread: &ThreadId, usage: &TokenUsage) -> Vec<AgentEvent> {
1091 vec![
1092 AgentEvent::TurnComplete {
1093 turn: 1,
1094 usage: usage.clone(),
1095 },
1096 AgentEvent::Done {
1097 thread_id: thread.clone(),
1098 total_turns: 2,
1099 total_usage: usage.clone(),
1100 duration: Duration::from_millis(1500),
1101 estimated_cost_usd: Some(0.0123),
1102 },
1103 ]
1104 }
1105
1106 fn failure_and_retry_events() -> Vec<AgentEvent> {
1108 vec![
1109 AgentEvent::Error {
1110 message: "e".into(),
1111 recoverable: true,
1112 },
1113 AgentEvent::AutoRetryStart {
1114 attempt: 1,
1115 max_attempts: 5,
1116 delay_ms: 100,
1117 error_message: "rate limited".into(),
1118 },
1119 AgentEvent::AutoRetryEnd {
1120 attempt: 1,
1121 success: true,
1122 final_error: None,
1123 },
1124 ]
1125 }
1126
1127 fn auxiliary_events(usage: &TokenUsage) -> Vec<AgentEvent> {
1130 vec![
1131 AgentEvent::Refusal {
1132 message_id: "m".into(),
1133 text: Some("no".into()),
1134 },
1135 AgentEvent::Cancelled {
1136 turn: 1,
1137 usage: usage.clone(),
1138 },
1139 AgentEvent::BudgetExceeded {
1140 thread_id: ThreadId::from_string("thread-1"),
1141 total_turns: 3,
1142 total_usage: usage.clone(),
1143 duration: Duration::from_millis(750),
1144 estimated_cost_usd: Some(0.5),
1145 limit: BudgetLimitKind::CostUsd,
1146 },
1147 AgentEvent::ContextCompacted {
1148 original_count: 10,
1149 new_count: 5,
1150 original_tokens: 100,
1151 new_tokens: 50,
1152 },
1153 AgentEvent::SubagentProgress {
1154 subagent_id: "s".into(),
1155 subagent_name: "explore".into(),
1156 nickname: None,
1157 child_thread_id: None,
1158 child_root_task_id: None,
1159 subagent_task_id: None,
1160 max_turns: None,
1161 current_turn: None,
1162 model: None,
1163 tool_name: "t".into(),
1164 tool_context: "c".into(),
1165 completed: false,
1166 success: false,
1167 tool_count: 0,
1168 total_tokens: 0,
1169 },
1170 ]
1171 }
1172
1173 #[test]
1174 fn every_variant_envelope_has_flat_keys_and_round_trips() -> serde_json::Result<()> {
1175 let seq = SequenceCounter::new();
1176 for event in sample_all_variants() {
1177 let label = format!("{event:?}");
1178 let envelope = AgentEventEnvelope::wrap(event, &seq);
1179 let json = serde_json::to_value(&envelope)?;
1180
1181 for key in ["event_id", "sequence", "timestamp", "type"] {
1183 assert!(
1184 json.get(key).is_some(),
1185 "{label}: missing flat key `{key}` in {json}"
1186 );
1187 }
1188 assert!(
1191 json.get("event").is_none(),
1192 "{label}: unexpected nested `event` key in {json}"
1193 );
1194
1195 let restored: AgentEventEnvelope = serde_json::from_value(json.clone())?;
1196 assert_eq!(
1197 serde_json::to_value(&restored)?,
1198 json,
1199 "{label}: envelope round-trip changed the wire form"
1200 );
1201 }
1202 Ok(())
1203 }
1204}