1use chrono::{DateTime, Utc};
22use serde::{Deserialize, Deserializer, Serialize};
23use serde_json::Value;
24use std::collections::HashMap;
25use uuid::Uuid;
26
27#[cfg(feature = "openapi")]
28use utoipa::ToSchema;
29
30use crate::localization::localized_tool_display_name;
31use crate::typed_id::{AgentId, EventId, ExecId, HarnessId, MessageId, ModelId, SessionId, TurnId};
32use crate::user_facing_error::{UserFacingError, UserFacingErrorFields};
33
34pub const INPUT_MESSAGE: &str = "input.message";
40
41pub const OUTPUT_MESSAGE_STARTED: &str = "output.message.started";
43pub const OUTPUT_MESSAGE_DELTA: &str = "output.message.delta";
44pub const OUTPUT_MESSAGE_COMPLETED: &str = "output.message.completed";
45pub const OUTPUT_MESSAGE_REPLACED: &str = "output.message.replaced";
50
51pub const TURN_STARTED: &str = "turn.started";
53pub const TURN_COMPLETED: &str = "turn.completed";
54pub const TURN_FAILED: &str = "turn.failed";
55pub const TURN_SEALED: &str = "turn.sealed";
60pub const TURN_CANCELLED: &str = "turn.cancelled";
61
62pub const REASON_STARTED: &str = "reason.started";
64pub const REASON_COMPLETED: &str = "reason.completed";
65pub const REASON_RECOVERED: &str = "reason.recovered";
66pub const CAPABILITY_USAGE: &str = "capability.usage";
67pub const ACT_STARTED: &str = "act.started";
68pub const ACT_COMPLETED: &str = "act.completed";
69pub const TOOL_STARTED: &str = "tool.started";
70pub const TOOL_COMPLETED: &str = "tool.completed";
71pub const TOOL_PROGRESS: &str = "tool.progress";
72pub const TOOL_OUTPUT_DELTA: &str = "tool.output.delta";
73pub const TOOL_CALL_REQUESTED: &str = "tool.call_requested";
74pub const TRANSCRIPT_REPAIRED: &str = "transcript.repaired";
75pub const TOOL_CALL_REPAIRED: &str = "tool.call_repaired";
79
80pub const LLM_GENERATION: &str = "llm.generation";
82
83fn is_ephemeral_event_type(event_type: &str) -> bool {
87 matches!(
88 event_type,
89 OUTPUT_MESSAGE_DELTA
90 | REASON_THINKING_DELTA
91 | TOOL_OUTPUT_DELTA
92 | VOICE_INPUT_TRANSCRIPT_DELTA
93 | VOICE_OUTPUT_TRANSCRIPT_DELTA
94 )
95}
96
97pub const REASON_THINKING_STARTED: &str = "reason.thinking.started";
99pub const REASON_THINKING_DELTA: &str = "reason.thinking.delta";
100pub const REASON_THINKING_COMPLETED: &str = "reason.thinking.completed";
101
102pub const REASON_ITEM: &str = "reason.item";
109
110pub const SESSION_STARTED: &str = "session.started";
112pub const SESSION_ACTIVATED: &str = "session.activated";
113pub const SESSION_IDLED: &str = "session.idled";
114pub const SESSION_TITLE_UPDATED: &str = "session.title.updated";
117
118pub const SCHEDULE_TRIGGERED: &str = "schedule.triggered";
120
121pub const TASK_CREATED: &str = "task.created";
128pub const TASK_UPDATED: &str = "task.updated";
129pub const TASK_MESSAGE_SENT: &str = "task.message.sent";
130pub const TASK_MESSAGE_RECEIVED: &str = "task.message.received";
131
132pub const CONTEXT_COMPACTING: &str = "context.compacting";
134pub const CONTEXT_COMPACTED: &str = "context.compacted";
135
136pub const FILE_WRITTEN: &str = "file.written";
138
139pub const BUDGET_WARNING: &str = "budget.warning";
141pub const BUDGET_PAUSED: &str = "budget.paused";
142pub const BUDGET_EXHAUSTED: &str = "budget.exhausted";
143pub const BUDGET_RESUMED: &str = "budget.resumed";
144
145pub const VOICE_SESSION_STARTED: &str = "voice.session.started";
147pub const VOICE_INPUT_TRANSCRIPT_DELTA: &str = "voice.input_transcript.delta";
148pub const VOICE_INPUT_TRANSCRIPT_COMPLETED: &str = "voice.input_transcript.completed";
149pub const VOICE_OUTPUT_TRANSCRIPT_DELTA: &str = "voice.output_transcript.delta";
150pub const VOICE_OUTPUT_TRANSCRIPT_COMPLETED: &str = "voice.output_transcript.completed";
151pub const VOICE_SESSION_ENDED: &str = "voice.session.ended";
152pub const VOICE_SESSION_FAILED: &str = "voice.session.failed";
153
154pub const VALID_EVENT_TYPES: &[&str] = &[
158 INPUT_MESSAGE,
159 OUTPUT_MESSAGE_STARTED,
160 OUTPUT_MESSAGE_DELTA,
161 OUTPUT_MESSAGE_COMPLETED,
162 OUTPUT_MESSAGE_REPLACED,
163 TURN_STARTED,
164 TURN_COMPLETED,
165 TURN_FAILED,
166 TURN_SEALED,
167 TURN_CANCELLED,
168 REASON_STARTED,
169 REASON_COMPLETED,
170 REASON_RECOVERED,
171 ACT_STARTED,
172 ACT_COMPLETED,
173 TOOL_STARTED,
174 TOOL_COMPLETED,
175 TOOL_PROGRESS,
176 TOOL_OUTPUT_DELTA,
177 TOOL_CALL_REQUESTED,
178 TRANSCRIPT_REPAIRED,
179 TOOL_CALL_REPAIRED,
180 LLM_GENERATION,
181 REASON_THINKING_STARTED,
182 REASON_THINKING_DELTA,
183 REASON_THINKING_COMPLETED,
184 REASON_ITEM,
185 SESSION_STARTED,
186 SESSION_ACTIVATED,
187 SESSION_IDLED,
188 SESSION_TITLE_UPDATED,
189 SCHEDULE_TRIGGERED,
190 CONTEXT_COMPACTING,
191 CONTEXT_COMPACTED,
192 BUDGET_WARNING,
193 BUDGET_PAUSED,
194 BUDGET_EXHAUSTED,
195 BUDGET_RESUMED,
196 VOICE_SESSION_STARTED,
197 VOICE_INPUT_TRANSCRIPT_DELTA,
198 VOICE_INPUT_TRANSCRIPT_COMPLETED,
199 VOICE_OUTPUT_TRANSCRIPT_DELTA,
200 VOICE_OUTPUT_TRANSCRIPT_COMPLETED,
201 VOICE_SESSION_ENDED,
202 VOICE_SESSION_FAILED,
203 FILE_WRITTEN,
204 CAPABILITY_USAGE,
205];
206
207use crate::atoms::AtomContext;
212
213#[derive(Debug, Clone, Serialize, Deserialize, Default)]
220#[cfg_attr(feature = "openapi", derive(ToSchema))]
221pub struct EventContext {
222 #[serde(skip_serializing_if = "Option::is_none")]
224 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "turn_01933b5a00007000800000000000001"))]
225 pub turn_id: Option<TurnId>,
226
227 #[serde(skip_serializing_if = "Option::is_none")]
229 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "message_01933b5a00007000800000000000001"))]
230 pub input_message_id: Option<MessageId>,
231
232 #[serde(skip_serializing_if = "Option::is_none")]
234 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "exec_01933b5a00007000800000000000001"))]
235 pub exec_id: Option<ExecId>,
236
237 #[serde(skip_serializing_if = "Option::is_none")]
240 pub trace_id: Option<String>,
241
242 #[serde(skip_serializing_if = "Option::is_none")]
245 pub span_id: Option<String>,
246
247 #[serde(skip_serializing_if = "Option::is_none")]
250 pub parent_span_id: Option<String>,
251}
252
253impl EventContext {
254 pub fn empty() -> Self {
256 Self::default()
257 }
258
259 pub fn from_atom_context(ctx: &AtomContext) -> Self {
261 Self {
262 turn_id: Some(ctx.turn_id),
263 input_message_id: Some(ctx.input_message_id),
264 exec_id: Some(ctx.exec_id),
265 trace_id: None,
266 span_id: None,
267 parent_span_id: None,
268 }
269 }
270
271 pub fn turn(turn_id: TurnId, input_message_id: MessageId) -> Self {
273 Self {
274 turn_id: Some(turn_id),
275 input_message_id: Some(input_message_id),
276 exec_id: None,
277 trace_id: None,
278 span_id: None,
279 parent_span_id: None,
280 }
281 }
282
283 pub fn with_span(
285 mut self,
286 trace_id: String,
287 span_id: String,
288 parent_span_id: Option<String>,
289 ) -> Self {
290 self.trace_id = Some(trace_id);
291 self.span_id = Some(span_id);
292 self.parent_span_id = parent_span_id;
293 self
294 }
295}
296
297#[derive(Debug, Clone, Serialize)]
313#[cfg_attr(feature = "openapi", derive(ToSchema))]
314pub struct Event {
315 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "event_01933b5a00007000800000000000001"))]
317 pub id: EventId,
318
319 #[serde(rename = "type")]
321 pub event_type: String,
322
323 pub ts: DateTime<Utc>,
325
326 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "session_01933b5a00007000800000000000001"))]
328 pub session_id: SessionId,
329
330 pub context: EventContext,
332
333 pub data: EventData,
336
337 #[serde(skip_serializing_if = "Option::is_none")]
339 pub metadata: Option<serde_json::Value>,
340
341 #[serde(skip_serializing_if = "Option::is_none")]
343 pub tags: Option<Vec<String>>,
344
345 #[serde(skip_serializing_if = "Option::is_none")]
347 pub sequence: Option<i32>,
348}
349
350#[derive(Debug, Deserialize)]
351struct RawEvent {
352 id: EventId,
353 #[serde(rename = "type")]
354 event_type: String,
355 ts: DateTime<Utc>,
356 session_id: SessionId,
357 context: EventContext,
358 data: serde_json::Value,
359 metadata: Option<serde_json::Value>,
360 tags: Option<Vec<String>>,
361 sequence: Option<i32>,
362}
363
364impl<'de> Deserialize<'de> for Event {
365 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
366 where
367 D: Deserializer<'de>,
368 {
369 let raw = RawEvent::deserialize(deserializer)?;
370 let data = deserialize_event_data(&raw.event_type, raw.data);
371 Ok(Self {
372 id: raw.id,
373 event_type: raw.event_type,
374 ts: raw.ts,
375 session_id: raw.session_id,
376 context: raw.context,
377 data,
378 metadata: raw.metadata,
379 tags: raw.tags,
380 sequence: raw.sequence,
381 })
382 }
383}
384
385impl Event {
386 pub fn new(session_id: SessionId, context: EventContext, data: impl Into<EventData>) -> Self {
390 let data = data.into();
391 let event_type = data.event_type().to_string();
392 Self {
393 id: EventId::new(),
394 event_type,
395 ts: Utc::now(),
396 session_id,
397 context,
398 data,
399 metadata: None,
400 tags: None,
401 sequence: None,
402 }
403 }
404
405 pub fn with_id(
407 id: EventId,
408 session_id: SessionId,
409 context: EventContext,
410 data: impl Into<EventData>,
411 ) -> Self {
412 let data = data.into();
413 let event_type = data.event_type().to_string();
414 Self {
415 id,
416 event_type,
417 ts: Utc::now(),
418 session_id,
419 context,
420 data,
421 metadata: None,
422 tags: None,
423 sequence: None,
424 }
425 }
426
427 pub fn with_sequence(mut self, sequence: i32) -> Self {
429 self.sequence = Some(sequence);
430 self
431 }
432
433 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
435 self.metadata = Some(metadata);
436 self
437 }
438
439 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
441 self.tags = Some(tags);
442 self
443 }
444
445 pub fn session_uuid(&self) -> Uuid {
447 self.session_id.uuid()
448 }
449
450 pub fn is_message_event(&self) -> bool {
452 self.event_type == INPUT_MESSAGE || self.event_type == OUTPUT_MESSAGE_COMPLETED
453 }
454
455 pub fn is_ephemeral(&self) -> bool {
463 is_ephemeral_event_type(&self.event_type)
464 }
465
466 pub fn is_input_event(&self) -> bool {
468 self.event_type.starts_with("input.")
469 }
470
471 pub fn is_output_event(&self) -> bool {
473 self.event_type.starts_with("output.")
474 }
475
476 pub fn is_atom_event(&self) -> bool {
478 matches!(
479 self.event_type.as_str(),
480 REASON_STARTED
481 | REASON_COMPLETED
482 | REASON_RECOVERED
483 | ACT_STARTED
484 | ACT_COMPLETED
485 | TOOL_STARTED
486 | TOOL_COMPLETED
487 | TOOL_PROGRESS
488 | TOOL_CALL_REQUESTED
489 | TRANSCRIPT_REPAIRED
490 )
491 }
492
493 pub fn is_turn_event(&self) -> bool {
495 self.event_type.starts_with("turn.")
496 }
497
498 pub fn is_session_event(&self) -> bool {
500 self.event_type.starts_with("session.")
501 }
502
503 pub fn is_unsupported(&self) -> bool {
506 self.data.is_unsupported()
507 }
508}
509
510use crate::message::{ContentPart, ExecutionPhase, Message};
515use crate::tool_narration::{
516 ToolNarrationPhase, render_group_headline_with_locale, render_tool_narration_with_locale,
517};
518use crate::tool_types::ToolCall;
519
520#[derive(Debug, Clone, Serialize, Deserialize)]
522#[cfg_attr(feature = "openapi", derive(ToSchema))]
523pub struct ModelMetadata {
524 pub model: String,
526
527 #[serde(skip_serializing_if = "Option::is_none")]
529 pub model_id: Option<Uuid>,
530
531 #[serde(skip_serializing_if = "Option::is_none")]
533 pub provider_id: Option<Uuid>,
534}
535
536#[derive(Debug, Clone, Serialize, Deserialize, Default)]
563#[cfg_attr(feature = "openapi", derive(ToSchema))]
564pub struct TokenUsage {
565 pub input_tokens: u32,
568 pub output_tokens: u32,
570 #[serde(skip_serializing_if = "Option::is_none")]
572 pub cache_read_tokens: Option<u32>,
573 #[serde(skip_serializing_if = "Option::is_none")]
575 pub cache_creation_tokens: Option<u32>,
576
577 #[serde(skip_serializing_if = "Option::is_none")]
581 pub actual_cost_usd: Option<f64>,
582
583 #[serde(skip_serializing_if = "Option::is_none")]
588 pub estimated_cost_usd: Option<f64>,
589
590 #[serde(skip_serializing_if = "Option::is_none")]
593 pub effective_cost_usd: Option<f64>,
594}
595
596impl TokenUsage {
597 pub fn new(input_tokens: u32, output_tokens: u32) -> Self {
599 Self {
600 input_tokens,
601 output_tokens,
602 cache_read_tokens: None,
603 cache_creation_tokens: None,
604 actual_cost_usd: None,
605 estimated_cost_usd: None,
606 effective_cost_usd: None,
607 }
608 }
609
610 pub fn with_cache(
612 input_tokens: u32,
613 output_tokens: u32,
614 cache_read_tokens: Option<u32>,
615 cache_creation_tokens: Option<u32>,
616 ) -> Self {
617 Self {
618 input_tokens,
619 output_tokens,
620 cache_read_tokens,
621 cache_creation_tokens,
622 actual_cost_usd: None,
623 estimated_cost_usd: None,
624 effective_cost_usd: None,
625 }
626 }
627
628 pub fn with_cost(
632 mut self,
633 actual_cost_usd: Option<f64>,
634 estimated_cost_usd: Option<f64>,
635 ) -> Self {
636 self.actual_cost_usd = actual_cost_usd;
637 self.estimated_cost_usd = estimated_cost_usd;
638 self
639 }
640
641 pub fn with_effective_cost(mut self, effective_cost_usd: Option<f64>) -> Self {
645 self.effective_cost_usd = effective_cost_usd;
646 self
647 }
648
649 pub fn effective_cost_usd(&self) -> Option<f64> {
653 self.effective_cost_usd
654 .or(self.actual_cost_usd.or(self.estimated_cost_usd))
655 }
656
657 pub fn total_tokens(&self) -> u32 {
659 self.input_tokens.saturating_add(self.output_tokens)
660 }
661
662 pub fn add(&mut self, other: &TokenUsage) {
664 let current_cost = self.effective_cost_usd();
669 self.input_tokens = self.input_tokens.saturating_add(other.input_tokens);
670 self.output_tokens = self.output_tokens.saturating_add(other.output_tokens);
671 if let Some(cache) = other.cache_read_tokens {
672 let total = self.cache_read_tokens.get_or_insert(0);
673 *total = total.saturating_add(cache);
674 }
675 if let Some(cache) = other.cache_creation_tokens {
676 let total = self.cache_creation_tokens.get_or_insert(0);
677 *total = total.saturating_add(cache);
678 }
679 if let Some(cost) = other.actual_cost_usd {
680 *self.actual_cost_usd.get_or_insert(0.0) += cost;
681 }
682 if let Some(cost) = other.estimated_cost_usd {
683 *self.estimated_cost_usd.get_or_insert(0.0) += cost;
684 }
685 if let Some(cost) = other.effective_cost_usd() {
686 *self
687 .effective_cost_usd
688 .get_or_insert(current_cost.unwrap_or(0.0)) += cost;
689 }
690 }
691}
692
693#[cfg(test)]
694mod token_usage_tests {
695 use super::TokenUsage;
696
697 #[test]
698 fn aggregate_effective_cost_preserves_mixed_actual_and_estimated_total() {
699 let aggregate = TokenUsage::with_cache(30, 15, None, None)
700 .with_cost(Some(1.0), Some(2.0))
701 .with_effective_cost(Some(3.0));
702
703 assert_eq!(aggregate.actual_cost_usd, Some(1.0));
704 assert_eq!(aggregate.estimated_cost_usd, Some(2.0));
705 assert_eq!(aggregate.effective_cost_usd(), Some(3.0));
706 }
707
708 #[test]
709 fn generation_effective_cost_still_prefers_actual_over_estimated() {
710 let generation = TokenUsage::new(10, 5).with_cost(Some(1.0), Some(2.0));
711
712 assert_eq!(generation.effective_cost_usd(), Some(1.0));
713 }
714
715 #[test]
716 fn add_seeds_effective_cost_from_accumulator_implicit_cost() {
717 let mut aggregate = TokenUsage::new(10, 5).with_cost(Some(1.0), Some(10.0));
718 let generation = TokenUsage::new(20, 10).with_cost(None, Some(2.0));
719
720 aggregate.add(&generation);
721
722 assert_eq!(aggregate.actual_cost_usd, Some(1.0));
723 assert_eq!(aggregate.estimated_cost_usd, Some(12.0));
724 assert_eq!(aggregate.effective_cost_usd(), Some(3.0));
725 }
726
727 #[test]
728 fn add_effective_cost_does_not_double_count_when_actual_is_mutated() {
729 let mut aggregate = TokenUsage::new(10, 5).with_cost(Some(1.0), None);
734 let generation = TokenUsage::new(20, 10).with_cost(Some(3.0), None);
735
736 aggregate.add(&generation);
737
738 assert_eq!(aggregate.actual_cost_usd, Some(4.0));
739 assert_eq!(aggregate.effective_cost_usd(), Some(4.0));
740 }
741
742 #[test]
743 fn aggregate_token_counters_saturate_at_their_bound() {
744 let mut aggregate = TokenUsage::with_cache(
745 u32::MAX - 1,
746 u32::MAX - 1,
747 Some(u32::MAX - 1),
748 Some(u32::MAX - 1),
749 );
750 aggregate.add(&TokenUsage::with_cache(10, 10, Some(10), Some(10)));
751
752 assert_eq!(aggregate.input_tokens, u32::MAX);
753 assert_eq!(aggregate.output_tokens, u32::MAX);
754 assert_eq!(aggregate.cache_read_tokens, Some(u32::MAX));
755 assert_eq!(aggregate.cache_creation_tokens, Some(u32::MAX));
756 assert_eq!(aggregate.total_tokens(), u32::MAX);
757 }
758}
759
760#[derive(Debug, Clone, Serialize, Deserialize)]
762#[cfg_attr(feature = "openapi", derive(ToSchema))]
763pub struct InputMessageData {
764 pub message: Message,
766}
767
768impl InputMessageData {
769 pub fn new(message: Message) -> Self {
770 Self { message }
771 }
772}
773
774#[derive(Debug, Clone, Serialize, Deserialize)]
783#[cfg_attr(feature = "openapi", derive(ToSchema))]
784pub struct OutputMessageStartedData {
785 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
787 pub turn_id: TurnId,
788
789 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_550e8400e29b41d4a716446655440000"))]
792 pub message_id: MessageId,
793
794 #[serde(skip_serializing_if = "Option::is_none")]
796 pub model: Option<String>,
797
798 #[serde(skip_serializing_if = "Option::is_none")]
801 pub iteration: Option<u32>,
802
803 #[serde(default, skip_serializing_if = "Option::is_none")]
813 pub phase: Option<ExecutionPhase>,
814}
815
816#[derive(Debug, Clone, Serialize, Deserialize)]
821#[cfg_attr(feature = "openapi", derive(ToSchema))]
822pub struct OutputMessageDeltaData {
823 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
825 pub turn_id: TurnId,
826
827 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_550e8400e29b41d4a716446655440000"))]
830 pub message_id: MessageId,
831
832 pub delta: String,
834
835 pub accumulated: String,
837
838 #[serde(default, skip_serializing_if = "Option::is_none")]
849 pub phase: Option<ExecutionPhase>,
850}
851
852#[derive(Debug, Clone, Serialize, Deserialize)]
854#[cfg_attr(feature = "openapi", derive(ToSchema))]
855pub struct OutputMessageCompletedData {
856 pub message: Message,
858
859 #[serde(skip_serializing_if = "Option::is_none")]
861 pub metadata: Option<ModelMetadata>,
862
863 #[serde(skip_serializing_if = "Option::is_none")]
865 pub usage: Option<TokenUsage>,
866
867 #[serde(default, skip_serializing_if = "Option::is_none")]
869 pub error_code: Option<String>,
870
871 #[serde(default, skip_serializing_if = "Option::is_none")]
873 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
874 pub error_fields: Option<UserFacingErrorFields>,
875
876 #[serde(default, skip_serializing_if = "Option::is_none")]
880 pub error_disclosure: Option<String>,
881}
882
883impl OutputMessageCompletedData {
884 pub fn new(message: Message) -> Self {
885 Self {
886 message,
887 metadata: None,
888 usage: None,
889 error_code: None,
890 error_fields: None,
891 error_disclosure: None,
892 }
893 }
894
895 pub fn with_metadata(mut self, metadata: ModelMetadata) -> Self {
896 self.metadata = Some(metadata);
897 self
898 }
899
900 pub fn with_usage(mut self, usage: TokenUsage) -> Self {
901 self.usage = Some(usage);
902 self
903 }
904
905 pub fn with_user_facing_error(mut self, error: &UserFacingError) -> Self {
906 error.apply_to_event_fields(&mut self.error_code, &mut self.error_fields);
907 self
908 }
909
910 pub fn with_error_disclosure(mut self, mode: crate::ErrorDisclosure) -> Self {
911 self.error_disclosure = Some(mode.as_str().to_string());
912 self
913 }
914}
915
916#[derive(Debug, Clone, Serialize, Deserialize)]
923#[cfg_attr(feature = "openapi", derive(ToSchema))]
924pub struct OutputMessageReplacedData {
925 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
927 pub turn_id: TurnId,
928
929 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_550e8400e29b41d4a716446655440000"))]
932 pub message_id: MessageId,
933
934 pub guardrail_capability_id: String,
937
938 pub guardrail_id: String,
940
941 pub reason_code: String,
944
945 pub replacement: String,
947}
948
949#[derive(Debug, Clone, Serialize, Deserialize)]
955#[cfg_attr(feature = "openapi", derive(ToSchema))]
956pub struct ReasonStartedData {
957 pub harness_id: HarnessId,
959
960 #[serde(skip_serializing_if = "Option::is_none")]
962 pub agent_id: Option<AgentId>,
963
964 #[serde(skip_serializing_if = "Option::is_none")]
966 pub metadata: Option<ModelMetadata>,
967}
968
969#[derive(Debug, Clone, Serialize, Deserialize)]
971#[cfg_attr(feature = "openapi", derive(ToSchema))]
972pub struct ReasonCompletedData {
973 pub success: bool,
975
976 #[serde(skip_serializing_if = "Option::is_none")]
978 pub text_preview: Option<String>,
979
980 pub has_tool_calls: bool,
982
983 pub tool_call_count: u32,
985
986 #[serde(skip_serializing_if = "Option::is_none")]
988 pub error: Option<String>,
989
990 #[serde(skip_serializing_if = "Option::is_none")]
992 pub duration_ms: Option<u64>,
993
994 #[serde(skip_serializing_if = "Option::is_none")]
996 pub usage: Option<TokenUsage>,
997}
998
999impl ReasonCompletedData {
1000 pub fn success(
1001 text: &str,
1002 has_tool_calls: bool,
1003 tool_call_count: u32,
1004 duration_ms: Option<u64>,
1005 usage: Option<TokenUsage>,
1006 ) -> Self {
1007 let text_preview = if text.is_empty() {
1008 None
1009 } else {
1010 Some(text.chars().take(200).collect())
1011 };
1012
1013 Self {
1014 success: true,
1015 text_preview,
1016 has_tool_calls,
1017 tool_call_count,
1018 error: None,
1019 duration_ms,
1020 usage,
1021 }
1022 }
1023
1024 pub fn failure(error: String, duration_ms: Option<u64>) -> Self {
1025 Self {
1026 success: false,
1027 text_preview: None,
1028 has_tool_calls: false,
1029 tool_call_count: 0,
1030 error: Some(error),
1031 duration_ms,
1032 usage: None,
1033 }
1034 }
1035}
1036
1037#[derive(Debug, Clone, Serialize, Deserialize)]
1039#[cfg_attr(feature = "openapi", derive(ToSchema))]
1040#[serde(rename_all = "snake_case")]
1041pub enum RecoveryMode {
1042 Finalize,
1045 Restart,
1047}
1048
1049#[derive(Debug, Clone, Serialize, Deserialize)]
1055#[cfg_attr(feature = "openapi", derive(ToSchema))]
1056pub struct ReasonRecoveredData {
1057 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1059 pub turn_id: TurnId,
1060
1061 pub mode: RecoveryMode,
1063
1064 pub accumulated_len: usize,
1066}
1067
1068#[derive(Debug, Clone, Serialize, Deserialize)]
1070#[serde(rename_all = "snake_case")]
1071#[cfg_attr(feature = "openapi", derive(ToSchema))]
1072pub enum CapabilityUsageKind {
1073 Configured,
1074 Resolved,
1075 Exposed,
1076 Invoked,
1077 EffectRan,
1078}
1079
1080#[derive(Debug, Clone, Serialize, Deserialize)]
1084#[cfg_attr(feature = "openapi", derive(ToSchema))]
1085pub struct CapabilityUsageRecord {
1086 pub capability_id: String,
1088 #[serde(default, skip_serializing_if = "Option::is_none")]
1090 pub capability_name: Option<String>,
1091 pub usage_kind: CapabilityUsageKind,
1093 #[serde(default, skip_serializing_if = "Option::is_none")]
1095 pub tool_name: Option<String>,
1096 #[serde(default, skip_serializing_if = "Option::is_none")]
1098 pub usage_count: Option<u64>,
1099 #[serde(default, skip_serializing_if = "Option::is_none")]
1101 pub duration_ms: Option<u64>,
1102}
1103
1104#[derive(Debug, Clone, Serialize, Deserialize)]
1106#[cfg_attr(feature = "openapi", derive(ToSchema))]
1107pub struct CapabilityUsageData {
1108 pub records: Vec<CapabilityUsageRecord>,
1109}
1110
1111#[derive(Debug, Clone, Serialize, Deserialize)]
1113#[cfg_attr(feature = "openapi", derive(ToSchema))]
1114pub struct ToolCallSummary {
1115 pub id: String,
1116 pub name: String,
1117 #[serde(default, skip_serializing_if = "Option::is_none")]
1119 pub display_name: Option<String>,
1120 #[serde(default, skip_serializing_if = "Option::is_none")]
1122 pub narration: Option<String>,
1123}
1124
1125impl From<&ToolCall> for ToolCallSummary {
1126 fn from(tc: &ToolCall) -> Self {
1127 Self {
1128 id: tc.id.clone(),
1129 name: tc.name.clone(),
1130 display_name: None,
1131 narration: None,
1132 }
1133 }
1134}
1135
1136#[derive(Debug, Clone, Serialize, Deserialize)]
1138#[cfg_attr(feature = "openapi", derive(ToSchema))]
1139pub struct ToolDefinitionSummary {
1140 pub name: String,
1142 #[serde(default, skip_serializing_if = "Option::is_none")]
1144 pub display_name: Option<String>,
1145 #[serde(default, skip_serializing_if = "Option::is_none")]
1147 pub category: Option<String>,
1148 #[serde(default, skip_serializing_if = "Option::is_none")]
1150 pub capability_id: Option<String>,
1151 #[serde(default, skip_serializing_if = "Option::is_none")]
1153 pub capability_name: Option<String>,
1154 pub description: String,
1156}
1157
1158impl From<&crate::tool_types::ToolDefinition> for ToolDefinitionSummary {
1159 fn from(tool: &crate::tool_types::ToolDefinition) -> Self {
1160 let capability_attribution = tool.capability_attribution();
1161 Self {
1162 name: tool.name().to_string(),
1163 display_name: tool.display_name().map(|s| s.to_string()),
1164 category: tool.category().map(|s| s.to_string()),
1165 capability_id: capability_attribution.map(|(id, _)| id.to_string()),
1166 capability_name: capability_attribution.and_then(|(_, name)| name.map(str::to_string)),
1167 description: tool.description().to_string(),
1168 }
1169 }
1170}
1171
1172#[derive(Debug, Clone, Serialize, Deserialize)]
1174#[cfg_attr(feature = "openapi", derive(ToSchema))]
1175pub struct ActStartedData {
1176 pub tool_calls: Vec<ToolCallSummary>,
1178 #[serde(default, skip_serializing_if = "Option::is_none")]
1180 pub headline: Option<String>,
1181}
1182
1183impl ActStartedData {
1184 pub fn new(tool_calls: &[ToolCall]) -> Self {
1185 Self::new_with_locale(tool_calls, None)
1186 }
1187
1188 pub fn new_with_locale(tool_calls: &[ToolCall], locale: Option<&str>) -> Self {
1189 Self {
1190 tool_calls: tool_calls.iter().map(ToolCallSummary::from).collect(),
1191 headline: render_group_headline_with_locale(
1192 tool_calls,
1193 &[],
1194 ToolNarrationPhase::Started,
1195 locale,
1196 ),
1197 }
1198 }
1199
1200 pub fn with_definitions(
1202 tool_calls: &[ToolCall],
1203 tool_defs: &[crate::tool_types::ToolDefinition],
1204 ) -> Self {
1205 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
1206 }
1207
1208 pub fn with_definitions_and_locale(
1209 tool_calls: &[ToolCall],
1210 tool_defs: &[crate::tool_types::ToolDefinition],
1211 locale: Option<&str>,
1212 ) -> Self {
1213 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
1214 tool_defs.iter().map(|d| (d.name(), d)).collect();
1215 Self {
1216 tool_calls: tool_calls
1217 .iter()
1218 .map(|tc| {
1219 let tool_def = def_map.get(tc.name.as_str()).copied();
1220 let display_name = localized_tool_display_name(
1221 &tc.name,
1222 tool_def.and_then(|d| d.display_name()),
1223 locale,
1224 );
1225 ToolCallSummary {
1226 id: tc.id.clone(),
1227 name: tc.name.clone(),
1228 display_name,
1229 narration: Some(render_tool_narration_with_locale(
1230 tool_def,
1231 tc,
1232 ToolNarrationPhase::Started,
1233 locale,
1234 )),
1235 }
1236 })
1237 .collect(),
1238 headline: render_group_headline_with_locale(
1239 tool_calls,
1240 tool_defs,
1241 ToolNarrationPhase::Started,
1242 locale,
1243 ),
1244 }
1245 }
1246}
1247
1248#[derive(Debug, Clone, Serialize, Deserialize)]
1250#[cfg_attr(feature = "openapi", derive(ToSchema))]
1251pub struct ActCompletedData {
1252 pub completed: bool,
1254
1255 pub success_count: u32,
1257
1258 pub error_count: u32,
1260
1261 #[serde(skip_serializing_if = "Option::is_none")]
1263 pub duration_ms: Option<u64>,
1264 #[serde(default, skip_serializing_if = "Option::is_none")]
1266 pub headline: Option<String>,
1267}
1268
1269#[derive(Debug, Clone, Serialize, Deserialize)]
1271#[cfg_attr(feature = "openapi", derive(ToSchema))]
1272pub struct ToolStartedData {
1273 pub tool_call: ToolCall,
1275 #[serde(default, skip_serializing_if = "Option::is_none")]
1277 pub tool_call_fingerprint: Option<String>,
1278 #[serde(default, skip_serializing_if = "Option::is_none")]
1280 pub display_name: Option<String>,
1281 #[serde(default, skip_serializing_if = "Option::is_none")]
1283 pub narration: Option<String>,
1284}
1285
1286#[derive(Debug, Clone, Serialize, Deserialize)]
1288#[cfg_attr(feature = "openapi", derive(ToSchema))]
1289pub struct ToolCompletedData {
1290 pub tool_call_id: String,
1292
1293 pub tool_name: String,
1295
1296 #[serde(default, skip_serializing_if = "Option::is_none")]
1298 pub tool_call_fingerprint: Option<String>,
1299
1300 #[serde(default, skip_serializing_if = "Option::is_none")]
1302 pub tool_result_fingerprint: Option<String>,
1303
1304 #[serde(default, skip_serializing_if = "Option::is_none")]
1306 pub display_name: Option<String>,
1307
1308 pub success: bool,
1310
1311 pub status: String,
1313
1314 #[serde(skip_serializing_if = "Option::is_none")]
1316 pub result: Option<Vec<ContentPart>>,
1317
1318 #[serde(skip_serializing_if = "Option::is_none")]
1320 pub error: Option<String>,
1321
1322 #[serde(skip_serializing_if = "Option::is_none")]
1324 pub duration_ms: Option<u64>,
1325
1326 #[serde(default, skip_serializing_if = "Option::is_none")]
1328 pub capability_id: Option<String>,
1329
1330 #[serde(default, skip_serializing_if = "Option::is_none")]
1332 pub capability_name: Option<String>,
1333
1334 #[serde(default, skip_serializing_if = "Option::is_none")]
1336 pub narration: Option<String>,
1337}
1338
1339impl ToolCompletedData {
1340 pub fn success(
1341 tool_call_id: String,
1342 tool_name: String,
1343 result: Vec<ContentPart>,
1344 duration_ms: Option<u64>,
1345 ) -> Self {
1346 Self {
1347 tool_call_id,
1348 tool_name,
1349 tool_call_fingerprint: None,
1350 tool_result_fingerprint: None,
1351 display_name: None,
1352 success: true,
1353 status: "success".to_string(),
1354 result: Some(result),
1355 error: None,
1356 duration_ms,
1357 capability_id: None,
1358 capability_name: None,
1359 narration: None,
1360 }
1361 }
1362
1363 pub fn failure(
1364 tool_call_id: String,
1365 tool_name: String,
1366 status: String,
1367 error: String,
1368 duration_ms: Option<u64>,
1369 ) -> Self {
1370 Self {
1371 tool_call_id,
1372 tool_name,
1373 tool_call_fingerprint: None,
1374 tool_result_fingerprint: None,
1375 display_name: None,
1376 success: false,
1377 status,
1378 result: None,
1379 error: Some(error),
1380 duration_ms,
1381 capability_id: None,
1382 capability_name: None,
1383 narration: None,
1384 }
1385 }
1386
1387 pub fn with_display_name(mut self, display_name: Option<String>) -> Self {
1389 self.display_name = display_name;
1390 self
1391 }
1392
1393 pub fn with_fingerprints(
1394 mut self,
1395 tool_call_fingerprint: String,
1396 tool_result_fingerprint: String,
1397 ) -> Self {
1398 self.tool_call_fingerprint = Some(tool_call_fingerprint);
1399 self.tool_result_fingerprint = Some(tool_result_fingerprint);
1400 self
1401 }
1402
1403 pub fn with_narration(mut self, narration: Option<String>) -> Self {
1405 self.narration = narration;
1406 self
1407 }
1408
1409 pub fn with_capability_attribution(
1411 mut self,
1412 capability_id: Option<String>,
1413 capability_name: Option<String>,
1414 ) -> Self {
1415 self.capability_id = capability_id;
1416 self.capability_name = capability_name;
1417 self
1418 }
1419}
1420
1421#[derive(Debug, Clone, Serialize, Deserialize)]
1427#[cfg_attr(feature = "openapi", derive(ToSchema))]
1428pub struct ToolProgressData {
1429 pub tool_call_id: String,
1431
1432 pub tool_name: String,
1434
1435 pub message: String,
1437
1438 #[serde(default, skip_serializing_if = "Option::is_none")]
1440 pub display_name: Option<String>,
1441}
1442
1443#[derive(Debug, Clone, Serialize, Deserialize)]
1453#[cfg_attr(feature = "openapi", derive(ToSchema))]
1454pub struct ToolOutputDeltaData {
1455 pub tool_call_id: String,
1457
1458 pub tool_name: String,
1460
1461 pub delta: String,
1463
1464 pub stream: String,
1466}
1467
1468#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1470#[cfg_attr(feature = "openapi", derive(ToSchema))]
1471#[serde(rename_all = "snake_case")]
1472pub enum TranscriptRepairAction {
1473 Replay,
1475 Synthesize,
1477}
1478
1479#[derive(Debug, Clone, Serialize, Deserialize)]
1485#[cfg_attr(feature = "openapi", derive(ToSchema))]
1486pub struct TranscriptRepairedData {
1487 pub tool_call_id: String,
1489
1490 #[serde(default, skip_serializing_if = "Option::is_none")]
1492 pub tool_name: Option<String>,
1493
1494 pub action: TranscriptRepairAction,
1496}
1497
1498#[derive(Debug, Clone, Serialize, Deserialize)]
1504#[cfg_attr(feature = "openapi", derive(ToSchema))]
1505pub struct ToolCallRepairedData {
1506 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1508 pub turn_id: TurnId,
1509
1510 pub tool_call_id: String,
1512
1513 pub tool_name: String,
1515
1516 pub outcome: String,
1518}
1519
1520#[derive(Debug, Clone, Serialize, Deserialize)]
1525#[cfg_attr(feature = "openapi", derive(ToSchema))]
1526pub struct ToolCallRequestedData {
1527 pub tool_calls: Vec<ToolCall>,
1529 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1531 pub tool_summaries: Vec<ToolCallSummary>,
1532 #[serde(default, skip_serializing_if = "Option::is_none")]
1534 pub headline: Option<String>,
1535}
1536
1537impl ToolCallRequestedData {
1538 pub fn with_definitions(
1539 tool_calls: &[ToolCall],
1540 tool_defs: &[crate::tool_types::ToolDefinition],
1541 ) -> Self {
1542 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
1543 }
1544
1545 pub fn with_definitions_and_locale(
1546 tool_calls: &[ToolCall],
1547 tool_defs: &[crate::tool_types::ToolDefinition],
1548 locale: Option<&str>,
1549 ) -> Self {
1550 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
1551 tool_defs.iter().map(|d| (d.name(), d)).collect();
1552
1553 let tool_summaries = tool_calls
1554 .iter()
1555 .map(|tool_call| {
1556 let tool_def = def_map.get(tool_call.name.as_str()).copied();
1557 ToolCallSummary {
1558 id: tool_call.id.clone(),
1559 name: tool_call.name.clone(),
1560 display_name: localized_tool_display_name(
1561 &tool_call.name,
1562 tool_def.and_then(|def| def.display_name()),
1563 locale,
1564 ),
1565 narration: Some(render_tool_narration_with_locale(
1566 tool_def,
1567 tool_call,
1568 ToolNarrationPhase::Waiting,
1569 locale,
1570 )),
1571 }
1572 })
1573 .collect();
1574
1575 Self {
1576 tool_calls: tool_calls.to_vec(),
1577 tool_summaries,
1578 headline: render_group_headline_with_locale(
1579 tool_calls,
1580 tool_defs,
1581 ToolNarrationPhase::Waiting,
1582 locale,
1583 ),
1584 }
1585 }
1586}
1587
1588#[derive(Debug, Clone, Serialize, Deserialize)]
1594#[cfg_attr(feature = "openapi", derive(ToSchema))]
1595pub struct LlmGenerationOutput {
1596 #[serde(skip_serializing_if = "Option::is_none")]
1598 pub text: Option<String>,
1599
1600 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1602 pub tool_calls: Vec<ToolCall>,
1603}
1604
1605#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1610#[cfg_attr(feature = "openapi", derive(ToSchema))]
1611pub struct LlmRequestOptions {
1612 #[serde(skip_serializing_if = "Option::is_none")]
1614 pub prompt_cache: Option<LlmPromptCacheInfo>,
1615 #[serde(skip_serializing_if = "Option::is_none")]
1617 pub tool_search: Option<LlmToolSearchInfo>,
1618 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1620 pub provider_options: HashMap<String, Value>,
1621 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1624 pub metadata: HashMap<String, String>,
1625}
1626
1627impl LlmRequestOptions {
1628 pub fn is_empty(&self) -> bool {
1629 self.prompt_cache.is_none()
1630 && self.tool_search.is_none()
1631 && self.provider_options.is_empty()
1632 && self.metadata.is_empty()
1633 }
1634}
1635
1636#[derive(Debug, Clone, Serialize, Deserialize)]
1638#[cfg_attr(feature = "openapi", derive(ToSchema))]
1639pub struct LlmPromptCacheInfo {
1640 pub enabled: bool,
1642 pub strategy: crate::driver_registry::PromptCacheStrategy,
1644 #[serde(skip_serializing_if = "Option::is_none")]
1646 pub provider_mode: Option<String>,
1647}
1648
1649#[derive(Debug, Clone, Serialize, Deserialize)]
1651#[cfg_attr(feature = "openapi", derive(ToSchema))]
1652pub struct LlmToolSearchInfo {
1653 pub enabled: bool,
1655 pub threshold: usize,
1657}
1658
1659#[derive(Debug, Clone, Serialize, Deserialize)]
1661#[cfg_attr(feature = "openapi", derive(ToSchema))]
1662pub struct LlmGenerationMetadata {
1663 #[cfg_attr(feature = "openapi", schema(example = "claude-sonnet-4-5"))]
1665 pub model: String,
1666
1667 #[serde(skip_serializing_if = "Option::is_none")]
1669 #[cfg_attr(feature = "openapi", schema(example = "anthropic"))]
1670 pub provider: Option<String>,
1671
1672 #[serde(skip_serializing_if = "Option::is_none")]
1674 pub usage: Option<TokenUsage>,
1675
1676 #[serde(skip_serializing_if = "Option::is_none")]
1678 #[cfg_attr(feature = "openapi", schema(example = 1_842u64))]
1679 pub duration_ms: Option<u64>,
1680
1681 #[serde(skip_serializing_if = "Option::is_none")]
1683 #[cfg_attr(feature = "openapi", schema(example = 312u64))]
1684 pub time_to_first_token_ms: Option<u64>,
1685
1686 #[cfg_attr(feature = "openapi", schema(example = true))]
1688 pub success: bool,
1689
1690 #[serde(skip_serializing_if = "Option::is_none")]
1692 #[cfg_attr(feature = "openapi", schema(example = "provider returned 503"))]
1693 pub error: Option<String>,
1694
1695 #[serde(skip_serializing_if = "Option::is_none")]
1698 #[cfg_attr(feature = "openapi", schema(example = json!(["tool_calls"])))]
1699 pub finish_reasons: Option<Vec<String>>,
1700
1701 #[serde(skip_serializing_if = "Option::is_none")]
1704 #[cfg_attr(feature = "openapi", schema(example = "msg_01ABCDef0123456789"))]
1705 pub response_id: Option<String>,
1706
1707 #[serde(skip_serializing_if = "Option::is_none")]
1710 pub retry: Option<LlmRetryInfo>,
1711
1712 #[serde(skip_serializing_if = "Option::is_none")]
1715 pub compaction: Option<LlmCompactionInfo>,
1716
1717 #[serde(skip_serializing_if = "Option::is_none")]
1719 pub request_options: Option<LlmRequestOptions>,
1720}
1721
1722#[derive(Debug, Clone, Serialize, Deserialize)]
1724#[cfg_attr(feature = "openapi", derive(ToSchema))]
1725pub struct LlmRetryInfo {
1726 pub attempts: u32,
1728
1729 pub total_wait_ms: u64,
1731}
1732
1733#[derive(Debug, Clone, Serialize, Deserialize)]
1738#[cfg_attr(feature = "openapi", derive(ToSchema))]
1739pub struct LlmCompactionInfo {
1740 pub compacted: bool,
1742
1743 #[serde(skip_serializing_if = "Option::is_none")]
1745 pub input_tokens_before: Option<u32>,
1746
1747 #[serde(skip_serializing_if = "Option::is_none")]
1749 pub input_tokens_after: Option<u32>,
1750
1751 #[serde(skip_serializing_if = "Option::is_none")]
1753 pub duration_ms: Option<u64>,
1754}
1755
1756impl LlmCompactionInfo {
1757 pub fn new(
1759 input_tokens_before: Option<u32>,
1760 input_tokens_after: Option<u32>,
1761 duration_ms: Option<u64>,
1762 ) -> Self {
1763 Self {
1764 compacted: true,
1765 input_tokens_before,
1766 input_tokens_after,
1767 duration_ms,
1768 }
1769 }
1770}
1771
1772#[derive(Debug, Clone, Serialize, Deserialize)]
1777#[cfg_attr(feature = "openapi", derive(ToSchema))]
1778pub struct LlmGenerationData {
1779 pub messages: Vec<Message>,
1781
1782 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1784 pub tools: Vec<ToolDefinitionSummary>,
1785
1786 pub output: LlmGenerationOutput,
1788
1789 pub metadata: LlmGenerationMetadata,
1791}
1792
1793impl LlmGenerationData {
1794 #[allow(clippy::too_many_arguments)]
1796 pub fn success(
1797 messages: Vec<Message>,
1798 tools: Vec<ToolDefinitionSummary>,
1799 text: Option<String>,
1800 tool_calls: Vec<ToolCall>,
1801 model: String,
1802 provider: Option<String>,
1803 usage: Option<TokenUsage>,
1804 duration_ms: Option<u64>,
1805 time_to_first_token_ms: Option<u64>,
1806 ) -> Self {
1807 let finish_reasons = if !tool_calls.is_empty() {
1809 Some(vec!["tool_calls".to_string()])
1810 } else {
1811 Some(vec!["stop".to_string()])
1812 };
1813
1814 Self {
1815 messages,
1816 tools,
1817 output: LlmGenerationOutput { text, tool_calls },
1818 metadata: LlmGenerationMetadata {
1819 model,
1820 provider,
1821 usage,
1822 duration_ms,
1823 time_to_first_token_ms,
1824 success: true,
1825 error: None,
1826 finish_reasons,
1827 response_id: None,
1828 retry: None,
1829 compaction: None,
1830 request_options: None,
1831 },
1832 }
1833 }
1834
1835 #[allow(clippy::too_many_arguments)]
1837 pub fn success_with_metadata(
1838 messages: Vec<Message>,
1839 tools: Vec<ToolDefinitionSummary>,
1840 text: Option<String>,
1841 tool_calls: Vec<ToolCall>,
1842 model: String,
1843 provider: Option<String>,
1844 usage: Option<TokenUsage>,
1845 duration_ms: Option<u64>,
1846 time_to_first_token_ms: Option<u64>,
1847 finish_reasons: Option<Vec<String>>,
1848 response_id: Option<String>,
1849 ) -> Self {
1850 Self {
1851 messages,
1852 tools,
1853 output: LlmGenerationOutput { text, tool_calls },
1854 metadata: LlmGenerationMetadata {
1855 model,
1856 provider,
1857 usage,
1858 duration_ms,
1859 time_to_first_token_ms,
1860 success: true,
1861 error: None,
1862 finish_reasons,
1863 response_id,
1864 retry: None,
1865 compaction: None,
1866 request_options: None,
1867 },
1868 }
1869 }
1870
1871 #[allow(clippy::too_many_arguments)]
1873 pub fn success_with_retry(
1874 messages: Vec<Message>,
1875 tools: Vec<ToolDefinitionSummary>,
1876 text: Option<String>,
1877 tool_calls: Vec<ToolCall>,
1878 model: String,
1879 provider: Option<String>,
1880 usage: Option<TokenUsage>,
1881 duration_ms: Option<u64>,
1882 time_to_first_token_ms: Option<u64>,
1883 finish_reasons: Option<Vec<String>>,
1884 response_id: Option<String>,
1885 retry: Option<LlmRetryInfo>,
1886 ) -> Self {
1887 Self {
1888 messages,
1889 tools,
1890 output: LlmGenerationOutput { text, tool_calls },
1891 metadata: LlmGenerationMetadata {
1892 model,
1893 provider,
1894 usage,
1895 duration_ms,
1896 time_to_first_token_ms,
1897 success: true,
1898 error: None,
1899 finish_reasons,
1900 response_id,
1901 retry,
1902 compaction: None,
1903 request_options: None,
1904 },
1905 }
1906 }
1907
1908 pub fn failure(
1910 messages: Vec<Message>,
1911 tools: Vec<ToolDefinitionSummary>,
1912 model: String,
1913 provider: Option<String>,
1914 error: String,
1915 duration_ms: Option<u64>,
1916 time_to_first_token_ms: Option<u64>,
1917 ) -> Self {
1918 Self {
1919 messages,
1920 tools,
1921 output: LlmGenerationOutput {
1922 text: None,
1923 tool_calls: vec![],
1924 },
1925 metadata: LlmGenerationMetadata {
1926 model,
1927 provider,
1928 usage: None,
1929 duration_ms,
1930 time_to_first_token_ms,
1931 success: false,
1932 error: Some(error),
1933 finish_reasons: Some(vec!["error".to_string()]),
1934 response_id: None,
1935 retry: None,
1936 compaction: None,
1937 request_options: None,
1938 },
1939 }
1940 }
1941
1942 pub fn with_compaction(mut self, compaction: LlmCompactionInfo) -> Self {
1946 self.metadata.compaction = Some(compaction);
1947 self
1948 }
1949
1950 pub fn with_retry(mut self, retry: LlmRetryInfo) -> Self {
1952 self.metadata.retry = Some(retry);
1953 self
1954 }
1955
1956 pub fn with_request_options(mut self, request_options: LlmRequestOptions) -> Self {
1958 if !request_options.is_empty() {
1959 self.metadata.request_options = Some(request_options);
1960 }
1961 self
1962 }
1963}
1964
1965#[derive(Debug, Clone, Serialize, Deserialize)]
1975#[cfg_attr(feature = "openapi", derive(ToSchema))]
1976pub struct ReasonThinkingStartedData {
1977 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1979 pub turn_id: TurnId,
1980
1981 #[serde(skip_serializing_if = "Option::is_none")]
1983 pub model: Option<String>,
1984}
1985
1986#[derive(Debug, Clone, Serialize, Deserialize)]
1992#[cfg_attr(feature = "openapi", derive(ToSchema))]
1993pub struct ReasonThinkingDeltaData {
1994 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1996 pub turn_id: TurnId,
1997
1998 pub delta: String,
2000
2001 pub accumulated: String,
2003}
2004
2005#[derive(Debug, Clone, Serialize, Deserialize)]
2010#[cfg_attr(feature = "openapi", derive(ToSchema))]
2011pub struct ReasonThinkingCompletedData {
2012 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2014 pub turn_id: TurnId,
2015
2016 pub thinking: String,
2018}
2019
2020#[derive(Debug, Clone, Serialize, Deserialize)]
2028#[cfg_attr(feature = "openapi", derive(ToSchema))]
2029pub struct ReasonItemData {
2030 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2032 pub turn_id: TurnId,
2033
2034 pub provider: String,
2036
2037 #[serde(skip_serializing_if = "Option::is_none")]
2039 pub model: Option<String>,
2040
2041 pub item_id: String,
2043
2044 #[serde(skip_serializing_if = "Option::is_none")]
2046 pub encrypted_content: Option<String>,
2047
2048 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2051 pub summary: Vec<String>,
2052
2053 #[serde(skip_serializing_if = "Option::is_none")]
2055 pub token_count: Option<u32>,
2056}
2057
2058#[derive(Debug, Clone, Serialize, Deserialize)]
2064#[cfg_attr(feature = "openapi", derive(ToSchema))]
2065pub struct TurnStartedData {
2066 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2068 pub turn_id: TurnId,
2069
2070 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
2072 pub input_message_id: MessageId,
2073
2074 #[serde(skip_serializing_if = "Option::is_none")]
2076 pub input_content: Option<String>,
2077}
2078
2079#[derive(Debug, Clone, Serialize, Deserialize)]
2081#[cfg_attr(feature = "openapi", derive(ToSchema))]
2082pub struct TurnCompletedData {
2083 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2085 pub turn_id: TurnId,
2086
2087 pub iterations: u32,
2089
2090 #[serde(skip_serializing_if = "Option::is_none")]
2092 pub duration_ms: Option<u64>,
2093
2094 #[serde(skip_serializing_if = "Option::is_none")]
2096 pub usage: Option<TokenUsage>,
2097
2098 #[serde(skip_serializing_if = "Option::is_none")]
2100 pub input_content: Option<String>,
2101
2102 #[serde(skip_serializing_if = "Option::is_none")]
2104 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "message_01933b5a00007000800000000000001"))]
2105 pub final_message_id: Option<MessageId>,
2106
2107 #[serde(skip_serializing_if = "Option::is_none")]
2109 pub final_answer_preview: Option<String>,
2110
2111 #[serde(skip_serializing_if = "Option::is_none")]
2113 pub time_to_first_token_ms: Option<u64>,
2114
2115 #[serde(skip_serializing_if = "Option::is_none")]
2117 pub tool_call_count: Option<u32>,
2118
2119 #[serde(skip_serializing_if = "Option::is_none")]
2121 pub llm_call_count: Option<u32>,
2122
2123 #[serde(skip_serializing_if = "Option::is_none")]
2125 pub status: Option<String>,
2126}
2127
2128#[derive(Debug, Clone, Serialize, Deserialize)]
2130#[cfg_attr(feature = "openapi", derive(ToSchema))]
2131pub struct TurnFailedData {
2132 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2134 pub turn_id: TurnId,
2135
2136 pub error: String,
2138
2139 #[serde(default, skip_serializing_if = "Option::is_none")]
2141 pub error_code: Option<String>,
2142
2143 #[serde(default, skip_serializing_if = "Option::is_none")]
2145 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
2146 pub error_fields: Option<UserFacingErrorFields>,
2147
2148 #[serde(default, skip_serializing_if = "Option::is_none")]
2152 pub error_disclosure: Option<String>,
2153}
2154
2155#[derive(Debug, Clone, Serialize, Deserialize)]
2162#[cfg_attr(feature = "openapi", derive(ToSchema))]
2163pub struct TurnSealedData {
2164 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2166 pub turn_id: TurnId,
2167
2168 pub reason: String,
2171
2172 #[serde(default, skip_serializing_if = "Option::is_none")]
2174 pub detail: Option<String>,
2175
2176 #[serde(default, skip_serializing_if = "Option::is_none")]
2178 pub iterations: Option<u32>,
2179
2180 #[serde(default, skip_serializing_if = "Option::is_none")]
2182 pub usage: Option<TokenUsage>,
2183}
2184
2185#[derive(Debug, Clone, Serialize, Deserialize)]
2187#[cfg_attr(feature = "openapi", derive(ToSchema))]
2188pub struct TurnCancelledData {
2189 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2191 pub turn_id: TurnId,
2192
2193 #[serde(skip_serializing_if = "Option::is_none")]
2195 pub reason: Option<String>,
2196
2197 #[serde(skip_serializing_if = "Option::is_none")]
2199 pub usage: Option<TokenUsage>,
2200}
2201
2202#[derive(Debug, Clone, Serialize, Deserialize)]
2208#[cfg_attr(feature = "openapi", derive(ToSchema))]
2209pub struct SessionStartedData {
2210 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "harness_01933b5a00007000800000000000001"))]
2212 pub harness_id: HarnessId,
2213
2214 #[serde(skip_serializing_if = "Option::is_none")]
2216 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agent_01933b5a00007000800000000000001"))]
2217 pub agent_id: Option<AgentId>,
2218
2219 #[serde(skip_serializing_if = "Option::is_none")]
2221 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
2222 pub model_id: Option<ModelId>,
2223}
2224
2225#[derive(Debug, Clone, Serialize, Deserialize)]
2227#[cfg_attr(feature = "openapi", derive(ToSchema))]
2228pub struct SessionActivatedData {
2229 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2231 pub turn_id: TurnId,
2232
2233 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
2235 pub input_message_id: MessageId,
2236}
2237
2238#[derive(Debug, Clone, Serialize, Deserialize)]
2240#[cfg_attr(feature = "openapi", derive(ToSchema))]
2241pub struct SessionIdledData {
2242 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2244 pub turn_id: TurnId,
2245
2246 #[serde(skip_serializing_if = "Option::is_none")]
2248 pub iterations: Option<u32>,
2249
2250 #[serde(skip_serializing_if = "Option::is_none")]
2252 pub usage: Option<TokenUsage>,
2253}
2254
2255#[derive(Debug, Clone, Serialize, Deserialize)]
2257#[cfg_attr(feature = "openapi", derive(ToSchema))]
2258pub struct SessionTitleUpdatedData {
2259 pub previous_title: Option<String>,
2261
2262 pub title: String,
2264}
2265
2266#[derive(Debug, Clone, Serialize, Deserialize)]
2275#[cfg_attr(feature = "openapi", derive(ToSchema))]
2276pub struct SessionTaskEventData {
2277 pub task: crate::session_task::SessionTask,
2278}
2279
2280#[derive(Debug, Clone, Serialize, Deserialize)]
2282#[cfg_attr(feature = "openapi", derive(ToSchema))]
2283pub struct TaskMessageEventData {
2284 pub task_id: String,
2285 pub message: crate::session_task::TaskMessage,
2286}
2287
2288#[derive(Debug, Clone, Serialize, Deserialize)]
2294#[cfg_attr(feature = "openapi", derive(ToSchema))]
2295#[serde(rename_all = "snake_case")]
2296pub enum CompactionReason {
2297 ProactiveBudget,
2299 RequestTooLarge,
2301 Manual,
2303}
2304
2305impl std::fmt::Display for CompactionReason {
2306 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2307 match self {
2308 Self::ProactiveBudget => write!(f, "proactive_budget"),
2309 Self::RequestTooLarge => write!(f, "request_too_large"),
2310 Self::Manual => write!(f, "manual"),
2311 }
2312 }
2313}
2314
2315#[derive(Debug, Clone, Serialize, Deserialize)]
2317#[cfg_attr(feature = "openapi", derive(ToSchema))]
2318pub struct ContextCompactingData {
2319 pub reason: CompactionReason,
2321 pub strategy: String,
2323 pub messages_before: usize,
2325 #[serde(default, skip_serializing_if = "Option::is_none")]
2327 pub tokens_before: Option<u64>,
2328 #[serde(default, skip_serializing_if = "Option::is_none")]
2330 pub bytes_before: Option<u64>,
2331}
2332
2333#[derive(Debug, Clone, Serialize, Deserialize)]
2335#[cfg_attr(feature = "openapi", derive(ToSchema))]
2336pub struct CompactionStepData {
2337 pub strategy: String,
2339 pub messages_after: usize,
2341 pub duration_ms: u64,
2343}
2344
2345#[derive(Debug, Clone, Serialize, Deserialize)]
2347#[cfg_attr(feature = "openapi", derive(ToSchema))]
2348pub struct ContextCompactedData {
2349 #[serde(default, skip_serializing_if = "Option::is_none")]
2351 pub checkpoint_id: Option<String>,
2352 pub strategy_used: String,
2354 pub messages_before: usize,
2356 pub messages_after: usize,
2358 #[serde(default, skip_serializing_if = "Option::is_none")]
2360 pub tokens_before: Option<u64>,
2361 #[serde(default, skip_serializing_if = "Option::is_none")]
2363 pub tokens_after: Option<u64>,
2364 #[serde(default, skip_serializing_if = "Option::is_none")]
2366 pub bytes_before: Option<u64>,
2367 #[serde(default, skip_serializing_if = "Option::is_none")]
2369 pub bytes_after: Option<u64>,
2370 pub duration_ms: u64,
2372 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2374 pub steps: Vec<CompactionStepData>,
2375}
2376
2377#[derive(Debug, Clone, Serialize, Deserialize)]
2383#[cfg_attr(feature = "openapi", derive(ToSchema))]
2384pub struct FileWrittenData {
2385 pub path: String,
2387 pub operation: String,
2389 pub size_bytes: i64,
2391 pub created: bool,
2393}
2394
2395pub const FILE_OP_CREATE: &str = "create";
2397pub const FILE_OP_UPDATE: &str = "update";
2398
2399#[derive(Debug, Clone, Serialize, Deserialize)]
2405#[cfg_attr(feature = "openapi", derive(ToSchema))]
2406pub struct BudgetEventData {
2407 pub budget_id: String,
2409 pub balance: f64,
2411 pub limit: f64,
2413 pub currency: String,
2415 #[serde(skip_serializing_if = "Option::is_none")]
2417 pub message: Option<String>,
2418 #[serde(skip_serializing_if = "Option::is_none")]
2420 pub soft_limit: Option<f64>,
2421}
2422
2423#[derive(Debug, Clone, Serialize, Deserialize)]
2429#[cfg_attr(feature = "openapi", derive(ToSchema))]
2430pub struct VoiceSessionStartedData {
2431 #[cfg_attr(
2433 feature = "openapi",
2434 schema(example = "voice_01933b5a00007000800000000000001")
2435 )]
2436 pub voice_connection_id: String,
2437 #[cfg_attr(feature = "openapi", schema(example = "gpt-realtime"))]
2439 pub model: String,
2440 #[cfg_attr(feature = "openapi", schema(example = "alloy"))]
2442 pub voice: String,
2443 #[cfg_attr(feature = "openapi", schema(example = "medium"))]
2445 pub reasoning_effort: String,
2446 #[cfg_attr(feature = "openapi", schema(example = "webrtc"))]
2448 pub transport: String,
2449}
2450
2451#[derive(Debug, Clone, Serialize, Deserialize)]
2453#[cfg_attr(feature = "openapi", derive(ToSchema))]
2454pub struct VoiceTranscriptData {
2455 pub voice_connection_id: String,
2457 #[serde(default, skip_serializing_if = "Option::is_none")]
2459 pub item_id: Option<String>,
2460 #[serde(default, skip_serializing_if = "Option::is_none")]
2462 pub response_id: Option<String>,
2463 #[serde(default, skip_serializing_if = "Option::is_none")]
2465 pub phase: Option<String>,
2466 #[serde(default, skip_serializing_if = "String::is_empty")]
2468 pub delta: String,
2469 pub accumulated: String,
2471}
2472
2473#[derive(Debug, Clone, Serialize, Deserialize)]
2475#[cfg_attr(feature = "openapi", derive(ToSchema))]
2476pub struct VoiceSessionEndedData {
2477 #[cfg_attr(
2479 feature = "openapi",
2480 schema(example = "voice_01933b5a00007000800000000000001")
2481 )]
2482 pub voice_connection_id: String,
2483 #[serde(default, skip_serializing_if = "Option::is_none")]
2485 #[cfg_attr(
2486 feature = "openapi",
2487 schema(example = "User hung up after refund confirmed.")
2488 )]
2489 pub reason: Option<String>,
2490 #[serde(default, skip_serializing_if = "Option::is_none")]
2493 #[cfg_attr(feature = "openapi", schema(example = 184_500_u64))]
2494 pub duration_ms: Option<u64>,
2495}
2496
2497#[derive(Debug, Clone, Serialize, Deserialize)]
2499#[cfg_attr(feature = "openapi", derive(ToSchema))]
2500pub struct VoiceSessionFailedData {
2501 #[cfg_attr(
2503 feature = "openapi",
2504 schema(example = "voice_01933b5a00007000800000000000001")
2505 )]
2506 pub voice_connection_id: String,
2507 #[cfg_attr(
2509 feature = "openapi",
2510 schema(example = "realtime provider closed stream: 1011 internal_error")
2511 )]
2512 pub error: String,
2513}
2514
2515#[derive(Debug, Clone, Serialize)]
2563#[serde(untagged)]
2564#[cfg_attr(feature = "openapi", derive(ToSchema))]
2565#[cfg_attr(feature = "openapi", schema(
2566 title = "EventData",
2567 description = "Event-specific payload. The schema depends on the event type field.",
2568 example = json!({"message": {"id": "...", "role": "user", "content": []}})
2569))]
2570pub enum EventData {
2571 InputMessage(InputMessageData),
2573
2574 OutputMessageDelta(OutputMessageDeltaData),
2576 OutputMessageStarted(OutputMessageStartedData),
2577 OutputMessageReplaced(OutputMessageReplacedData),
2578 OutputMessageCompleted(OutputMessageCompletedData),
2579
2580 TurnStarted(TurnStartedData),
2582 TurnCompleted(TurnCompletedData),
2583 TurnFailed(TurnFailedData),
2584
2585 ReasonStarted(ReasonStartedData),
2587 ReasonCompleted(ReasonCompletedData),
2588 ReasonRecovered(ReasonRecoveredData),
2589 CapabilityUsage(CapabilityUsageData),
2590 ActStarted(ActStartedData),
2591 ActCompleted(ActCompletedData),
2592 ToolStarted(ToolStartedData),
2593 ToolCompleted(ToolCompletedData),
2594 ToolProgress(ToolProgressData),
2595 ToolOutputDelta(ToolOutputDeltaData),
2596 ToolCallRequested(ToolCallRequestedData),
2597
2598 TranscriptRepaired(TranscriptRepairedData),
2600 ToolCallRepaired(ToolCallRepairedData),
2601
2602 LlmGeneration(LlmGenerationData),
2604
2605 ReasonThinkingDelta(ReasonThinkingDeltaData),
2607 ReasonItem(ReasonItemData),
2608 ReasonThinkingStarted(ReasonThinkingStartedData),
2609 ReasonThinkingCompleted(ReasonThinkingCompletedData),
2610
2611 TurnSealed(TurnSealedData),
2612 TurnCancelled(TurnCancelledData),
2613
2614 SessionStarted(SessionStartedData),
2616 SessionActivated(SessionActivatedData),
2617 SessionIdled(SessionIdledData),
2618 SessionTitleUpdated(SessionTitleUpdatedData),
2619
2620 TaskCreated(SessionTaskEventData),
2622 TaskUpdated(SessionTaskEventData),
2623 TaskMessageSent(TaskMessageEventData),
2624 TaskMessageReceived(TaskMessageEventData),
2625
2626 ContextCompacting(ContextCompactingData),
2628 ContextCompacted(ContextCompactedData),
2629
2630 FileWritten(FileWrittenData),
2632
2633 BudgetWarning(BudgetEventData),
2635 BudgetPaused(BudgetEventData),
2636 BudgetExhausted(BudgetEventData),
2637 BudgetResumed(BudgetEventData),
2638
2639 VoiceSessionStarted(VoiceSessionStartedData),
2641 VoiceInputTranscriptDelta(VoiceTranscriptData),
2642 VoiceInputTranscriptCompleted(VoiceTranscriptData),
2643 VoiceOutputTranscriptDelta(VoiceTranscriptData),
2644 VoiceOutputTranscriptCompleted(VoiceTranscriptData),
2645 VoiceSessionEnded(VoiceSessionEndedData),
2646 VoiceSessionFailed(VoiceSessionFailedData),
2647
2648 #[serde(skip)]
2652 Unsupported {
2653 event_type: String,
2655 data: serde_json::Value,
2657 },
2658}
2659
2660impl EventData {
2661 pub fn is_unsupported(&self) -> bool {
2664 matches!(self, EventData::Unsupported { .. })
2665 }
2666
2667 pub fn unsupported(event_type: String, data: serde_json::Value) -> Self {
2670 tracing::warn!(
2671 event_type = %event_type,
2672 "Encountered unsupported event type - will be filtered from API responses"
2673 );
2674 EventData::Unsupported { event_type, data }
2675 }
2676}
2677
2678macro_rules! event_data_kinds {
2692 ($( $variant:ident($data:ty) = $type_const:path ),+ $(,)?) => {
2693 impl EventData {
2694 pub fn event_type(&self) -> &'static str {
2697 match self {
2698 $( EventData::$variant(_) => $type_const, )+
2699 EventData::Unsupported { .. } => "unsupported",
2700 }
2701 }
2702 }
2703
2704 pub fn deserialize_event_data(event_type: &str, data: serde_json::Value) -> EventData {
2716 let result = match event_type {
2717 $(
2718 $type_const => serde_json::from_value::<$data>(data.clone())
2719 .map(EventData::$variant),
2720 )+
2721 _ => return EventData::unsupported(event_type.to_string(), data),
2722 };
2723
2724 result.unwrap_or_else(|e| {
2725 tracing::warn!(
2726 event_type = %event_type,
2727 error = %e,
2728 "Failed to deserialize known event type - treating as unsupported"
2729 );
2730 EventData::Unsupported {
2731 event_type: event_type.to_string(),
2732 data,
2733 }
2734 })
2735 }
2736 };
2737}
2738
2739event_data_kinds! {
2740 InputMessage(InputMessageData) = INPUT_MESSAGE,
2742
2743 OutputMessageStarted(OutputMessageStartedData) = OUTPUT_MESSAGE_STARTED,
2745 OutputMessageDelta(OutputMessageDeltaData) = OUTPUT_MESSAGE_DELTA,
2746 OutputMessageReplaced(OutputMessageReplacedData) = OUTPUT_MESSAGE_REPLACED,
2747 OutputMessageCompleted(OutputMessageCompletedData) = OUTPUT_MESSAGE_COMPLETED,
2748
2749 TurnStarted(TurnStartedData) = TURN_STARTED,
2751 TurnCompleted(TurnCompletedData) = TURN_COMPLETED,
2752 TurnFailed(TurnFailedData) = TURN_FAILED,
2753 TurnSealed(TurnSealedData) = TURN_SEALED,
2754 TurnCancelled(TurnCancelledData) = TURN_CANCELLED,
2755
2756 ReasonStarted(ReasonStartedData) = REASON_STARTED,
2758 ReasonCompleted(ReasonCompletedData) = REASON_COMPLETED,
2759 ReasonRecovered(ReasonRecoveredData) = REASON_RECOVERED,
2760 CapabilityUsage(CapabilityUsageData) = CAPABILITY_USAGE,
2761 ActStarted(ActStartedData) = ACT_STARTED,
2762 ActCompleted(ActCompletedData) = ACT_COMPLETED,
2763 ToolStarted(ToolStartedData) = TOOL_STARTED,
2764 ToolCompleted(ToolCompletedData) = TOOL_COMPLETED,
2765 ToolProgress(ToolProgressData) = TOOL_PROGRESS,
2766 ToolOutputDelta(ToolOutputDeltaData) = TOOL_OUTPUT_DELTA,
2767 ToolCallRequested(ToolCallRequestedData) = TOOL_CALL_REQUESTED,
2768
2769 TranscriptRepaired(TranscriptRepairedData) = TRANSCRIPT_REPAIRED,
2771 ToolCallRepaired(ToolCallRepairedData) = TOOL_CALL_REPAIRED,
2772
2773 LlmGeneration(LlmGenerationData) = LLM_GENERATION,
2775
2776 ReasonThinkingStarted(ReasonThinkingStartedData) = REASON_THINKING_STARTED,
2778 ReasonThinkingDelta(ReasonThinkingDeltaData) = REASON_THINKING_DELTA,
2779 ReasonThinkingCompleted(ReasonThinkingCompletedData) = REASON_THINKING_COMPLETED,
2780 ReasonItem(ReasonItemData) = REASON_ITEM,
2781
2782 SessionStarted(SessionStartedData) = SESSION_STARTED,
2784 SessionActivated(SessionActivatedData) = SESSION_ACTIVATED,
2785 SessionIdled(SessionIdledData) = SESSION_IDLED,
2786 SessionTitleUpdated(SessionTitleUpdatedData) = SESSION_TITLE_UPDATED,
2787
2788 ContextCompacting(ContextCompactingData) = CONTEXT_COMPACTING,
2790 ContextCompacted(ContextCompactedData) = CONTEXT_COMPACTED,
2791
2792 FileWritten(FileWrittenData) = FILE_WRITTEN,
2794
2795 BudgetWarning(BudgetEventData) = BUDGET_WARNING,
2797 BudgetPaused(BudgetEventData) = BUDGET_PAUSED,
2798 BudgetExhausted(BudgetEventData) = BUDGET_EXHAUSTED,
2799 BudgetResumed(BudgetEventData) = BUDGET_RESUMED,
2800
2801 VoiceSessionStarted(VoiceSessionStartedData) = VOICE_SESSION_STARTED,
2803 VoiceInputTranscriptDelta(VoiceTranscriptData) = VOICE_INPUT_TRANSCRIPT_DELTA,
2804 VoiceInputTranscriptCompleted(VoiceTranscriptData) = VOICE_INPUT_TRANSCRIPT_COMPLETED,
2805 VoiceOutputTranscriptDelta(VoiceTranscriptData) = VOICE_OUTPUT_TRANSCRIPT_DELTA,
2806 VoiceOutputTranscriptCompleted(VoiceTranscriptData) = VOICE_OUTPUT_TRANSCRIPT_COMPLETED,
2807 VoiceSessionEnded(VoiceSessionEndedData) = VOICE_SESSION_ENDED,
2808 VoiceSessionFailed(VoiceSessionFailedData) = VOICE_SESSION_FAILED,
2809
2810 TaskCreated(SessionTaskEventData) = TASK_CREATED,
2812 TaskUpdated(SessionTaskEventData) = TASK_UPDATED,
2813 TaskMessageSent(TaskMessageEventData) = TASK_MESSAGE_SENT,
2814 TaskMessageReceived(TaskMessageEventData) = TASK_MESSAGE_RECEIVED,
2815}
2816
2817macro_rules! impl_from_event_data {
2821 ($($data_type:ty => $variant:ident),* $(,)?) => {
2822 $(
2823 impl From<$data_type> for EventData {
2824 fn from(data: $data_type) -> Self {
2825 EventData::$variant(data)
2826 }
2827 }
2828 )*
2829 };
2830}
2831
2832impl_from_event_data! {
2834 InputMessageData => InputMessage,
2835 OutputMessageStartedData => OutputMessageStarted,
2836 OutputMessageDeltaData => OutputMessageDelta,
2837 OutputMessageReplacedData => OutputMessageReplaced,
2838 OutputMessageCompletedData => OutputMessageCompleted,
2839 TurnStartedData => TurnStarted,
2840 TurnCompletedData => TurnCompleted,
2841 TurnFailedData => TurnFailed,
2842 TurnSealedData => TurnSealed,
2843 TurnCancelledData => TurnCancelled,
2844 ReasonStartedData => ReasonStarted,
2845 ReasonCompletedData => ReasonCompleted,
2846 ReasonRecoveredData => ReasonRecovered,
2847 CapabilityUsageData => CapabilityUsage,
2848 ActStartedData => ActStarted,
2849 ActCompletedData => ActCompleted,
2850 ToolStartedData => ToolStarted,
2851 ToolCompletedData => ToolCompleted,
2852 ToolProgressData => ToolProgress,
2853 ToolOutputDeltaData => ToolOutputDelta,
2854 ToolCallRequestedData => ToolCallRequested,
2855 TranscriptRepairedData => TranscriptRepaired,
2856 ToolCallRepairedData => ToolCallRepaired,
2857 LlmGenerationData => LlmGeneration,
2858 ReasonThinkingStartedData => ReasonThinkingStarted,
2859 ReasonThinkingDeltaData => ReasonThinkingDelta,
2860 ReasonThinkingCompletedData => ReasonThinkingCompleted,
2861 ReasonItemData => ReasonItem,
2862 SessionStartedData => SessionStarted,
2863 SessionActivatedData => SessionActivated,
2864 SessionIdledData => SessionIdled,
2865 SessionTitleUpdatedData => SessionTitleUpdated,
2866 ContextCompactingData => ContextCompacting,
2867 ContextCompactedData => ContextCompacted,
2868 FileWrittenData => FileWritten,
2869 VoiceSessionStartedData => VoiceSessionStarted,
2870 VoiceSessionEndedData => VoiceSessionEnded,
2871 VoiceSessionFailedData => VoiceSessionFailed,
2872}
2873
2874impl EventData {
2875 pub fn voice_transcript_event(data: VoiceTranscriptData, event_type: &str) -> Self {
2876 match event_type {
2877 VOICE_INPUT_TRANSCRIPT_DELTA => EventData::VoiceInputTranscriptDelta(data),
2878 VOICE_INPUT_TRANSCRIPT_COMPLETED => EventData::VoiceInputTranscriptCompleted(data),
2879 VOICE_OUTPUT_TRANSCRIPT_DELTA => EventData::VoiceOutputTranscriptDelta(data),
2880 VOICE_OUTPUT_TRANSCRIPT_COMPLETED => EventData::VoiceOutputTranscriptCompleted(data),
2881 _ => EventData::unsupported(
2882 event_type.to_string(),
2883 serde_json::to_value(&data).unwrap_or(serde_json::Value::Null),
2884 ),
2885 }
2886 }
2887}
2888
2889impl EventData {
2892 pub fn budget_event(data: BudgetEventData, event_type: &str) -> Self {
2893 match event_type {
2894 BUDGET_WARNING => EventData::BudgetWarning(data),
2895 BUDGET_PAUSED => EventData::BudgetPaused(data),
2896 BUDGET_EXHAUSTED => EventData::BudgetExhausted(data),
2897 BUDGET_RESUMED => EventData::BudgetResumed(data),
2898 _ => EventData::unsupported(
2899 event_type.to_string(),
2900 serde_json::to_value(&data).unwrap_or(serde_json::Value::Null),
2901 ),
2902 }
2903 }
2904}
2905
2906#[derive(Debug, Clone, Serialize)]
2916#[cfg_attr(feature = "openapi", derive(ToSchema))]
2917pub struct EventRequest {
2918 #[serde(rename = "type")]
2920 pub event_type: String,
2921
2922 pub ts: DateTime<Utc>,
2924
2925 pub session_id: SessionId,
2927
2928 pub context: EventContext,
2930
2931 pub data: EventData,
2933
2934 #[serde(skip_serializing_if = "Option::is_none")]
2936 pub metadata: Option<serde_json::Value>,
2937
2938 #[serde(skip_serializing_if = "Option::is_none")]
2940 pub tags: Option<Vec<String>>,
2941}
2942
2943#[derive(Debug, Deserialize)]
2944struct RawEventRequest {
2945 #[serde(rename = "type")]
2946 event_type: String,
2947 ts: DateTime<Utc>,
2948 session_id: SessionId,
2949 context: EventContext,
2950 data: serde_json::Value,
2951 metadata: Option<serde_json::Value>,
2952 tags: Option<Vec<String>>,
2953}
2954
2955impl<'de> Deserialize<'de> for EventRequest {
2956 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2957 where
2958 D: Deserializer<'de>,
2959 {
2960 let raw = RawEventRequest::deserialize(deserializer)?;
2961 let data = deserialize_event_data(&raw.event_type, raw.data);
2962 Ok(Self {
2963 event_type: raw.event_type,
2964 ts: raw.ts,
2965 session_id: raw.session_id,
2966 context: raw.context,
2967 data,
2968 metadata: raw.metadata,
2969 tags: raw.tags,
2970 })
2971 }
2972}
2973
2974impl EventRequest {
2975 pub fn new(session_id: SessionId, context: EventContext, data: impl Into<EventData>) -> Self {
2979 let data = data.into();
2980 let event_type = data.event_type().to_string();
2981 Self {
2982 event_type,
2983 ts: Utc::now(),
2984 session_id,
2985 context,
2986 data,
2987 metadata: None,
2988 tags: None,
2989 }
2990 }
2991
2992 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
2994 self.metadata = Some(metadata);
2995 self
2996 }
2997
2998 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
3000 self.tags = Some(tags);
3001 self
3002 }
3003
3004 pub fn is_ephemeral(&self) -> bool {
3012 is_ephemeral_event_type(&self.event_type)
3013 }
3014
3015 pub fn into_event(self, id: EventId, sequence: i32) -> Event {
3017 Event {
3018 id,
3019 event_type: self.event_type,
3020 ts: self.ts,
3021 session_id: self.session_id,
3022 context: self.context,
3023 data: self.data,
3024 metadata: self.metadata,
3025 tags: self.tags,
3026 sequence: Some(sequence),
3027 }
3028 }
3029}
3030
3031pub struct EventBuilder {
3037 session_id: SessionId,
3038 context: EventContext,
3039}
3040
3041impl EventBuilder {
3042 pub fn new(session_id: SessionId) -> Self {
3043 Self {
3044 session_id,
3045 context: EventContext::empty(),
3046 }
3047 }
3048
3049 pub fn with_turn(mut self, turn_id: TurnId, input_message_id: MessageId) -> Self {
3050 self.context.turn_id = Some(turn_id);
3051 self.context.input_message_id = Some(input_message_id);
3052 self
3053 }
3054
3055 pub fn with_exec(mut self, exec_id: ExecId) -> Self {
3056 self.context.exec_id = Some(exec_id);
3057 self
3058 }
3059
3060 pub fn build(self, data: impl Into<EventData>) -> Event {
3061 Event::new(self.session_id, self.context, data)
3062 }
3063}
3064
3065#[cfg(test)]
3070mod tests {
3071 use super::*;
3072 use crate::driver_registry::PromptCacheStrategy;
3073 use serde_json::json;
3074 use std::collections::HashMap;
3075
3076 #[test]
3077 fn test_event_creation() {
3078 let session_id = SessionId::new();
3079 let context = EventContext::empty();
3080 let data = InputMessageData::new(Message::user("test"));
3081
3082 let event = Event::new(session_id, context, data);
3083
3084 assert_eq!(event.event_type, "input.message");
3085 assert_eq!(event.session_uuid(), session_id.uuid());
3086 assert!(event.is_input_event());
3087 assert!(event.is_message_event());
3088 }
3089
3090 #[test]
3091 fn test_event_context_from_atom_context() {
3092 let session_id = SessionId::new();
3093 let turn_id = TurnId::new();
3094 let input_message_id = MessageId::new();
3095
3096 let atom_ctx = AtomContext::new(session_id, turn_id, input_message_id);
3097 let context = EventContext::from_atom_context(&atom_ctx);
3098
3099 assert_eq!(context.turn_id, Some(turn_id));
3100 assert_eq!(context.input_message_id, Some(input_message_id));
3101 assert_eq!(context.exec_id, Some(atom_ctx.exec_id));
3102 }
3103
3104 #[test]
3105 fn test_event_serialization() {
3106 let session_id = SessionId::new();
3107 let context = EventContext::empty();
3108 let event = Event::new(
3109 session_id,
3110 context,
3111 InputMessageData::new(Message::user("test")),
3112 );
3113
3114 let json = serde_json::to_string(&event).unwrap();
3115
3116 assert!(json.contains("\"type\":\"input.message\""));
3117 assert!(json.contains("\"session_id\""));
3118 assert!(json.contains("\"context\""));
3119 assert!(json.contains("\"data\""));
3120 }
3121
3122 #[test]
3123 fn transcript_repaired_is_valid_filter_event_type() {
3124 assert!(VALID_EVENT_TYPES.contains(&TRANSCRIPT_REPAIRED));
3125 }
3126
3127 #[test]
3131 fn capability_usage_is_valid_filter_event_type() {
3132 assert!(VALID_EVENT_TYPES.contains(&CAPABILITY_USAGE));
3133 }
3134
3135 #[test]
3136 fn test_event_builder() {
3137 let session_id = SessionId::new();
3138 let turn_id = TurnId::new();
3139 let input_message_id = MessageId::new();
3140 let exec_id = ExecId::new();
3141
3142 let event = EventBuilder::new(session_id)
3143 .with_turn(turn_id, input_message_id)
3144 .with_exec(exec_id)
3145 .build(ReasonStartedData {
3146 harness_id: HarnessId::from_seed(1),
3147 agent_id: Some(AgentId::new()),
3148 metadata: Some(ModelMetadata {
3149 model: "gpt-4o".to_string(),
3150 model_id: None,
3151 provider_id: None,
3152 }),
3153 });
3154
3155 assert_eq!(event.event_type, "reason.started");
3156 assert_eq!(event.session_id, session_id);
3157 assert_eq!(event.context.turn_id, Some(turn_id));
3158 assert_eq!(event.context.exec_id, Some(exec_id));
3159 }
3160
3161 #[test]
3162 fn test_reason_completed_data() {
3163 let data = ReasonCompletedData::success("Hello world", true, 2, Some(1000), None);
3164 assert!(data.success);
3165 assert_eq!(data.text_preview, Some("Hello world".to_string()));
3166 assert!(data.has_tool_calls);
3167 assert_eq!(data.tool_call_count, 2);
3168 assert_eq!(data.duration_ms, Some(1000));
3169 assert!(data.usage.is_none());
3170
3171 let data = ReasonCompletedData::failure("Network error".to_string(), Some(500));
3172 assert!(!data.success);
3173 assert_eq!(data.error, Some("Network error".to_string()));
3174 assert_eq!(data.duration_ms, Some(500));
3175 }
3176
3177 #[test]
3178 fn test_input_output_event_types() {
3179 assert_eq!(INPUT_MESSAGE, "input.message");
3180 assert_eq!(OUTPUT_MESSAGE_STARTED, "output.message.started");
3181 assert_eq!(OUTPUT_MESSAGE_DELTA, "output.message.delta");
3182 assert_eq!(OUTPUT_MESSAGE_COMPLETED, "output.message.completed");
3183 }
3184
3185 #[test]
3186 fn test_turn_event_types() {
3187 assert_eq!(TURN_STARTED, "turn.started");
3188 assert_eq!(TURN_COMPLETED, "turn.completed");
3189 assert_eq!(TURN_FAILED, "turn.failed");
3190 assert_eq!(TURN_CANCELLED, "turn.cancelled");
3191 }
3192
3193 #[test]
3194 fn test_turn_cancelled_data() {
3195 let data = TurnCancelledData {
3196 turn_id: TurnId::from_uuid(Uuid::now_v7()),
3197 reason: Some("User requested cancellation".to_string()),
3198 usage: Some(TokenUsage::new(100, 50)),
3199 };
3200
3201 let event_data: EventData = data.into();
3202 assert_eq!(event_data.event_type(), TURN_CANCELLED);
3203 }
3204
3205 #[test]
3206 fn test_tool_event_types() {
3207 assert_eq!(TOOL_STARTED, "tool.started");
3208 assert_eq!(TOOL_COMPLETED, "tool.completed");
3209 }
3210
3211 #[test]
3212 fn test_llm_generation_event_type() {
3213 assert_eq!(LLM_GENERATION, "llm.generation");
3214 }
3215
3216 #[test]
3217 fn test_llm_generation_data_success() {
3218 let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")];
3219 let tools = vec![ToolDefinitionSummary {
3220 name: "get_weather".to_string(),
3221 display_name: None,
3222 category: None,
3223 capability_id: None,
3224 capability_name: None,
3225 description: "Get weather for a city".to_string(),
3226 }];
3227 let tool_calls = vec![];
3228 let data = LlmGenerationData::success(
3229 messages.clone(),
3230 tools,
3231 Some("Hi there!".to_string()),
3232 tool_calls,
3233 "gpt-4o".to_string(),
3234 Some("openai".to_string()),
3235 Some(TokenUsage {
3236 input_tokens: 10,
3237 output_tokens: 5,
3238 cache_read_tokens: None,
3239 cache_creation_tokens: None,
3240 actual_cost_usd: None,
3241 estimated_cost_usd: None,
3242 effective_cost_usd: None,
3243 }),
3244 Some(100),
3245 Some(25), );
3247
3248 assert_eq!(data.messages.len(), 2);
3249 assert_eq!(data.tools.len(), 1);
3250 assert_eq!(data.tools[0].name, "get_weather");
3251 assert_eq!(data.output.text, Some("Hi there!".to_string()));
3252 assert!(data.output.tool_calls.is_empty());
3253 assert!(data.metadata.success);
3254 assert_eq!(data.metadata.model, "gpt-4o");
3255 assert_eq!(data.metadata.provider, Some("openai".to_string()));
3256 assert!(data.metadata.error.is_none());
3257 assert_eq!(data.metadata.finish_reasons, Some(vec!["stop".to_string()]));
3259 assert!(data.metadata.response_id.is_none());
3260 }
3261
3262 #[test]
3263 fn test_llm_generation_data_with_full_metadata() {
3264 let messages = vec![Message::user("Hello")];
3265 let data = LlmGenerationData::success_with_metadata(
3266 messages,
3267 vec![],
3268 Some("Hi!".to_string()),
3269 vec![],
3270 "claude-3-opus".to_string(),
3271 Some("anthropic".to_string()),
3272 Some(TokenUsage {
3273 input_tokens: 5,
3274 output_tokens: 3,
3275 cache_read_tokens: None,
3276 cache_creation_tokens: None,
3277 actual_cost_usd: None,
3278 estimated_cost_usd: None,
3279 effective_cost_usd: None,
3280 }),
3281 Some(50),
3282 Some(25), Some(vec!["end_turn".to_string()]),
3284 Some("msg_12345".to_string()),
3285 );
3286
3287 assert!(data.metadata.success);
3288 assert_eq!(data.metadata.model, "claude-3-opus");
3289 assert_eq!(data.metadata.provider, Some("anthropic".to_string()));
3290 assert_eq!(data.metadata.time_to_first_token_ms, Some(25));
3291 assert_eq!(
3292 data.metadata.finish_reasons,
3293 Some(vec!["end_turn".to_string()])
3294 );
3295 assert_eq!(data.metadata.response_id, Some("msg_12345".to_string()));
3296 }
3297
3298 #[test]
3299 fn test_llm_generation_data_failure() {
3300 let messages = vec![Message::user("Hello")];
3301 let data = LlmGenerationData::failure(
3302 messages,
3303 vec![],
3304 "gpt-4o".to_string(),
3305 Some("openai".to_string()),
3306 "Rate limit exceeded".to_string(),
3307 Some(50),
3308 None, );
3310
3311 assert!(!data.metadata.success);
3312 assert_eq!(data.metadata.error, Some("Rate limit exceeded".to_string()));
3313 assert!(data.output.text.is_none());
3314 assert!(data.output.tool_calls.is_empty());
3315 }
3316
3317 #[test]
3318 fn test_llm_generation_event_data() {
3319 let data = LlmGenerationData::success(
3320 vec![Message::user("test")],
3321 vec![],
3322 Some("response".to_string()),
3323 vec![],
3324 "model".to_string(),
3325 None,
3326 None,
3327 None,
3328 None, );
3330
3331 let event_data: EventData = data.into();
3332 assert_eq!(event_data.event_type(), LLM_GENERATION);
3333 }
3334
3335 #[test]
3336 fn test_llm_generation_is_durable_not_ephemeral() {
3337 let session_id = SessionId::new();
3338 let data = LlmGenerationData::success(
3339 vec![Message::user("test")],
3340 vec![],
3341 Some("response".to_string()),
3342 vec![],
3343 "model".to_string(),
3344 None,
3345 None,
3346 None,
3347 None,
3348 );
3349
3350 let request = EventRequest::new(session_id, EventContext::empty(), data);
3351 assert!(!request.is_ephemeral());
3352 }
3353
3354 #[test]
3355 fn test_delta_events_are_ephemeral() {
3356 let session_id = SessionId::new();
3357 let turn_id = TurnId::new();
3358
3359 let output_delta = EventRequest::new(
3360 session_id,
3361 EventContext::empty(),
3362 OutputMessageDeltaData {
3363 turn_id,
3364 message_id: MessageId::new(),
3365 delta: "hel".to_string(),
3366 accumulated: "hel".to_string(),
3367 phase: None,
3368 },
3369 );
3370 assert!(output_delta.is_ephemeral());
3371
3372 let thinking_delta = EventRequest::new(
3373 session_id,
3374 EventContext::empty(),
3375 ReasonThinkingDeltaData {
3376 turn_id,
3377 delta: "step".to_string(),
3378 accumulated: "step".to_string(),
3379 },
3380 );
3381 assert!(thinking_delta.is_ephemeral());
3382
3383 let tool_delta = EventRequest::new(
3384 session_id,
3385 EventContext::empty(),
3386 ToolOutputDeltaData {
3387 tool_call_id: "call_123".to_string(),
3388 tool_name: "bash".to_string(),
3389 delta: "line".to_string(),
3390 stream: "stdout".to_string(),
3391 },
3392 );
3393 assert!(tool_delta.is_ephemeral());
3394 }
3395
3396 #[test]
3397 fn test_llm_generation_data_with_request_options() {
3398 let mut provider_options = HashMap::new();
3399 provider_options.insert(
3400 "openai".to_string(),
3401 json!({ "previous_response_id": true }),
3402 );
3403
3404 let data = LlmGenerationData::success(
3405 vec![Message::user("Hello")],
3406 vec![],
3407 Some("Hi".to_string()),
3408 vec![],
3409 "gpt-5.4".to_string(),
3410 Some("openai".to_string()),
3411 None,
3412 Some(42),
3413 Some(12),
3414 )
3415 .with_request_options(LlmRequestOptions {
3416 prompt_cache: Some(LlmPromptCacheInfo {
3417 enabled: true,
3418 strategy: PromptCacheStrategy::Auto,
3419 provider_mode: Some("prompt_cache_key".to_string()),
3420 }),
3421 tool_search: Some(LlmToolSearchInfo {
3422 enabled: true,
3423 threshold: 8,
3424 }),
3425 provider_options,
3426 metadata: Default::default(),
3427 });
3428
3429 let json = serde_json::to_value(&data).unwrap();
3430 assert_eq!(
3431 json["metadata"]["request_options"]["prompt_cache"]["provider_mode"],
3432 "prompt_cache_key"
3433 );
3434 assert_eq!(
3435 json["metadata"]["request_options"]["tool_search"]["threshold"],
3436 8
3437 );
3438 assert_eq!(
3439 json["metadata"]["request_options"]["provider_options"]["openai"]["previous_response_id"],
3440 true
3441 );
3442 }
3443
3444 #[test]
3445 fn test_extended_thinking_event_types() {
3446 assert_eq!(REASON_THINKING_STARTED, "reason.thinking.started");
3447 assert_eq!(REASON_THINKING_DELTA, "reason.thinking.delta");
3448 assert_eq!(REASON_THINKING_COMPLETED, "reason.thinking.completed");
3449 }
3450
3451 #[test]
3452 fn test_output_message_started_data() {
3453 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3454 let data = OutputMessageStartedData {
3455 turn_id,
3456 message_id: MessageId::new(),
3457 model: Some("claude-4-opus".to_string()),
3458 iteration: None,
3459 phase: None,
3460 };
3461
3462 let event_data: EventData = data.into();
3463 assert_eq!(event_data.event_type(), OUTPUT_MESSAGE_STARTED);
3464
3465 let json = serde_json::to_string(&event_data).unwrap();
3467 assert!(json.contains("turn_id"));
3468 assert!(json.contains("claude-4-opus"));
3469 }
3470
3471 #[test]
3472 fn test_output_message_started_data_without_model() {
3473 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3474 let data = OutputMessageStartedData {
3475 turn_id,
3476 message_id: MessageId::new(),
3477 model: None,
3478 iteration: None,
3479 phase: None,
3480 };
3481
3482 let json = serde_json::to_string(&data).unwrap();
3484 assert!(!json.contains("model"));
3485 }
3486
3487 #[test]
3488 fn test_reason_thinking_started_data() {
3489 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3490 let data = ReasonThinkingStartedData {
3491 turn_id,
3492 model: Some("claude-4-opus".to_string()),
3493 };
3494
3495 let event_data: EventData = data.into();
3496 assert_eq!(event_data.event_type(), REASON_THINKING_STARTED);
3497
3498 let json = serde_json::to_string(&event_data).unwrap();
3500 assert!(json.contains("turn_id"));
3501 assert!(json.contains("claude-4-opus"));
3502 }
3503
3504 #[test]
3505 fn test_reason_thinking_delta_data() {
3506 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3507 let data = ReasonThinkingDeltaData {
3508 turn_id,
3509 delta: "thinking step 1".to_string(),
3510 accumulated: "thinking step 1".to_string(),
3511 };
3512
3513 let event_data: EventData = data.into();
3514 assert_eq!(event_data.event_type(), REASON_THINKING_DELTA);
3515
3516 let json = serde_json::to_string(&event_data).unwrap();
3518 assert!(json.contains("turn_id"));
3519 assert!(json.contains("delta"));
3520 assert!(json.contains("accumulated"));
3521 }
3522
3523 #[test]
3524 fn test_reason_thinking_completed_data() {
3525 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3526 let data = ReasonThinkingCompletedData {
3527 turn_id,
3528 thinking: "Full thinking content here".to_string(),
3529 };
3530
3531 let event_data: EventData = data.into();
3532 assert_eq!(event_data.event_type(), REASON_THINKING_COMPLETED);
3533
3534 let json = serde_json::to_string(&event_data).unwrap();
3536 assert!(json.contains("turn_id"));
3537 assert!(json.contains("thinking"));
3538 }
3539
3540 #[test]
3541 fn test_output_message_delta_data() {
3542 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3543 let data = OutputMessageDeltaData {
3544 turn_id,
3545 message_id: MessageId::new(),
3546 delta: "Hello".to_string(),
3547 accumulated: "Hello".to_string(),
3548 phase: None,
3549 };
3550
3551 let event_data: EventData = data.into();
3552 assert_eq!(event_data.event_type(), OUTPUT_MESSAGE_DELTA);
3553
3554 let json = serde_json::to_string(&event_data).unwrap();
3556 assert!(json.contains("turn_id"));
3557 assert!(json.contains("delta"));
3558 assert!(json.contains("accumulated"));
3559 }
3560
3561 #[test]
3562 fn test_output_message_lifecycle_shares_message_id() {
3563 let turn_id = TurnId::new();
3564 let message_id = MessageId::new();
3565 let next_message_id = MessageId::new();
3566
3567 let started = OutputMessageStartedData {
3568 turn_id,
3569 message_id,
3570 model: None,
3571 iteration: Some(1),
3572 phase: None,
3573 };
3574 let next_started = OutputMessageStartedData {
3575 turn_id,
3576 message_id: next_message_id,
3577 model: None,
3578 iteration: Some(2),
3579 phase: None,
3580 };
3581 let delta = OutputMessageDeltaData {
3582 turn_id,
3583 message_id,
3584 delta: "Hello".to_string(),
3585 accumulated: "Hello".to_string(),
3586 phase: None,
3587 };
3588 let replaced = OutputMessageReplacedData {
3589 turn_id,
3590 message_id,
3591 guardrail_capability_id: "guardrails".to_string(),
3592 guardrail_id: "example".to_string(),
3593 reason_code: "blocked".to_string(),
3594 replacement: "Safe response".to_string(),
3595 };
3596 let completed = OutputMessageCompletedData::new(
3597 Message::assistant("Safe response").with_id(message_id),
3598 );
3599
3600 assert_eq!(started.message_id, message_id);
3601 assert_eq!(delta.message_id, message_id);
3602 assert_eq!(replaced.message_id, message_id);
3603 assert_eq!(completed.message.id, message_id);
3604 assert_ne!(started.message_id, next_started.message_id);
3605
3606 for value in [
3607 serde_json::to_value(started).unwrap(),
3608 serde_json::to_value(delta).unwrap(),
3609 serde_json::to_value(replaced).unwrap(),
3610 ] {
3611 assert_eq!(value["message_id"], message_id.to_string());
3612 }
3613 }
3614
3615 #[test]
3616 fn test_output_message_phase_hint_serde() {
3617 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3621
3622 let started_none = OutputMessageStartedData {
3623 turn_id,
3624 message_id: MessageId::new(),
3625 model: None,
3626 iteration: None,
3627 phase: None,
3628 };
3629 assert!(
3630 !serde_json::to_string(&started_none)
3631 .unwrap()
3632 .contains("phase")
3633 );
3634
3635 let delta_commentary = OutputMessageDeltaData {
3636 turn_id,
3637 message_id: MessageId::new(),
3638 delta: "one moment".to_string(),
3639 accumulated: "one moment".to_string(),
3640 phase: Some(crate::message::ExecutionPhase::Commentary),
3641 };
3642 let json = serde_json::to_value(&delta_commentary).unwrap();
3643 assert_eq!(json["phase"], "commentary");
3644
3645 let back: OutputMessageDeltaData = serde_json::from_value(json).unwrap();
3647 assert_eq!(back.phase, Some(crate::message::ExecutionPhase::Commentary));
3648
3649 let delta_final = OutputMessageDeltaData {
3650 turn_id,
3651 message_id: MessageId::new(),
3652 delta: "done".to_string(),
3653 accumulated: "done".to_string(),
3654 phase: Some(crate::message::ExecutionPhase::FinalAnswer),
3655 };
3656 assert_eq!(
3657 serde_json::to_value(&delta_final).unwrap()["phase"],
3658 "final_answer"
3659 );
3660 }
3661
3662 #[test]
3663 fn test_output_message_delta_deserialization_preserves_fields() {
3664 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3667 let data = OutputMessageDeltaData {
3668 turn_id,
3669 message_id: MessageId::new(),
3670 delta: "Hello world".to_string(),
3671 accumulated: "Hello world".to_string(),
3672 phase: None,
3673 };
3674
3675 let json = serde_json::to_value(EventData::OutputMessageDelta(data.clone())).unwrap();
3677
3678 let deserialized = deserialize_event_data(OUTPUT_MESSAGE_DELTA, json);
3680
3681 match deserialized {
3683 EventData::OutputMessageDelta(td) => {
3684 assert_eq!(td.turn_id, turn_id);
3685 assert_eq!(td.delta, "Hello world");
3686 assert_eq!(td.accumulated, "Hello world");
3687 }
3688 _ => panic!("Expected OutputMessageDelta, got different variant"),
3689 }
3690 }
3691
3692 #[test]
3693 fn test_output_message_started_deserialization() {
3694 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3695 let data = OutputMessageStartedData {
3696 turn_id,
3697 message_id: MessageId::new(),
3698 model: Some("claude-3".to_string()),
3699 iteration: None,
3700 phase: None,
3701 };
3702
3703 let json = serde_json::to_value(EventData::OutputMessageStarted(data.clone())).unwrap();
3705
3706 let deserialized = deserialize_event_data(OUTPUT_MESSAGE_STARTED, json);
3708
3709 match deserialized {
3711 EventData::OutputMessageStarted(at) => {
3712 assert_eq!(at.turn_id, turn_id);
3713 assert_eq!(at.model, Some("claude-3".to_string()));
3714 }
3715 _ => panic!("Expected OutputMessageStarted, got different variant"),
3716 }
3717 }
3718
3719 #[test]
3720 fn test_reason_thinking_started_deserialization() {
3721 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3726 let data = ReasonThinkingStartedData {
3727 turn_id,
3728 model: Some("claude-3".to_string()),
3729 };
3730
3731 let json = serde_json::to_value(&data).unwrap();
3733
3734 let deserialized = deserialize_event_data(REASON_THINKING_STARTED, json);
3736
3737 match deserialized {
3739 EventData::ReasonThinkingStarted(at) => {
3740 assert_eq!(at.turn_id, turn_id);
3741 assert_eq!(at.model, Some("claude-3".to_string()));
3742 }
3743 other => panic!("Expected ReasonThinkingStarted, got {}", other.event_type()),
3744 }
3745 }
3746
3747 #[test]
3748 fn test_llm_generation_with_ttft() {
3749 let messages = vec![Message::user("Hello")];
3750 let data = LlmGenerationData::success_with_metadata(
3751 messages,
3752 vec![],
3753 Some("Hi!".to_string()),
3754 vec![],
3755 "gpt-4o".to_string(),
3756 Some("openai".to_string()),
3757 Some(TokenUsage {
3758 input_tokens: 10,
3759 output_tokens: 5,
3760 cache_read_tokens: None,
3761 cache_creation_tokens: None,
3762 actual_cost_usd: None,
3763 estimated_cost_usd: None,
3764 effective_cost_usd: None,
3765 }),
3766 Some(500), Some(120), Some(vec!["stop".to_string()]),
3769 None,
3770 );
3771
3772 assert!(data.metadata.success);
3773 assert_eq!(data.metadata.duration_ms, Some(500));
3774 assert_eq!(data.metadata.time_to_first_token_ms, Some(120));
3775 }
3776
3777 #[test]
3778 fn test_llm_generation_ttft_serialization() {
3779 let messages = vec![Message::user("test")];
3780 let data = LlmGenerationData::success_with_metadata(
3781 messages,
3782 vec![],
3783 Some("response".to_string()),
3784 vec![],
3785 "model".to_string(),
3786 None,
3787 None,
3788 Some(1000),
3789 Some(150), None,
3791 None,
3792 );
3793
3794 let json = serde_json::to_string(&data).unwrap();
3795 assert!(json.contains("time_to_first_token_ms"));
3796 assert!(json.contains("150"));
3797 }
3798
3799 #[test]
3800 fn test_reason_item_data_event_type_and_serialization() {
3801 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3802 let data = ReasonItemData {
3803 turn_id,
3804 provider: "openai".to_string(),
3805 model: Some("gpt-5.5".to_string()),
3806 item_id: "rs_abc".to_string(),
3807 encrypted_content: Some("OPAQUE_BLOB".to_string()),
3808 summary: vec!["safe summary".to_string()],
3809 token_count: Some(123),
3810 };
3811
3812 let event_data: EventData = data.into();
3813 assert_eq!(event_data.event_type(), REASON_ITEM);
3814
3815 let json = serde_json::to_string(&event_data).unwrap();
3816 assert!(json.contains("turn_id"));
3817 assert!(json.contains("openai"));
3818 assert!(json.contains("rs_abc"));
3819 assert!(json.contains("OPAQUE_BLOB"));
3820 assert!(json.contains("safe summary"));
3821 }
3822
3823 #[test]
3824 fn test_event_deserialize_reason_item_uses_event_type_dispatch() {
3825 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3826 let payload = serde_json::json!({
3827 "id": EventId::new().to_string(),
3828 "type": REASON_ITEM,
3829 "ts": Utc::now().to_rfc3339(),
3830 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
3831 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
3832 "data": {
3833 "turn_id": turn_id.to_string(),
3834 "provider": "openai",
3835 "model": "gpt-5",
3836 "item_id": "rs_event",
3837 "encrypted_content": "ENC",
3838 "summary": ["safe"],
3839 "token_count": 9
3840 }
3841 });
3842
3843 let event: Event = serde_json::from_value(payload).expect("event deserializes");
3844 match event.data {
3845 EventData::ReasonItem(data) => {
3846 assert_eq!(data.turn_id, turn_id);
3847 assert_eq!(data.provider, "openai");
3848 assert_eq!(data.item_id, "rs_event");
3849 assert_eq!(data.token_count, Some(9));
3850 }
3851 other => panic!("expected reason.item data, got {}", other.event_type()),
3852 }
3853 }
3854
3855 #[test]
3856 fn test_event_request_deserialize_reason_item_uses_event_type_dispatch() {
3857 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3858 let payload = serde_json::json!({
3859 "type": REASON_ITEM,
3860 "ts": Utc::now().to_rfc3339(),
3861 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
3862 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
3863 "data": {
3864 "turn_id": turn_id.to_string(),
3865 "provider": "openai",
3866 "item_id": "rs_request",
3867 "encrypted_content": "ENC",
3868 "summary": ["safe"]
3869 }
3870 });
3871
3872 let req: EventRequest = serde_json::from_value(payload).expect("request deserializes");
3873 match req.data {
3874 EventData::ReasonItem(data) => {
3875 assert_eq!(data.turn_id, turn_id);
3876 assert_eq!(data.provider, "openai");
3877 assert_eq!(data.item_id, "rs_request");
3878 }
3879 other => panic!("expected reason.item data, got {}", other.event_type()),
3880 }
3881 }
3882
3883 #[test]
3884 fn test_reason_item_data_round_trip_uses_typed_dispatch() {
3885 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3889 let data = ReasonItemData {
3890 turn_id,
3891 provider: "openai".to_string(),
3892 model: Some("gpt-5".to_string()),
3893 item_id: "rs_xyz".to_string(),
3894 encrypted_content: Some("ENC".to_string()),
3895 summary: vec![],
3896 token_count: None,
3897 };
3898
3899 let json = serde_json::to_value(&data).unwrap();
3900 let deserialized = deserialize_event_data(REASON_ITEM, json);
3901
3902 match deserialized {
3903 EventData::ReasonItem(out) => {
3904 assert_eq!(out.turn_id, turn_id);
3905 assert_eq!(out.provider, "openai");
3906 assert_eq!(out.item_id, "rs_xyz");
3907 assert_eq!(out.encrypted_content.as_deref(), Some("ENC"));
3908 }
3909 other => panic!("Expected ReasonItem, got {}", other.event_type()),
3910 }
3911 }
3912
3913 #[test]
3922 fn test_reason_item_resolves_via_type_dispatch_despite_overlap() {
3923 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3928 let json = serde_json::json!({
3929 "turn_id": turn_id.to_string(),
3930 "provider": "openai",
3931 "model": "gpt-5",
3932 "item_id": "rs_keep",
3933 "encrypted_content": "ENC",
3934 "summary": ["s"],
3935 "token_count": 7,
3936 });
3937
3938 let as_thinking: ReasonThinkingStartedData =
3943 serde_json::from_value(json.clone()).expect("thinking ignores extra fields");
3944 assert_eq!(as_thinking.turn_id, turn_id);
3945 assert_eq!(as_thinking.model.as_deref(), Some("gpt-5"));
3946
3947 let as_item: ReasonItemData =
3948 serde_json::from_value(json.clone()).expect("ReasonItem accepts payload");
3949 assert_eq!(as_item.item_id, "rs_keep");
3950 assert_eq!(as_item.provider, "openai");
3951
3952 let event_data = deserialize_event_data(REASON_ITEM, json);
3955 match event_data {
3956 EventData::ReasonItem(out) => {
3957 assert_eq!(out.item_id, "rs_keep");
3958 assert_eq!(out.provider, "openai");
3959 }
3960 other => panic!(
3961 "Typed dispatcher must select ReasonItem for {REASON_ITEM}, got {}",
3962 other.event_type()
3963 ),
3964 }
3965 }
3966
3967 #[test]
3974 fn test_reason_item_data_excludes_plaintext_reasoning() {
3975 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3976 let data = ReasonItemData {
3977 turn_id,
3978 provider: "openai".to_string(),
3979 model: Some("gpt-5".to_string()),
3980 item_id: "rs_secret".to_string(),
3981 encrypted_content: Some("opaque_blob_thinking_content_reasoning_text".to_string()),
3985 summary: vec!["safe summary mentioning content and thinking".to_string()],
3986 token_count: Some(1),
3987 };
3988
3989 let value = serde_json::to_value(&data).expect("serializable");
3990 let object = value.as_object().expect("data serializes to JSON object");
3991 for forbidden in [
3992 "content",
3993 "reasoning_text",
3994 "thinking",
3995 "reasoning_content",
3996 "raw_reasoning",
3997 ] {
3998 assert!(
3999 !object.contains_key(forbidden),
4000 "ReasonItemData JSON must not expose `{forbidden}` key, got: {object:?}",
4001 );
4002 }
4003 assert!(object.contains_key("encrypted_content"));
4005 assert!(object.contains_key("summary"));
4006 }
4007
4008 #[test]
4009 fn test_llm_generation_ttft_omitted_when_none() {
4010 let messages = vec![Message::user("test")];
4011 let data = LlmGenerationData::success(
4012 messages,
4013 vec![],
4014 Some("response".to_string()),
4015 vec![],
4016 "model".to_string(),
4017 None,
4018 None,
4019 None,
4020 None, );
4022
4023 assert!(data.metadata.time_to_first_token_ms.is_none());
4025
4026 let json = serde_json::to_string(&data).unwrap();
4028 assert!(!json.contains("time_to_first_token_ms"));
4029 }
4030}
4031
4032#[cfg(test)]
4041mod contract_tests {
4042 use super::*;
4043 use insta::{assert_json_snapshot, with_settings};
4044
4045 fn test_session_id() -> SessionId {
4047 SessionId::from_uuid(uuid::Uuid::from_u128(
4048 0x0000_0000_0000_0000_0000_0000_0000_0001,
4049 ))
4050 }
4051
4052 fn test_turn_id() -> TurnId {
4053 TurnId::from_uuid(uuid::Uuid::from_u128(
4054 0x0000_0000_0000_0000_0000_0000_0000_0002,
4055 ))
4056 }
4057
4058 fn test_message_id() -> MessageId {
4059 MessageId::from_uuid(uuid::Uuid::from_u128(
4060 0x0000_0000_0000_0000_0000_0000_0000_0003,
4061 ))
4062 }
4063
4064 fn test_agent_id() -> AgentId {
4065 AgentId::from_uuid(uuid::Uuid::from_u128(
4066 0x0000_0000_0000_0000_0000_0000_0000_0004,
4067 ))
4068 }
4069
4070 fn test_harness_id() -> HarnessId {
4071 HarnessId::from_uuid(uuid::Uuid::from_u128(
4072 0x0000_0000_0000_0000_0000_0000_0000_0005,
4073 ))
4074 }
4075
4076 #[test]
4083 fn snapshot_input_message() {
4084 let data = InputMessageData::new(Message::user("Hello, world!"));
4085 with_settings!({
4086 sort_maps => true,
4087 }, {
4088 assert_json_snapshot!("event_data_input_message", data, {
4090 ".message.id" => "[MESSAGE_ID]",
4091 ".message.created_at" => "[TIMESTAMP]"
4092 });
4093 });
4094 }
4095
4096 #[test]
4097 fn snapshot_output_message_started() {
4098 let data = OutputMessageStartedData {
4099 turn_id: test_turn_id(),
4100 message_id: test_message_id(),
4101 model: Some("gpt-4o".to_string()),
4102 iteration: None,
4103 phase: None,
4104 };
4105 with_settings!({
4106 sort_maps => true,
4107 }, {
4108 assert_json_snapshot!("event_data_output_message_started", data);
4109 });
4110 }
4111
4112 #[test]
4113 fn snapshot_output_message_delta() {
4114 let data = OutputMessageDeltaData {
4115 turn_id: test_turn_id(),
4116 message_id: test_message_id(),
4117 delta: "Hello".to_string(),
4118 accumulated: "Hello".to_string(),
4119 phase: None,
4120 };
4121 with_settings!({
4122 sort_maps => true,
4123 }, {
4124 assert_json_snapshot!("event_data_output_message_delta", data);
4125 });
4126 }
4127
4128 #[test]
4129 fn snapshot_output_message_completed() {
4130 let data = OutputMessageCompletedData::new(Message::assistant("Hello!"));
4131 with_settings!({
4132 sort_maps => true,
4133 }, {
4134 assert_json_snapshot!("event_data_output_message_completed", data, {
4136 ".message.id" => "[MESSAGE_ID]",
4137 ".message.created_at" => "[TIMESTAMP]"
4138 });
4139 });
4140 }
4141
4142 #[test]
4143 fn snapshot_turn_started() {
4144 let data = TurnStartedData {
4145 turn_id: test_turn_id(),
4146 input_message_id: test_message_id(),
4147 input_content: Some("Hello".to_string()),
4148 };
4149 with_settings!({
4150 sort_maps => true,
4151 }, {
4152 assert_json_snapshot!("event_data_turn_started", data);
4153 });
4154 }
4155
4156 #[test]
4157 fn snapshot_turn_completed() {
4158 let data = TurnCompletedData {
4159 turn_id: test_turn_id(),
4160 iterations: 3,
4161 duration_ms: Some(1500),
4162 usage: Some(TokenUsage::new(100, 50)),
4163 input_content: None,
4164 final_message_id: Some(test_message_id()),
4165 final_answer_preview: Some("Done.".to_string()),
4166 time_to_first_token_ms: Some(120),
4167 tool_call_count: Some(2),
4168 llm_call_count: Some(3),
4169 status: Some("completed".to_string()),
4170 };
4171 with_settings!({
4172 sort_maps => true,
4173 }, {
4174 assert_json_snapshot!("event_data_turn_completed", data);
4175 });
4176 }
4177
4178 #[test]
4179 fn snapshot_turn_failed() {
4180 let data = TurnFailedData {
4181 turn_id: test_turn_id(),
4182 error: "Rate limit exceeded".to_string(),
4183 error_code: Some("RATE_LIMIT".to_string()),
4184 error_fields: None,
4185 error_disclosure: None,
4186 };
4187 with_settings!({
4188 sort_maps => true,
4189 }, {
4190 assert_json_snapshot!("event_data_turn_failed", data);
4191 });
4192 }
4193
4194 #[test]
4195 fn snapshot_turn_cancelled() {
4196 let data = TurnCancelledData {
4197 turn_id: test_turn_id(),
4198 reason: Some("User requested".to_string()),
4199 usage: Some(TokenUsage::new(50, 25)),
4200 };
4201 with_settings!({
4202 sort_maps => true,
4203 }, {
4204 assert_json_snapshot!("event_data_turn_cancelled", data);
4205 });
4206 }
4207
4208 #[test]
4209 fn snapshot_reason_started() {
4210 let data = ReasonStartedData {
4211 harness_id: test_harness_id(),
4212 agent_id: Some(test_agent_id()),
4213 metadata: Some(ModelMetadata {
4214 model: "gpt-4o".to_string(),
4215 model_id: None,
4216 provider_id: None,
4217 }),
4218 };
4219 with_settings!({
4220 sort_maps => true,
4221 }, {
4222 assert_json_snapshot!("event_data_reason_started", data);
4223 });
4224 }
4225
4226 #[test]
4227 fn snapshot_reason_completed() {
4228 let data = ReasonCompletedData::success(
4229 "Hello world",
4230 true,
4231 2,
4232 Some(1000),
4233 Some(TokenUsage::new(100, 50)),
4234 );
4235 with_settings!({
4236 sort_maps => true,
4237 }, {
4238 assert_json_snapshot!("event_data_reason_completed", data);
4239 });
4240 }
4241
4242 #[test]
4243 fn snapshot_act_started() {
4244 let data = ActStartedData {
4245 tool_calls: vec![ToolCallSummary {
4246 id: "tc_1".to_string(),
4247 name: "get_weather".to_string(),
4248 display_name: None,
4249 narration: None,
4250 }],
4251 headline: None,
4252 };
4253 with_settings!({
4254 sort_maps => true,
4255 }, {
4256 assert_json_snapshot!("event_data_act_started", data);
4257 });
4258 }
4259
4260 #[test]
4261 fn snapshot_act_completed() {
4262 let data = ActCompletedData {
4263 completed: true,
4264 success_count: 2,
4265 error_count: 0,
4266 duration_ms: Some(500),
4267 headline: None,
4268 };
4269 with_settings!({
4270 sort_maps => true,
4271 }, {
4272 assert_json_snapshot!("event_data_act_completed", data);
4273 });
4274 }
4275
4276 #[test]
4277 fn snapshot_tool_started() {
4278 let data = ToolStartedData {
4279 tool_call: ToolCall {
4280 id: "tc_1".to_string(),
4281 name: "get_weather".to_string(),
4282 arguments: serde_json::json!({"city": "London"}),
4283 },
4284 tool_call_fingerprint: None,
4285 display_name: None,
4286 narration: None,
4287 };
4288 with_settings!({
4289 sort_maps => true,
4290 }, {
4291 assert_json_snapshot!("event_data_tool_started", data);
4292 });
4293 }
4294
4295 #[test]
4296 fn snapshot_tool_completed() {
4297 let data = ToolCompletedData::success(
4298 "tc_1".to_string(),
4299 "get_weather".to_string(),
4300 vec![crate::message::ContentPart::text("Sunny, 22°C")],
4301 Some(250),
4302 );
4303 with_settings!({
4304 sort_maps => true,
4305 }, {
4306 assert_json_snapshot!("event_data_tool_completed", data);
4307 });
4308 }
4309
4310 #[test]
4311 fn snapshot_llm_generation() {
4312 let data = LlmGenerationData::success(
4313 vec![Message::user("Hello")],
4314 vec![ToolDefinitionSummary {
4315 name: "tool1".to_string(),
4316 display_name: None,
4317 category: None,
4318 capability_id: None,
4319 capability_name: None,
4320 description: "A tool".to_string(),
4321 }],
4322 Some("Hi there!".to_string()),
4323 vec![],
4324 "gpt-4o".to_string(),
4325 Some("openai".to_string()),
4326 Some(TokenUsage::new(10, 5)),
4327 Some(100),
4328 Some(25),
4329 );
4330 with_settings!({
4331 sort_maps => true,
4332 }, {
4333 assert_json_snapshot!("event_data_llm_generation", data, {
4335 ".messages[].id" => "[MESSAGE_ID]",
4336 ".messages[].created_at" => "[TIMESTAMP]"
4337 });
4338 });
4339 }
4340
4341 #[test]
4342 fn snapshot_reason_thinking_started() {
4343 let data = ReasonThinkingStartedData {
4344 turn_id: test_turn_id(),
4345 model: Some("claude-4-opus".to_string()),
4346 };
4347 with_settings!({
4348 sort_maps => true,
4349 }, {
4350 assert_json_snapshot!("event_data_reason_thinking_started", data);
4351 });
4352 }
4353
4354 #[test]
4355 fn snapshot_reason_thinking_delta() {
4356 let data = ReasonThinkingDeltaData {
4357 turn_id: test_turn_id(),
4358 delta: "Let me think...".to_string(),
4359 accumulated: "Let me think...".to_string(),
4360 };
4361 with_settings!({
4362 sort_maps => true,
4363 }, {
4364 assert_json_snapshot!("event_data_reason_thinking_delta", data);
4365 });
4366 }
4367
4368 #[test]
4369 fn snapshot_reason_thinking_completed() {
4370 let data = ReasonThinkingCompletedData {
4371 turn_id: test_turn_id(),
4372 thinking: "I need to consider...".to_string(),
4373 };
4374 with_settings!({
4375 sort_maps => true,
4376 }, {
4377 assert_json_snapshot!("event_data_reason_thinking_completed", data);
4378 });
4379 }
4380
4381 #[test]
4382 fn snapshot_reason_item() {
4383 let data = ReasonItemData {
4384 turn_id: test_turn_id(),
4385 provider: "openai".to_string(),
4386 model: Some("gpt-5.5".to_string()),
4387 item_id: "rs_test".to_string(),
4388 encrypted_content: Some("OPAQUE".to_string()),
4389 summary: vec!["safe summary".to_string()],
4390 token_count: Some(42),
4391 };
4392 with_settings!({
4393 sort_maps => true,
4394 }, {
4395 assert_json_snapshot!("event_data_reason_item", data);
4396 });
4397 }
4398
4399 #[test]
4400 fn snapshot_session_started() {
4401 let data = SessionStartedData {
4402 harness_id: test_harness_id(),
4403 agent_id: Some(test_agent_id()),
4404 model_id: None,
4405 };
4406 with_settings!({
4407 sort_maps => true,
4408 }, {
4409 assert_json_snapshot!("event_data_session_started", data);
4410 });
4411 }
4412
4413 #[test]
4414 fn snapshot_session_activated() {
4415 let data = SessionActivatedData {
4416 turn_id: test_turn_id(),
4417 input_message_id: test_message_id(),
4418 };
4419 with_settings!({
4420 sort_maps => true,
4421 }, {
4422 assert_json_snapshot!("event_data_session_activated", data);
4423 });
4424 }
4425
4426 #[test]
4427 fn snapshot_session_idled() {
4428 let data = SessionIdledData {
4429 turn_id: test_turn_id(),
4430 iterations: Some(3),
4431 usage: Some(TokenUsage::new(500, 200)),
4432 };
4433 with_settings!({
4434 sort_maps => true,
4435 }, {
4436 assert_json_snapshot!("event_data_session_idled", data);
4437 });
4438 }
4439
4440 #[test]
4441 fn snapshot_session_title_updated() {
4442 let data = SessionTitleUpdatedData {
4443 previous_title: Some("Old title".to_string()),
4444 title: "Automatic Session Titles".to_string(),
4445 };
4446 with_settings!({
4447 sort_maps => true,
4448 }, {
4449 assert_json_snapshot!("event_data_session_title_updated", data);
4450 });
4451
4452 let untitled = serde_json::to_value(SessionTitleUpdatedData {
4453 previous_title: None,
4454 title: "First title".to_string(),
4455 })
4456 .expect("serialize title event");
4457 assert!(untitled["previous_title"].is_null());
4458 }
4459
4460 #[test]
4466 fn tool_call_summary_with_display_name() {
4467 let summary = ToolCallSummary {
4468 id: "tc_1".to_string(),
4469 name: "get_weather".to_string(),
4470 display_name: Some("Get Weather".to_string()),
4471 narration: None,
4472 };
4473 let json = serde_json::to_value(&summary).unwrap();
4474 assert_eq!(json["display_name"], "Get Weather");
4475
4476 let deserialized: ToolCallSummary = serde_json::from_value(json).unwrap();
4478 assert_eq!(deserialized.display_name.as_deref(), Some("Get Weather"));
4479 }
4480
4481 #[test]
4482 fn tool_call_summary_without_display_name_omits_field() {
4483 let summary = ToolCallSummary {
4484 id: "tc_1".to_string(),
4485 name: "get_weather".to_string(),
4486 display_name: None,
4487 narration: None,
4488 };
4489 let json = serde_json::to_string(&summary).unwrap();
4490 assert!(!json.contains("display_name"));
4491
4492 let json_without = r#"{"id":"tc_1","name":"get_weather"}"#;
4494 let deserialized: ToolCallSummary = serde_json::from_str(json_without).unwrap();
4495 assert_eq!(deserialized.display_name, None);
4496 }
4497
4498 #[test]
4499 fn act_started_with_definitions_populates_display_names() {
4500 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4501
4502 let tool_calls = vec![
4503 ToolCall {
4504 id: "tc_1".to_string(),
4505 name: "get_weather".to_string(),
4506 arguments: serde_json::json!({}),
4507 },
4508 ToolCall {
4509 id: "tc_2".to_string(),
4510 name: "unknown_tool".to_string(),
4511 arguments: serde_json::json!({}),
4512 },
4513 ];
4514 let tool_defs = vec![crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4515 name: "get_weather".to_string(),
4516 display_name: Some("Get Weather".to_string()),
4517 description: "Gets weather".to_string(),
4518 parameters: serde_json::json!({}),
4519 policy: ToolPolicy::Auto,
4520 category: None,
4521 deferrable: DeferrablePolicy::default(),
4522 hints: crate::tool_types::ToolHints::default(),
4523 full_parameters: None,
4524 })];
4525
4526 let data = ActStartedData::with_definitions(&tool_calls, &tool_defs);
4527 assert_eq!(data.tool_calls.len(), 2);
4528 assert_eq!(
4529 data.tool_calls[0].display_name.as_deref(),
4530 Some("Get Weather")
4531 );
4532 assert_eq!(data.tool_calls[1].display_name, None);
4533 }
4534
4535 #[test]
4536 fn tool_completed_with_display_name_roundtrip() {
4537 let data = ToolCompletedData::success(
4538 "tc_1".to_string(),
4539 "get_weather".to_string(),
4540 vec![crate::message::ContentPart::text("Sunny")],
4541 Some(100),
4542 )
4543 .with_display_name(Some("Get Weather".to_string()));
4544
4545 assert_eq!(data.display_name.as_deref(), Some("Get Weather"));
4546
4547 let json = serde_json::to_value(&data).unwrap();
4548 assert_eq!(json["display_name"], "Get Weather");
4549
4550 let deserialized: ToolCompletedData = serde_json::from_value(json).unwrap();
4551 assert_eq!(deserialized.display_name.as_deref(), Some("Get Weather"));
4552 }
4553
4554 #[test]
4555 fn tool_started_display_name_serialization() {
4556 let data = ToolStartedData {
4557 tool_call: ToolCall {
4558 id: "tc_1".to_string(),
4559 name: "bash".to_string(),
4560 arguments: serde_json::json!({"command": "ls"}),
4561 },
4562 tool_call_fingerprint: None,
4563 display_name: Some("Bash".to_string()),
4564 narration: None,
4565 };
4566
4567 let json = serde_json::to_value(&data).unwrap();
4568 assert_eq!(json["display_name"], "Bash");
4569 }
4570
4571 #[test]
4572 fn tool_definition_summary_display_name() {
4573 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4574
4575 let def = crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4576 name: "read_file".to_string(),
4577 display_name: Some("Read File".to_string()),
4578 description: "Reads a file".to_string(),
4579 parameters: serde_json::json!({}),
4580 policy: ToolPolicy::Auto,
4581 category: None,
4582 deferrable: DeferrablePolicy::default(),
4583 hints: crate::tool_types::ToolHints::default(),
4584 full_parameters: None,
4585 });
4586
4587 let summary = ToolDefinitionSummary::from(&def);
4588 assert_eq!(summary.display_name.as_deref(), Some("Read File"));
4589
4590 let json = serde_json::to_value(&summary).unwrap();
4591 assert_eq!(json["display_name"], "Read File");
4592 }
4593
4594 #[test]
4601 fn forward_compat_unknown_fields_ignored() {
4602 let json = r#"{
4604 "turn_id": "turn_00000000000000000000000000000002",
4605 "iterations": 3,
4606 "duration_ms": 1500,
4607 "usage": {"input_tokens": 100, "output_tokens": 50},
4608 "future_field": "should be ignored",
4609 "another_new_field": 42
4610 }"#;
4611
4612 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4613 assert_eq!(data.iterations, 3);
4614 assert_eq!(data.duration_ms, Some(1500));
4615 }
4616
4617 #[test]
4618 fn forward_compat_unknown_event_type_becomes_unsupported() {
4619 let json = serde_json::json!({"some_field": "value"});
4621 let data = deserialize_event_data("future.event.type", json);
4622
4623 assert!(data.is_unsupported());
4624 assert_eq!(data.event_type(), "unsupported");
4625 }
4626
4627 #[test]
4628 fn forward_compat_unsupported_preserves_data() {
4629 let original = serde_json::json!({"key": "value", "nested": {"a": 1}});
4631 let data = deserialize_event_data("unknown.event", original.clone());
4632
4633 match data {
4634 EventData::Unsupported { event_type, data } => {
4635 assert_eq!(event_type, "unknown.event");
4636 assert_eq!(data, original);
4637 }
4638 _ => panic!("Expected Unsupported variant"),
4639 }
4640 }
4641
4642 #[test]
4643 fn forward_compat_optional_fields_absent() {
4644 let json = r#"{
4646 "turn_id": "turn_00000000000000000000000000000002",
4647 "iterations": 3
4648 }"#;
4649
4650 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4651 assert_eq!(data.iterations, 3);
4652 assert!(data.duration_ms.is_none());
4653 assert!(data.usage.is_none());
4654 assert!(data.input_content.is_none());
4655 assert!(data.final_message_id.is_none());
4656 assert!(data.final_answer_preview.is_none());
4657 assert!(data.time_to_first_token_ms.is_none());
4658 assert!(data.tool_call_count.is_none());
4659 assert!(data.llm_call_count.is_none());
4660 assert!(data.status.is_none());
4661 }
4662
4663 #[test]
4669 fn round_trip_all_event_data_types() {
4670 let test_cases: Vec<(&str, EventData)> = vec![
4672 (
4673 INPUT_MESSAGE,
4674 InputMessageData::new(Message::user("test")).into(),
4675 ),
4676 (
4677 OUTPUT_MESSAGE_STARTED,
4678 OutputMessageStartedData {
4679 turn_id: test_turn_id(),
4680 message_id: test_message_id(),
4681 model: None,
4682 iteration: None,
4683 phase: None,
4684 }
4685 .into(),
4686 ),
4687 (
4688 OUTPUT_MESSAGE_DELTA,
4689 OutputMessageDeltaData {
4690 turn_id: test_turn_id(),
4691 message_id: test_message_id(),
4692 delta: "x".to_string(),
4693 accumulated: "x".to_string(),
4694 phase: None,
4695 }
4696 .into(),
4697 ),
4698 (
4699 OUTPUT_MESSAGE_COMPLETED,
4700 OutputMessageCompletedData::new(Message::assistant("hi")).into(),
4701 ),
4702 (
4703 TURN_STARTED,
4704 TurnStartedData {
4705 turn_id: test_turn_id(),
4706 input_message_id: test_message_id(),
4707 input_content: None,
4708 }
4709 .into(),
4710 ),
4711 (
4712 TURN_COMPLETED,
4713 TurnCompletedData {
4714 turn_id: test_turn_id(),
4715 iterations: 1,
4716 duration_ms: None,
4717 usage: None,
4718 input_content: None,
4719 final_message_id: None,
4720 final_answer_preview: None,
4721 time_to_first_token_ms: None,
4722 tool_call_count: None,
4723 llm_call_count: None,
4724 status: None,
4725 }
4726 .into(),
4727 ),
4728 (
4729 TURN_FAILED,
4730 TurnFailedData {
4731 turn_id: test_turn_id(),
4732 error: "err".to_string(),
4733 error_code: None,
4734 error_fields: None,
4735 error_disclosure: None,
4736 }
4737 .into(),
4738 ),
4739 (
4740 TURN_CANCELLED,
4741 TurnCancelledData {
4742 turn_id: test_turn_id(),
4743 reason: None,
4744 usage: None,
4745 }
4746 .into(),
4747 ),
4748 (
4749 TURN_SEALED,
4750 TurnSealedData {
4751 turn_id: test_turn_id(),
4752 reason: "no_progress".to_string(),
4753 detail: Some("sealed".to_string()),
4754 iterations: Some(3),
4755 usage: None,
4756 }
4757 .into(),
4758 ),
4759 (
4760 REASON_STARTED,
4761 ReasonStartedData {
4762 harness_id: test_harness_id(),
4763 agent_id: Some(test_agent_id()),
4764 metadata: None,
4765 }
4766 .into(),
4767 ),
4768 (
4769 REASON_COMPLETED,
4770 ReasonCompletedData::success("", false, 0, None, None).into(),
4771 ),
4772 (
4773 ACT_STARTED,
4774 ActStartedData {
4775 tool_calls: vec![],
4776 headline: None,
4777 }
4778 .into(),
4779 ),
4780 (
4781 ACT_COMPLETED,
4782 ActCompletedData {
4783 completed: true,
4784 success_count: 0,
4785 error_count: 0,
4786 duration_ms: None,
4787 headline: None,
4788 }
4789 .into(),
4790 ),
4791 (
4792 SESSION_STARTED,
4793 SessionStartedData {
4794 harness_id: test_harness_id(),
4795 agent_id: Some(test_agent_id()),
4796 model_id: None,
4797 }
4798 .into(),
4799 ),
4800 (
4801 SESSION_ACTIVATED,
4802 SessionActivatedData {
4803 turn_id: test_turn_id(),
4804 input_message_id: test_message_id(),
4805 }
4806 .into(),
4807 ),
4808 (
4809 SESSION_IDLED,
4810 SessionIdledData {
4811 turn_id: test_turn_id(),
4812 iterations: None,
4813 usage: None,
4814 }
4815 .into(),
4816 ),
4817 (
4818 SESSION_TITLE_UPDATED,
4819 SessionTitleUpdatedData {
4820 previous_title: Some("Old title".to_string()),
4821 title: "New title".to_string(),
4822 }
4823 .into(),
4824 ),
4825 ];
4826
4827 for (event_type, original) in test_cases {
4828 let json = serde_json::to_value(&original).unwrap();
4830 let deserialized = deserialize_event_data(event_type, json);
4832 assert_eq!(
4834 original.event_type(),
4835 deserialized.event_type(),
4836 "Event type mismatch for {}",
4837 event_type
4838 );
4839 }
4840 }
4841
4842 #[test]
4848 fn event_structure_has_required_fields() {
4849 let session_id = test_session_id();
4850 let context = EventContext::turn(test_turn_id(), test_message_id());
4851 let event = Event::new(
4852 session_id,
4853 context,
4854 InputMessageData::new(Message::user("test")),
4855 );
4856
4857 let json = serde_json::to_value(&event).unwrap();
4859 assert!(json.get("id").is_some(), "Missing id field");
4860 assert!(json.get("type").is_some(), "Missing type field");
4861 assert!(json.get("ts").is_some(), "Missing ts field");
4862 assert!(json.get("session_id").is_some(), "Missing session_id field");
4863 assert!(json.get("context").is_some(), "Missing context field");
4864 assert!(json.get("data").is_some(), "Missing data field");
4865 }
4866
4867 #[test]
4868 fn event_context_span_fields() {
4869 let context = EventContext::empty().with_span(
4870 "trace123".to_string(),
4871 "span456".to_string(),
4872 Some("parent789".to_string()),
4873 );
4874
4875 let json = serde_json::to_value(&context).unwrap();
4876 assert_eq!(
4877 json.get("trace_id").and_then(|v| v.as_str()),
4878 Some("trace123")
4879 );
4880 assert_eq!(
4881 json.get("span_id").and_then(|v| v.as_str()),
4882 Some("span456")
4883 );
4884 assert_eq!(
4885 json.get("parent_span_id").and_then(|v| v.as_str()),
4886 Some("parent789")
4887 );
4888 }
4889
4890 #[test]
4891 fn is_unsupported_returns_false_for_known_types() {
4892 let data = InputMessageData::new(Message::user("test"));
4893 let event_data: EventData = data.into();
4894 assert!(!event_data.is_unsupported());
4895 }
4896
4897 #[test]
4898 fn is_unsupported_returns_true_for_unsupported() {
4899 let data = deserialize_event_data("unknown.type", serde_json::json!({}));
4900 assert!(data.is_unsupported());
4901 }
4902}