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 #[serde(default, skip_serializing_if = "Option::is_none")]
1125 pub completed_narration: Option<String>,
1126}
1127
1128impl From<&ToolCall> for ToolCallSummary {
1129 fn from(tc: &ToolCall) -> Self {
1130 Self {
1131 id: tc.id.clone(),
1132 name: tc.name.clone(),
1133 display_name: None,
1134 narration: None,
1135 completed_narration: None,
1136 }
1137 }
1138}
1139
1140#[derive(Debug, Clone, Serialize, Deserialize)]
1142#[cfg_attr(feature = "openapi", derive(ToSchema))]
1143pub struct ToolDefinitionSummary {
1144 pub name: String,
1146 #[serde(default, skip_serializing_if = "Option::is_none")]
1148 pub display_name: Option<String>,
1149 #[serde(default, skip_serializing_if = "Option::is_none")]
1151 pub category: Option<String>,
1152 #[serde(default, skip_serializing_if = "Option::is_none")]
1154 pub capability_id: Option<String>,
1155 #[serde(default, skip_serializing_if = "Option::is_none")]
1157 pub capability_name: Option<String>,
1158 pub description: String,
1160}
1161
1162impl From<&crate::tool_types::ToolDefinition> for ToolDefinitionSummary {
1163 fn from(tool: &crate::tool_types::ToolDefinition) -> Self {
1164 let capability_attribution = tool.capability_attribution();
1165 Self {
1166 name: tool.name().to_string(),
1167 display_name: tool.display_name().map(|s| s.to_string()),
1168 category: tool.category().map(|s| s.to_string()),
1169 capability_id: capability_attribution.map(|(id, _)| id.to_string()),
1170 capability_name: capability_attribution.and_then(|(_, name)| name.map(str::to_string)),
1171 description: tool.description().to_string(),
1172 }
1173 }
1174}
1175
1176#[derive(Debug, Clone, Serialize, Deserialize)]
1178#[cfg_attr(feature = "openapi", derive(ToSchema))]
1179pub struct ActStartedData {
1180 pub tool_calls: Vec<ToolCallSummary>,
1182 #[serde(default, skip_serializing_if = "Option::is_none")]
1184 pub headline: Option<String>,
1185}
1186
1187impl ActStartedData {
1188 pub fn new(tool_calls: &[ToolCall]) -> Self {
1189 Self::new_with_locale(tool_calls, None)
1190 }
1191
1192 pub fn new_with_locale(tool_calls: &[ToolCall], locale: Option<&str>) -> Self {
1193 Self {
1194 tool_calls: tool_calls.iter().map(ToolCallSummary::from).collect(),
1195 headline: render_group_headline_with_locale(
1196 tool_calls,
1197 &[],
1198 ToolNarrationPhase::Started,
1199 locale,
1200 ),
1201 }
1202 }
1203
1204 pub fn with_definitions(
1206 tool_calls: &[ToolCall],
1207 tool_defs: &[crate::tool_types::ToolDefinition],
1208 ) -> Self {
1209 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
1210 }
1211
1212 pub fn with_definitions_and_locale(
1213 tool_calls: &[ToolCall],
1214 tool_defs: &[crate::tool_types::ToolDefinition],
1215 locale: Option<&str>,
1216 ) -> Self {
1217 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
1218 tool_defs.iter().map(|d| (d.name(), d)).collect();
1219 Self {
1220 tool_calls: tool_calls
1221 .iter()
1222 .map(|tc| {
1223 let tool_def = def_map.get(tc.name.as_str()).copied();
1224 let display_name = localized_tool_display_name(
1225 &tc.name,
1226 tool_def.and_then(|d| d.display_name()),
1227 locale,
1228 );
1229 ToolCallSummary {
1230 id: tc.id.clone(),
1231 name: tc.name.clone(),
1232 display_name,
1233 narration: Some(render_tool_narration_with_locale(
1234 tool_def,
1235 tc,
1236 ToolNarrationPhase::Started,
1237 locale,
1238 )),
1239 completed_narration: Some(render_tool_narration_with_locale(
1240 tool_def,
1241 tc,
1242 ToolNarrationPhase::Completed,
1243 locale,
1244 )),
1245 }
1246 })
1247 .collect(),
1248 headline: render_group_headline_with_locale(
1249 tool_calls,
1250 tool_defs,
1251 ToolNarrationPhase::Started,
1252 locale,
1253 ),
1254 }
1255 }
1256}
1257
1258#[derive(Debug, Clone, Serialize, Deserialize)]
1260#[cfg_attr(feature = "openapi", derive(ToSchema))]
1261pub struct ActCompletedData {
1262 pub completed: bool,
1264
1265 pub success_count: u32,
1267
1268 pub error_count: u32,
1270
1271 #[serde(skip_serializing_if = "Option::is_none")]
1273 pub duration_ms: Option<u64>,
1274 #[serde(default, skip_serializing_if = "Option::is_none")]
1276 pub headline: Option<String>,
1277}
1278
1279#[derive(Debug, Clone, Serialize, Deserialize)]
1281#[cfg_attr(feature = "openapi", derive(ToSchema))]
1282pub struct ToolStartedData {
1283 pub tool_call: ToolCall,
1285 #[serde(default, skip_serializing_if = "Option::is_none")]
1287 pub tool_call_fingerprint: Option<String>,
1288 #[serde(default, skip_serializing_if = "Option::is_none")]
1290 pub display_name: Option<String>,
1291 #[serde(default, skip_serializing_if = "Option::is_none")]
1293 pub narration: Option<String>,
1294}
1295
1296#[derive(Debug, Clone, Serialize, Deserialize)]
1298#[cfg_attr(feature = "openapi", derive(ToSchema))]
1299pub struct ToolCompletedData {
1300 pub tool_call_id: String,
1302
1303 pub tool_name: String,
1305
1306 #[serde(default, skip_serializing_if = "Option::is_none")]
1308 pub tool_call_fingerprint: Option<String>,
1309
1310 #[serde(default, skip_serializing_if = "Option::is_none")]
1312 pub tool_result_fingerprint: Option<String>,
1313
1314 #[serde(default, skip_serializing_if = "Option::is_none")]
1316 pub display_name: Option<String>,
1317
1318 pub success: bool,
1320
1321 pub status: String,
1323
1324 #[serde(skip_serializing_if = "Option::is_none")]
1326 pub result: Option<Vec<ContentPart>>,
1327
1328 #[serde(skip_serializing_if = "Option::is_none")]
1330 pub error: Option<String>,
1331
1332 #[serde(skip_serializing_if = "Option::is_none")]
1334 pub duration_ms: Option<u64>,
1335
1336 #[serde(default, skip_serializing_if = "Option::is_none")]
1338 pub capability_id: Option<String>,
1339
1340 #[serde(default, skip_serializing_if = "Option::is_none")]
1342 pub capability_name: Option<String>,
1343
1344 #[serde(default, skip_serializing_if = "Option::is_none")]
1346 pub narration: Option<String>,
1347}
1348
1349impl ToolCompletedData {
1350 pub fn success(
1351 tool_call_id: String,
1352 tool_name: String,
1353 result: Vec<ContentPart>,
1354 duration_ms: Option<u64>,
1355 ) -> Self {
1356 Self {
1357 tool_call_id,
1358 tool_name,
1359 tool_call_fingerprint: None,
1360 tool_result_fingerprint: None,
1361 display_name: None,
1362 success: true,
1363 status: "success".to_string(),
1364 result: Some(result),
1365 error: None,
1366 duration_ms,
1367 capability_id: None,
1368 capability_name: None,
1369 narration: None,
1370 }
1371 }
1372
1373 pub fn failure(
1374 tool_call_id: String,
1375 tool_name: String,
1376 status: String,
1377 error: String,
1378 duration_ms: Option<u64>,
1379 ) -> Self {
1380 Self {
1381 tool_call_id,
1382 tool_name,
1383 tool_call_fingerprint: None,
1384 tool_result_fingerprint: None,
1385 display_name: None,
1386 success: false,
1387 status,
1388 result: None,
1389 error: Some(error),
1390 duration_ms,
1391 capability_id: None,
1392 capability_name: None,
1393 narration: None,
1394 }
1395 }
1396
1397 pub fn with_display_name(mut self, display_name: Option<String>) -> Self {
1399 self.display_name = display_name;
1400 self
1401 }
1402
1403 pub fn with_fingerprints(
1404 mut self,
1405 tool_call_fingerprint: String,
1406 tool_result_fingerprint: String,
1407 ) -> Self {
1408 self.tool_call_fingerprint = Some(tool_call_fingerprint);
1409 self.tool_result_fingerprint = Some(tool_result_fingerprint);
1410 self
1411 }
1412
1413 pub fn with_narration(mut self, narration: Option<String>) -> Self {
1415 self.narration = narration;
1416 self
1417 }
1418
1419 pub fn with_capability_attribution(
1421 mut self,
1422 capability_id: Option<String>,
1423 capability_name: Option<String>,
1424 ) -> Self {
1425 self.capability_id = capability_id;
1426 self.capability_name = capability_name;
1427 self
1428 }
1429}
1430
1431#[derive(Debug, Clone, Serialize, Deserialize)]
1437#[cfg_attr(feature = "openapi", derive(ToSchema))]
1438pub struct ToolProgressData {
1439 pub tool_call_id: String,
1441
1442 pub tool_name: String,
1444
1445 pub message: String,
1447
1448 #[serde(default, skip_serializing_if = "Option::is_none")]
1450 pub display_name: Option<String>,
1451}
1452
1453#[derive(Debug, Clone, Serialize, Deserialize)]
1463#[cfg_attr(feature = "openapi", derive(ToSchema))]
1464pub struct ToolOutputDeltaData {
1465 pub tool_call_id: String,
1467
1468 pub tool_name: String,
1470
1471 pub delta: String,
1473
1474 pub stream: String,
1476}
1477
1478#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1480#[cfg_attr(feature = "openapi", derive(ToSchema))]
1481#[serde(rename_all = "snake_case")]
1482pub enum TranscriptRepairAction {
1483 Replay,
1485 Synthesize,
1487}
1488
1489#[derive(Debug, Clone, Serialize, Deserialize)]
1495#[cfg_attr(feature = "openapi", derive(ToSchema))]
1496pub struct TranscriptRepairedData {
1497 pub tool_call_id: String,
1499
1500 #[serde(default, skip_serializing_if = "Option::is_none")]
1502 pub tool_name: Option<String>,
1503
1504 pub action: TranscriptRepairAction,
1506}
1507
1508#[derive(Debug, Clone, Serialize, Deserialize)]
1514#[cfg_attr(feature = "openapi", derive(ToSchema))]
1515pub struct ToolCallRepairedData {
1516 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1518 pub turn_id: TurnId,
1519
1520 pub tool_call_id: String,
1522
1523 pub tool_name: String,
1525
1526 pub outcome: String,
1528}
1529
1530#[derive(Debug, Clone, Serialize, Deserialize)]
1535#[cfg_attr(feature = "openapi", derive(ToSchema))]
1536pub struct ToolCallRequestedData {
1537 pub tool_calls: Vec<ToolCall>,
1539 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1541 pub tool_summaries: Vec<ToolCallSummary>,
1542 #[serde(default, skip_serializing_if = "Option::is_none")]
1544 pub headline: Option<String>,
1545 #[serde(default, skip_serializing_if = "Option::is_none")]
1547 pub completed_headline: Option<String>,
1548}
1549
1550impl ToolCallRequestedData {
1551 pub fn with_definitions(
1552 tool_calls: &[ToolCall],
1553 tool_defs: &[crate::tool_types::ToolDefinition],
1554 ) -> Self {
1555 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
1556 }
1557
1558 pub fn with_definitions_and_locale(
1559 tool_calls: &[ToolCall],
1560 tool_defs: &[crate::tool_types::ToolDefinition],
1561 locale: Option<&str>,
1562 ) -> Self {
1563 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
1564 tool_defs.iter().map(|d| (d.name(), d)).collect();
1565
1566 let tool_summaries = tool_calls
1567 .iter()
1568 .map(|tool_call| {
1569 let tool_def = def_map.get(tool_call.name.as_str()).copied();
1570 ToolCallSummary {
1571 id: tool_call.id.clone(),
1572 name: tool_call.name.clone(),
1573 display_name: localized_tool_display_name(
1574 &tool_call.name,
1575 tool_def.and_then(|def| def.display_name()),
1576 locale,
1577 ),
1578 narration: Some(render_tool_narration_with_locale(
1579 tool_def,
1580 tool_call,
1581 ToolNarrationPhase::Waiting,
1582 locale,
1583 )),
1584 completed_narration: Some(render_tool_narration_with_locale(
1585 tool_def,
1586 tool_call,
1587 ToolNarrationPhase::Completed,
1588 locale,
1589 )),
1590 }
1591 })
1592 .collect();
1593
1594 Self {
1595 tool_calls: tool_calls.to_vec(),
1596 tool_summaries,
1597 headline: render_group_headline_with_locale(
1598 tool_calls,
1599 tool_defs,
1600 ToolNarrationPhase::Waiting,
1601 locale,
1602 ),
1603 completed_headline: render_group_headline_with_locale(
1604 tool_calls,
1605 tool_defs,
1606 ToolNarrationPhase::Completed,
1607 locale,
1608 ),
1609 }
1610 }
1611}
1612
1613#[derive(Debug, Clone, Serialize, Deserialize)]
1619#[cfg_attr(feature = "openapi", derive(ToSchema))]
1620pub struct LlmGenerationOutput {
1621 #[serde(skip_serializing_if = "Option::is_none")]
1623 pub text: Option<String>,
1624
1625 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1627 pub tool_calls: Vec<ToolCall>,
1628}
1629
1630#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1635#[cfg_attr(feature = "openapi", derive(ToSchema))]
1636pub struct LlmRequestOptions {
1637 #[serde(skip_serializing_if = "Option::is_none")]
1639 pub prompt_cache: Option<LlmPromptCacheInfo>,
1640 #[serde(skip_serializing_if = "Option::is_none")]
1642 pub tool_search: Option<LlmToolSearchInfo>,
1643 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1645 pub provider_options: HashMap<String, Value>,
1646 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1649 pub metadata: HashMap<String, String>,
1650}
1651
1652impl LlmRequestOptions {
1653 pub fn is_empty(&self) -> bool {
1654 self.prompt_cache.is_none()
1655 && self.tool_search.is_none()
1656 && self.provider_options.is_empty()
1657 && self.metadata.is_empty()
1658 }
1659}
1660
1661#[derive(Debug, Clone, Serialize, Deserialize)]
1663#[cfg_attr(feature = "openapi", derive(ToSchema))]
1664pub struct LlmPromptCacheInfo {
1665 pub enabled: bool,
1667 pub strategy: crate::driver_registry::PromptCacheStrategy,
1669 #[serde(skip_serializing_if = "Option::is_none")]
1671 pub provider_mode: Option<String>,
1672}
1673
1674#[derive(Debug, Clone, Serialize, Deserialize)]
1676#[cfg_attr(feature = "openapi", derive(ToSchema))]
1677pub struct LlmToolSearchInfo {
1678 pub enabled: bool,
1680 pub threshold: usize,
1682}
1683
1684#[derive(Debug, Clone, Serialize, Deserialize)]
1686#[cfg_attr(feature = "openapi", derive(ToSchema))]
1687pub struct LlmGenerationMetadata {
1688 #[cfg_attr(feature = "openapi", schema(example = "claude-sonnet-4-5"))]
1690 pub model: String,
1691
1692 #[serde(skip_serializing_if = "Option::is_none")]
1694 #[cfg_attr(feature = "openapi", schema(example = "anthropic"))]
1695 pub provider: Option<String>,
1696
1697 #[serde(skip_serializing_if = "Option::is_none")]
1699 pub usage: Option<TokenUsage>,
1700
1701 #[serde(skip_serializing_if = "Option::is_none")]
1703 #[cfg_attr(feature = "openapi", schema(example = 1_842u64))]
1704 pub duration_ms: Option<u64>,
1705
1706 #[serde(skip_serializing_if = "Option::is_none")]
1708 #[cfg_attr(feature = "openapi", schema(example = 312u64))]
1709 pub time_to_first_token_ms: Option<u64>,
1710
1711 #[cfg_attr(feature = "openapi", schema(example = true))]
1713 pub success: bool,
1714
1715 #[serde(skip_serializing_if = "Option::is_none")]
1717 #[cfg_attr(feature = "openapi", schema(example = "provider returned 503"))]
1718 pub error: Option<String>,
1719
1720 #[serde(skip_serializing_if = "Option::is_none")]
1723 #[cfg_attr(feature = "openapi", schema(example = json!(["tool_calls"])))]
1724 pub finish_reasons: Option<Vec<String>>,
1725
1726 #[serde(skip_serializing_if = "Option::is_none")]
1729 #[cfg_attr(feature = "openapi", schema(example = "msg_01ABCDef0123456789"))]
1730 pub response_id: Option<String>,
1731
1732 #[serde(skip_serializing_if = "Option::is_none")]
1735 pub retry: Option<LlmRetryInfo>,
1736
1737 #[serde(skip_serializing_if = "Option::is_none")]
1740 pub compaction: Option<LlmCompactionInfo>,
1741
1742 #[serde(skip_serializing_if = "Option::is_none")]
1744 pub request_options: Option<LlmRequestOptions>,
1745}
1746
1747#[derive(Debug, Clone, Serialize, Deserialize)]
1749#[cfg_attr(feature = "openapi", derive(ToSchema))]
1750pub struct LlmRetryInfo {
1751 pub attempts: u32,
1753
1754 pub total_wait_ms: u64,
1756}
1757
1758#[derive(Debug, Clone, Serialize, Deserialize)]
1763#[cfg_attr(feature = "openapi", derive(ToSchema))]
1764pub struct LlmCompactionInfo {
1765 pub compacted: bool,
1767
1768 #[serde(skip_serializing_if = "Option::is_none")]
1770 pub input_tokens_before: Option<u32>,
1771
1772 #[serde(skip_serializing_if = "Option::is_none")]
1774 pub input_tokens_after: Option<u32>,
1775
1776 #[serde(skip_serializing_if = "Option::is_none")]
1778 pub duration_ms: Option<u64>,
1779}
1780
1781impl LlmCompactionInfo {
1782 pub fn new(
1784 input_tokens_before: Option<u32>,
1785 input_tokens_after: Option<u32>,
1786 duration_ms: Option<u64>,
1787 ) -> Self {
1788 Self {
1789 compacted: true,
1790 input_tokens_before,
1791 input_tokens_after,
1792 duration_ms,
1793 }
1794 }
1795}
1796
1797#[derive(Debug, Clone, Serialize, Deserialize)]
1802#[cfg_attr(feature = "openapi", derive(ToSchema))]
1803pub struct LlmGenerationData {
1804 pub messages: Vec<Message>,
1806
1807 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1809 pub tools: Vec<ToolDefinitionSummary>,
1810
1811 pub output: LlmGenerationOutput,
1813
1814 pub metadata: LlmGenerationMetadata,
1816}
1817
1818impl LlmGenerationData {
1819 #[allow(clippy::too_many_arguments)]
1821 pub fn success(
1822 messages: Vec<Message>,
1823 tools: Vec<ToolDefinitionSummary>,
1824 text: Option<String>,
1825 tool_calls: Vec<ToolCall>,
1826 model: String,
1827 provider: Option<String>,
1828 usage: Option<TokenUsage>,
1829 duration_ms: Option<u64>,
1830 time_to_first_token_ms: Option<u64>,
1831 ) -> Self {
1832 let finish_reasons = if !tool_calls.is_empty() {
1834 Some(vec!["tool_calls".to_string()])
1835 } else {
1836 Some(vec!["stop".to_string()])
1837 };
1838
1839 Self {
1840 messages,
1841 tools,
1842 output: LlmGenerationOutput { text, tool_calls },
1843 metadata: LlmGenerationMetadata {
1844 model,
1845 provider,
1846 usage,
1847 duration_ms,
1848 time_to_first_token_ms,
1849 success: true,
1850 error: None,
1851 finish_reasons,
1852 response_id: None,
1853 retry: None,
1854 compaction: None,
1855 request_options: None,
1856 },
1857 }
1858 }
1859
1860 #[allow(clippy::too_many_arguments)]
1862 pub fn success_with_metadata(
1863 messages: Vec<Message>,
1864 tools: Vec<ToolDefinitionSummary>,
1865 text: Option<String>,
1866 tool_calls: Vec<ToolCall>,
1867 model: String,
1868 provider: Option<String>,
1869 usage: Option<TokenUsage>,
1870 duration_ms: Option<u64>,
1871 time_to_first_token_ms: Option<u64>,
1872 finish_reasons: Option<Vec<String>>,
1873 response_id: Option<String>,
1874 ) -> Self {
1875 Self {
1876 messages,
1877 tools,
1878 output: LlmGenerationOutput { text, tool_calls },
1879 metadata: LlmGenerationMetadata {
1880 model,
1881 provider,
1882 usage,
1883 duration_ms,
1884 time_to_first_token_ms,
1885 success: true,
1886 error: None,
1887 finish_reasons,
1888 response_id,
1889 retry: None,
1890 compaction: None,
1891 request_options: None,
1892 },
1893 }
1894 }
1895
1896 #[allow(clippy::too_many_arguments)]
1898 pub fn success_with_retry(
1899 messages: Vec<Message>,
1900 tools: Vec<ToolDefinitionSummary>,
1901 text: Option<String>,
1902 tool_calls: Vec<ToolCall>,
1903 model: String,
1904 provider: Option<String>,
1905 usage: Option<TokenUsage>,
1906 duration_ms: Option<u64>,
1907 time_to_first_token_ms: Option<u64>,
1908 finish_reasons: Option<Vec<String>>,
1909 response_id: Option<String>,
1910 retry: Option<LlmRetryInfo>,
1911 ) -> Self {
1912 Self {
1913 messages,
1914 tools,
1915 output: LlmGenerationOutput { text, tool_calls },
1916 metadata: LlmGenerationMetadata {
1917 model,
1918 provider,
1919 usage,
1920 duration_ms,
1921 time_to_first_token_ms,
1922 success: true,
1923 error: None,
1924 finish_reasons,
1925 response_id,
1926 retry,
1927 compaction: None,
1928 request_options: None,
1929 },
1930 }
1931 }
1932
1933 pub fn failure(
1935 messages: Vec<Message>,
1936 tools: Vec<ToolDefinitionSummary>,
1937 model: String,
1938 provider: Option<String>,
1939 error: String,
1940 duration_ms: Option<u64>,
1941 time_to_first_token_ms: Option<u64>,
1942 ) -> Self {
1943 Self {
1944 messages,
1945 tools,
1946 output: LlmGenerationOutput {
1947 text: None,
1948 tool_calls: vec![],
1949 },
1950 metadata: LlmGenerationMetadata {
1951 model,
1952 provider,
1953 usage: None,
1954 duration_ms,
1955 time_to_first_token_ms,
1956 success: false,
1957 error: Some(error),
1958 finish_reasons: Some(vec!["error".to_string()]),
1959 response_id: None,
1960 retry: None,
1961 compaction: None,
1962 request_options: None,
1963 },
1964 }
1965 }
1966
1967 pub fn with_compaction(mut self, compaction: LlmCompactionInfo) -> Self {
1971 self.metadata.compaction = Some(compaction);
1972 self
1973 }
1974
1975 pub fn with_retry(mut self, retry: LlmRetryInfo) -> Self {
1977 self.metadata.retry = Some(retry);
1978 self
1979 }
1980
1981 pub fn with_request_options(mut self, request_options: LlmRequestOptions) -> Self {
1983 if !request_options.is_empty() {
1984 self.metadata.request_options = Some(request_options);
1985 }
1986 self
1987 }
1988}
1989
1990#[derive(Debug, Clone, Serialize, Deserialize)]
2000#[cfg_attr(feature = "openapi", derive(ToSchema))]
2001pub struct ReasonThinkingStartedData {
2002 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2004 pub turn_id: TurnId,
2005
2006 #[serde(skip_serializing_if = "Option::is_none")]
2008 pub model: Option<String>,
2009}
2010
2011#[derive(Debug, Clone, Serialize, Deserialize)]
2017#[cfg_attr(feature = "openapi", derive(ToSchema))]
2018pub struct ReasonThinkingDeltaData {
2019 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2021 pub turn_id: TurnId,
2022
2023 pub delta: String,
2025
2026 pub accumulated: String,
2028}
2029
2030#[derive(Debug, Clone, Serialize, Deserialize)]
2035#[cfg_attr(feature = "openapi", derive(ToSchema))]
2036pub struct ReasonThinkingCompletedData {
2037 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2039 pub turn_id: TurnId,
2040
2041 pub thinking: String,
2043}
2044
2045#[derive(Debug, Clone, Serialize, Deserialize)]
2053#[cfg_attr(feature = "openapi", derive(ToSchema))]
2054pub struct ReasonItemData {
2055 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2057 pub turn_id: TurnId,
2058
2059 pub provider: String,
2061
2062 #[serde(skip_serializing_if = "Option::is_none")]
2064 pub model: Option<String>,
2065
2066 pub item_id: String,
2068
2069 #[serde(skip_serializing_if = "Option::is_none")]
2071 pub encrypted_content: Option<String>,
2072
2073 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2076 pub summary: Vec<String>,
2077
2078 #[serde(skip_serializing_if = "Option::is_none")]
2080 pub token_count: Option<u32>,
2081}
2082
2083#[derive(Debug, Clone, Serialize, Deserialize)]
2089#[cfg_attr(feature = "openapi", derive(ToSchema))]
2090pub struct TurnStartedData {
2091 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2093 pub turn_id: TurnId,
2094
2095 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
2097 pub input_message_id: MessageId,
2098
2099 #[serde(skip_serializing_if = "Option::is_none")]
2101 pub input_content: Option<String>,
2102}
2103
2104#[derive(Debug, Clone, Serialize, Deserialize)]
2106#[cfg_attr(feature = "openapi", derive(ToSchema))]
2107pub struct TurnCompletedData {
2108 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2110 pub turn_id: TurnId,
2111
2112 pub iterations: u32,
2114
2115 #[serde(skip_serializing_if = "Option::is_none")]
2117 pub duration_ms: Option<u64>,
2118
2119 #[serde(skip_serializing_if = "Option::is_none")]
2121 pub usage: Option<TokenUsage>,
2122
2123 #[serde(skip_serializing_if = "Option::is_none")]
2125 pub input_content: Option<String>,
2126
2127 #[serde(skip_serializing_if = "Option::is_none")]
2129 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "message_01933b5a00007000800000000000001"))]
2130 pub final_message_id: Option<MessageId>,
2131
2132 #[serde(skip_serializing_if = "Option::is_none")]
2134 pub final_answer_preview: Option<String>,
2135
2136 #[serde(skip_serializing_if = "Option::is_none")]
2138 pub time_to_first_token_ms: Option<u64>,
2139
2140 #[serde(skip_serializing_if = "Option::is_none")]
2142 pub tool_call_count: Option<u32>,
2143
2144 #[serde(skip_serializing_if = "Option::is_none")]
2146 pub llm_call_count: Option<u32>,
2147
2148 #[serde(skip_serializing_if = "Option::is_none")]
2150 pub status: Option<String>,
2151}
2152
2153#[derive(Debug, Clone, Serialize, Deserialize)]
2155#[cfg_attr(feature = "openapi", derive(ToSchema))]
2156pub struct TurnFailedData {
2157 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2159 pub turn_id: TurnId,
2160
2161 pub error: String,
2163
2164 #[serde(default, skip_serializing_if = "Option::is_none")]
2166 pub error_code: Option<String>,
2167
2168 #[serde(default, skip_serializing_if = "Option::is_none")]
2170 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
2171 pub error_fields: Option<UserFacingErrorFields>,
2172
2173 #[serde(default, skip_serializing_if = "Option::is_none")]
2177 pub error_disclosure: Option<String>,
2178}
2179
2180#[derive(Debug, Clone, Serialize, Deserialize)]
2187#[cfg_attr(feature = "openapi", derive(ToSchema))]
2188pub struct TurnSealedData {
2189 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2191 pub turn_id: TurnId,
2192
2193 pub reason: String,
2196
2197 #[serde(default, skip_serializing_if = "Option::is_none")]
2199 pub detail: Option<String>,
2200
2201 #[serde(default, skip_serializing_if = "Option::is_none")]
2203 pub iterations: Option<u32>,
2204
2205 #[serde(default, skip_serializing_if = "Option::is_none")]
2207 pub usage: Option<TokenUsage>,
2208}
2209
2210#[derive(Debug, Clone, Serialize, Deserialize)]
2212#[cfg_attr(feature = "openapi", derive(ToSchema))]
2213pub struct TurnCancelledData {
2214 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2216 pub turn_id: TurnId,
2217
2218 #[serde(skip_serializing_if = "Option::is_none")]
2220 pub reason: Option<String>,
2221
2222 #[serde(skip_serializing_if = "Option::is_none")]
2224 pub usage: Option<TokenUsage>,
2225}
2226
2227#[derive(Debug, Clone, Serialize, Deserialize)]
2233#[cfg_attr(feature = "openapi", derive(ToSchema))]
2234pub struct SessionStartedData {
2235 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "harness_01933b5a00007000800000000000001"))]
2237 pub harness_id: HarnessId,
2238
2239 #[serde(skip_serializing_if = "Option::is_none")]
2241 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agent_01933b5a00007000800000000000001"))]
2242 pub agent_id: Option<AgentId>,
2243
2244 #[serde(skip_serializing_if = "Option::is_none")]
2246 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
2247 pub model_id: Option<ModelId>,
2248}
2249
2250#[derive(Debug, Clone, Serialize, Deserialize)]
2252#[cfg_attr(feature = "openapi", derive(ToSchema))]
2253pub struct SessionActivatedData {
2254 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2256 pub turn_id: TurnId,
2257
2258 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
2260 pub input_message_id: MessageId,
2261}
2262
2263#[derive(Debug, Clone, Serialize, Deserialize)]
2265#[cfg_attr(feature = "openapi", derive(ToSchema))]
2266pub struct SessionIdledData {
2267 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2269 pub turn_id: TurnId,
2270
2271 #[serde(skip_serializing_if = "Option::is_none")]
2273 pub iterations: Option<u32>,
2274
2275 #[serde(skip_serializing_if = "Option::is_none")]
2277 pub usage: Option<TokenUsage>,
2278}
2279
2280#[derive(Debug, Clone, Serialize, Deserialize)]
2282#[cfg_attr(feature = "openapi", derive(ToSchema))]
2283pub struct SessionTitleUpdatedData {
2284 pub previous_title: Option<String>,
2286
2287 pub title: String,
2289}
2290
2291#[derive(Debug, Clone, Serialize, Deserialize)]
2300#[cfg_attr(feature = "openapi", derive(ToSchema))]
2301pub struct SessionTaskEventData {
2302 pub task: crate::session_task::SessionTask,
2303}
2304
2305#[derive(Debug, Clone, Serialize, Deserialize)]
2307#[cfg_attr(feature = "openapi", derive(ToSchema))]
2308pub struct TaskMessageEventData {
2309 pub task_id: String,
2310 pub message: crate::session_task::TaskMessage,
2311}
2312
2313#[derive(Debug, Clone, Serialize, Deserialize)]
2319#[cfg_attr(feature = "openapi", derive(ToSchema))]
2320#[serde(rename_all = "snake_case")]
2321pub enum CompactionReason {
2322 ProactiveBudget,
2324 RequestTooLarge,
2326 Manual,
2328}
2329
2330impl std::fmt::Display for CompactionReason {
2331 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2332 match self {
2333 Self::ProactiveBudget => write!(f, "proactive_budget"),
2334 Self::RequestTooLarge => write!(f, "request_too_large"),
2335 Self::Manual => write!(f, "manual"),
2336 }
2337 }
2338}
2339
2340#[derive(Debug, Clone, Serialize, Deserialize)]
2342#[cfg_attr(feature = "openapi", derive(ToSchema))]
2343pub struct ContextCompactingData {
2344 pub reason: CompactionReason,
2346 pub strategy: String,
2348 pub messages_before: usize,
2350 #[serde(default, skip_serializing_if = "Option::is_none")]
2352 pub tokens_before: Option<u64>,
2353 #[serde(default, skip_serializing_if = "Option::is_none")]
2355 pub bytes_before: Option<u64>,
2356}
2357
2358#[derive(Debug, Clone, Serialize, Deserialize)]
2360#[cfg_attr(feature = "openapi", derive(ToSchema))]
2361pub struct CompactionStepData {
2362 pub strategy: String,
2364 pub messages_after: usize,
2366 pub duration_ms: u64,
2368}
2369
2370#[derive(Debug, Clone, Serialize, Deserialize)]
2372#[cfg_attr(feature = "openapi", derive(ToSchema))]
2373pub struct ContextCompactedData {
2374 #[serde(default, skip_serializing_if = "Option::is_none")]
2376 pub checkpoint_id: Option<String>,
2377 pub strategy_used: String,
2379 pub messages_before: usize,
2381 pub messages_after: usize,
2383 #[serde(default, skip_serializing_if = "Option::is_none")]
2385 pub tokens_before: Option<u64>,
2386 #[serde(default, skip_serializing_if = "Option::is_none")]
2388 pub tokens_after: Option<u64>,
2389 #[serde(default, skip_serializing_if = "Option::is_none")]
2391 pub bytes_before: Option<u64>,
2392 #[serde(default, skip_serializing_if = "Option::is_none")]
2394 pub bytes_after: Option<u64>,
2395 pub duration_ms: u64,
2397 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2399 pub steps: Vec<CompactionStepData>,
2400}
2401
2402#[derive(Debug, Clone, Serialize, Deserialize)]
2408#[cfg_attr(feature = "openapi", derive(ToSchema))]
2409pub struct FileWrittenData {
2410 pub path: String,
2412 pub operation: String,
2414 pub size_bytes: i64,
2416 pub created: bool,
2418}
2419
2420pub const FILE_OP_CREATE: &str = "create";
2422pub const FILE_OP_UPDATE: &str = "update";
2423
2424#[derive(Debug, Clone, Serialize, Deserialize)]
2430#[cfg_attr(feature = "openapi", derive(ToSchema))]
2431pub struct BudgetEventData {
2432 pub budget_id: String,
2434 pub balance: f64,
2436 pub limit: f64,
2438 pub currency: String,
2440 #[serde(skip_serializing_if = "Option::is_none")]
2442 pub message: Option<String>,
2443 #[serde(skip_serializing_if = "Option::is_none")]
2445 pub soft_limit: Option<f64>,
2446}
2447
2448#[derive(Debug, Clone, Serialize, Deserialize)]
2454#[cfg_attr(feature = "openapi", derive(ToSchema))]
2455pub struct VoiceSessionStartedData {
2456 #[cfg_attr(
2458 feature = "openapi",
2459 schema(example = "voice_01933b5a00007000800000000000001")
2460 )]
2461 pub voice_connection_id: String,
2462 #[cfg_attr(feature = "openapi", schema(example = "gpt-realtime"))]
2464 pub model: String,
2465 #[cfg_attr(feature = "openapi", schema(example = "alloy"))]
2467 pub voice: String,
2468 #[cfg_attr(feature = "openapi", schema(example = "medium"))]
2470 pub reasoning_effort: String,
2471 #[cfg_attr(feature = "openapi", schema(example = "webrtc"))]
2473 pub transport: String,
2474}
2475
2476#[derive(Debug, Clone, Serialize, Deserialize)]
2478#[cfg_attr(feature = "openapi", derive(ToSchema))]
2479pub struct VoiceTranscriptData {
2480 pub voice_connection_id: String,
2482 #[serde(default, skip_serializing_if = "Option::is_none")]
2484 pub item_id: Option<String>,
2485 #[serde(default, skip_serializing_if = "Option::is_none")]
2487 pub response_id: Option<String>,
2488 #[serde(default, skip_serializing_if = "Option::is_none")]
2490 pub phase: Option<String>,
2491 #[serde(default, skip_serializing_if = "String::is_empty")]
2493 pub delta: String,
2494 pub accumulated: String,
2496}
2497
2498#[derive(Debug, Clone, Serialize, Deserialize)]
2500#[cfg_attr(feature = "openapi", derive(ToSchema))]
2501pub struct VoiceSessionEndedData {
2502 #[cfg_attr(
2504 feature = "openapi",
2505 schema(example = "voice_01933b5a00007000800000000000001")
2506 )]
2507 pub voice_connection_id: String,
2508 #[serde(default, skip_serializing_if = "Option::is_none")]
2510 #[cfg_attr(
2511 feature = "openapi",
2512 schema(example = "User hung up after refund confirmed.")
2513 )]
2514 pub reason: Option<String>,
2515 #[serde(default, skip_serializing_if = "Option::is_none")]
2518 #[cfg_attr(feature = "openapi", schema(example = 184_500_u64))]
2519 pub duration_ms: Option<u64>,
2520}
2521
2522#[derive(Debug, Clone, Serialize, Deserialize)]
2524#[cfg_attr(feature = "openapi", derive(ToSchema))]
2525pub struct VoiceSessionFailedData {
2526 #[cfg_attr(
2528 feature = "openapi",
2529 schema(example = "voice_01933b5a00007000800000000000001")
2530 )]
2531 pub voice_connection_id: String,
2532 #[cfg_attr(
2534 feature = "openapi",
2535 schema(example = "realtime provider closed stream: 1011 internal_error")
2536 )]
2537 pub error: String,
2538}
2539
2540#[derive(Debug, Clone, Serialize)]
2588#[serde(untagged)]
2589#[cfg_attr(feature = "openapi", derive(ToSchema))]
2590#[cfg_attr(feature = "openapi", schema(
2591 title = "EventData",
2592 description = "Event-specific payload. The schema depends on the event type field.",
2593 example = json!({"message": {"id": "...", "role": "user", "content": []}})
2594))]
2595pub enum EventData {
2596 InputMessage(InputMessageData),
2598
2599 OutputMessageDelta(OutputMessageDeltaData),
2601 OutputMessageStarted(OutputMessageStartedData),
2602 OutputMessageReplaced(OutputMessageReplacedData),
2603 OutputMessageCompleted(OutputMessageCompletedData),
2604
2605 TurnStarted(TurnStartedData),
2607 TurnCompleted(TurnCompletedData),
2608 TurnFailed(TurnFailedData),
2609
2610 ReasonStarted(ReasonStartedData),
2612 ReasonCompleted(ReasonCompletedData),
2613 ReasonRecovered(ReasonRecoveredData),
2614 CapabilityUsage(CapabilityUsageData),
2615 ActStarted(ActStartedData),
2616 ActCompleted(ActCompletedData),
2617 ToolStarted(ToolStartedData),
2618 ToolCompleted(ToolCompletedData),
2619 ToolProgress(ToolProgressData),
2620 ToolOutputDelta(ToolOutputDeltaData),
2621 ToolCallRequested(ToolCallRequestedData),
2622
2623 TranscriptRepaired(TranscriptRepairedData),
2625 ToolCallRepaired(ToolCallRepairedData),
2626
2627 LlmGeneration(LlmGenerationData),
2629
2630 ReasonThinkingDelta(ReasonThinkingDeltaData),
2632 ReasonItem(ReasonItemData),
2633 ReasonThinkingStarted(ReasonThinkingStartedData),
2634 ReasonThinkingCompleted(ReasonThinkingCompletedData),
2635
2636 TurnSealed(TurnSealedData),
2637 TurnCancelled(TurnCancelledData),
2638
2639 SessionStarted(SessionStartedData),
2641 SessionActivated(SessionActivatedData),
2642 SessionIdled(SessionIdledData),
2643 SessionTitleUpdated(SessionTitleUpdatedData),
2644
2645 TaskCreated(SessionTaskEventData),
2647 TaskUpdated(SessionTaskEventData),
2648 TaskMessageSent(TaskMessageEventData),
2649 TaskMessageReceived(TaskMessageEventData),
2650
2651 ContextCompacting(ContextCompactingData),
2653 ContextCompacted(ContextCompactedData),
2654
2655 FileWritten(FileWrittenData),
2657
2658 BudgetWarning(BudgetEventData),
2660 BudgetPaused(BudgetEventData),
2661 BudgetExhausted(BudgetEventData),
2662 BudgetResumed(BudgetEventData),
2663
2664 VoiceSessionStarted(VoiceSessionStartedData),
2666 VoiceInputTranscriptDelta(VoiceTranscriptData),
2667 VoiceInputTranscriptCompleted(VoiceTranscriptData),
2668 VoiceOutputTranscriptDelta(VoiceTranscriptData),
2669 VoiceOutputTranscriptCompleted(VoiceTranscriptData),
2670 VoiceSessionEnded(VoiceSessionEndedData),
2671 VoiceSessionFailed(VoiceSessionFailedData),
2672
2673 #[serde(skip)]
2677 Unsupported {
2678 event_type: String,
2680 data: serde_json::Value,
2682 },
2683}
2684
2685impl EventData {
2686 pub fn is_unsupported(&self) -> bool {
2689 matches!(self, EventData::Unsupported { .. })
2690 }
2691
2692 pub fn unsupported(event_type: String, data: serde_json::Value) -> Self {
2695 tracing::warn!(
2696 event_type = %event_type,
2697 "Encountered unsupported event type - will be filtered from API responses"
2698 );
2699 EventData::Unsupported { event_type, data }
2700 }
2701}
2702
2703macro_rules! event_data_kinds {
2717 ($( $variant:ident($data:ty) = $type_const:path ),+ $(,)?) => {
2718 impl EventData {
2719 pub fn event_type(&self) -> &'static str {
2722 match self {
2723 $( EventData::$variant(_) => $type_const, )+
2724 EventData::Unsupported { .. } => "unsupported",
2725 }
2726 }
2727 }
2728
2729 pub fn deserialize_event_data(event_type: &str, data: serde_json::Value) -> EventData {
2741 let result = match event_type {
2742 $(
2743 $type_const => serde_json::from_value::<$data>(data.clone())
2744 .map(EventData::$variant),
2745 )+
2746 _ => return EventData::unsupported(event_type.to_string(), data),
2747 };
2748
2749 result.unwrap_or_else(|e| {
2750 tracing::warn!(
2751 event_type = %event_type,
2752 error = %e,
2753 "Failed to deserialize known event type - treating as unsupported"
2754 );
2755 EventData::Unsupported {
2756 event_type: event_type.to_string(),
2757 data,
2758 }
2759 })
2760 }
2761 };
2762}
2763
2764event_data_kinds! {
2765 InputMessage(InputMessageData) = INPUT_MESSAGE,
2767
2768 OutputMessageStarted(OutputMessageStartedData) = OUTPUT_MESSAGE_STARTED,
2770 OutputMessageDelta(OutputMessageDeltaData) = OUTPUT_MESSAGE_DELTA,
2771 OutputMessageReplaced(OutputMessageReplacedData) = OUTPUT_MESSAGE_REPLACED,
2772 OutputMessageCompleted(OutputMessageCompletedData) = OUTPUT_MESSAGE_COMPLETED,
2773
2774 TurnStarted(TurnStartedData) = TURN_STARTED,
2776 TurnCompleted(TurnCompletedData) = TURN_COMPLETED,
2777 TurnFailed(TurnFailedData) = TURN_FAILED,
2778 TurnSealed(TurnSealedData) = TURN_SEALED,
2779 TurnCancelled(TurnCancelledData) = TURN_CANCELLED,
2780
2781 ReasonStarted(ReasonStartedData) = REASON_STARTED,
2783 ReasonCompleted(ReasonCompletedData) = REASON_COMPLETED,
2784 ReasonRecovered(ReasonRecoveredData) = REASON_RECOVERED,
2785 CapabilityUsage(CapabilityUsageData) = CAPABILITY_USAGE,
2786 ActStarted(ActStartedData) = ACT_STARTED,
2787 ActCompleted(ActCompletedData) = ACT_COMPLETED,
2788 ToolStarted(ToolStartedData) = TOOL_STARTED,
2789 ToolCompleted(ToolCompletedData) = TOOL_COMPLETED,
2790 ToolProgress(ToolProgressData) = TOOL_PROGRESS,
2791 ToolOutputDelta(ToolOutputDeltaData) = TOOL_OUTPUT_DELTA,
2792 ToolCallRequested(ToolCallRequestedData) = TOOL_CALL_REQUESTED,
2793
2794 TranscriptRepaired(TranscriptRepairedData) = TRANSCRIPT_REPAIRED,
2796 ToolCallRepaired(ToolCallRepairedData) = TOOL_CALL_REPAIRED,
2797
2798 LlmGeneration(LlmGenerationData) = LLM_GENERATION,
2800
2801 ReasonThinkingStarted(ReasonThinkingStartedData) = REASON_THINKING_STARTED,
2803 ReasonThinkingDelta(ReasonThinkingDeltaData) = REASON_THINKING_DELTA,
2804 ReasonThinkingCompleted(ReasonThinkingCompletedData) = REASON_THINKING_COMPLETED,
2805 ReasonItem(ReasonItemData) = REASON_ITEM,
2806
2807 SessionStarted(SessionStartedData) = SESSION_STARTED,
2809 SessionActivated(SessionActivatedData) = SESSION_ACTIVATED,
2810 SessionIdled(SessionIdledData) = SESSION_IDLED,
2811 SessionTitleUpdated(SessionTitleUpdatedData) = SESSION_TITLE_UPDATED,
2812
2813 ContextCompacting(ContextCompactingData) = CONTEXT_COMPACTING,
2815 ContextCompacted(ContextCompactedData) = CONTEXT_COMPACTED,
2816
2817 FileWritten(FileWrittenData) = FILE_WRITTEN,
2819
2820 BudgetWarning(BudgetEventData) = BUDGET_WARNING,
2822 BudgetPaused(BudgetEventData) = BUDGET_PAUSED,
2823 BudgetExhausted(BudgetEventData) = BUDGET_EXHAUSTED,
2824 BudgetResumed(BudgetEventData) = BUDGET_RESUMED,
2825
2826 VoiceSessionStarted(VoiceSessionStartedData) = VOICE_SESSION_STARTED,
2828 VoiceInputTranscriptDelta(VoiceTranscriptData) = VOICE_INPUT_TRANSCRIPT_DELTA,
2829 VoiceInputTranscriptCompleted(VoiceTranscriptData) = VOICE_INPUT_TRANSCRIPT_COMPLETED,
2830 VoiceOutputTranscriptDelta(VoiceTranscriptData) = VOICE_OUTPUT_TRANSCRIPT_DELTA,
2831 VoiceOutputTranscriptCompleted(VoiceTranscriptData) = VOICE_OUTPUT_TRANSCRIPT_COMPLETED,
2832 VoiceSessionEnded(VoiceSessionEndedData) = VOICE_SESSION_ENDED,
2833 VoiceSessionFailed(VoiceSessionFailedData) = VOICE_SESSION_FAILED,
2834
2835 TaskCreated(SessionTaskEventData) = TASK_CREATED,
2837 TaskUpdated(SessionTaskEventData) = TASK_UPDATED,
2838 TaskMessageSent(TaskMessageEventData) = TASK_MESSAGE_SENT,
2839 TaskMessageReceived(TaskMessageEventData) = TASK_MESSAGE_RECEIVED,
2840}
2841
2842macro_rules! impl_from_event_data {
2846 ($($data_type:ty => $variant:ident),* $(,)?) => {
2847 $(
2848 impl From<$data_type> for EventData {
2849 fn from(data: $data_type) -> Self {
2850 EventData::$variant(data)
2851 }
2852 }
2853 )*
2854 };
2855}
2856
2857impl_from_event_data! {
2859 InputMessageData => InputMessage,
2860 OutputMessageStartedData => OutputMessageStarted,
2861 OutputMessageDeltaData => OutputMessageDelta,
2862 OutputMessageReplacedData => OutputMessageReplaced,
2863 OutputMessageCompletedData => OutputMessageCompleted,
2864 TurnStartedData => TurnStarted,
2865 TurnCompletedData => TurnCompleted,
2866 TurnFailedData => TurnFailed,
2867 TurnSealedData => TurnSealed,
2868 TurnCancelledData => TurnCancelled,
2869 ReasonStartedData => ReasonStarted,
2870 ReasonCompletedData => ReasonCompleted,
2871 ReasonRecoveredData => ReasonRecovered,
2872 CapabilityUsageData => CapabilityUsage,
2873 ActStartedData => ActStarted,
2874 ActCompletedData => ActCompleted,
2875 ToolStartedData => ToolStarted,
2876 ToolCompletedData => ToolCompleted,
2877 ToolProgressData => ToolProgress,
2878 ToolOutputDeltaData => ToolOutputDelta,
2879 ToolCallRequestedData => ToolCallRequested,
2880 TranscriptRepairedData => TranscriptRepaired,
2881 ToolCallRepairedData => ToolCallRepaired,
2882 LlmGenerationData => LlmGeneration,
2883 ReasonThinkingStartedData => ReasonThinkingStarted,
2884 ReasonThinkingDeltaData => ReasonThinkingDelta,
2885 ReasonThinkingCompletedData => ReasonThinkingCompleted,
2886 ReasonItemData => ReasonItem,
2887 SessionStartedData => SessionStarted,
2888 SessionActivatedData => SessionActivated,
2889 SessionIdledData => SessionIdled,
2890 SessionTitleUpdatedData => SessionTitleUpdated,
2891 ContextCompactingData => ContextCompacting,
2892 ContextCompactedData => ContextCompacted,
2893 FileWrittenData => FileWritten,
2894 VoiceSessionStartedData => VoiceSessionStarted,
2895 VoiceSessionEndedData => VoiceSessionEnded,
2896 VoiceSessionFailedData => VoiceSessionFailed,
2897}
2898
2899impl EventData {
2900 pub fn voice_transcript_event(data: VoiceTranscriptData, event_type: &str) -> Self {
2901 match event_type {
2902 VOICE_INPUT_TRANSCRIPT_DELTA => EventData::VoiceInputTranscriptDelta(data),
2903 VOICE_INPUT_TRANSCRIPT_COMPLETED => EventData::VoiceInputTranscriptCompleted(data),
2904 VOICE_OUTPUT_TRANSCRIPT_DELTA => EventData::VoiceOutputTranscriptDelta(data),
2905 VOICE_OUTPUT_TRANSCRIPT_COMPLETED => EventData::VoiceOutputTranscriptCompleted(data),
2906 _ => EventData::unsupported(
2907 event_type.to_string(),
2908 serde_json::to_value(&data).unwrap_or(serde_json::Value::Null),
2909 ),
2910 }
2911 }
2912}
2913
2914impl EventData {
2917 pub fn budget_event(data: BudgetEventData, event_type: &str) -> Self {
2918 match event_type {
2919 BUDGET_WARNING => EventData::BudgetWarning(data),
2920 BUDGET_PAUSED => EventData::BudgetPaused(data),
2921 BUDGET_EXHAUSTED => EventData::BudgetExhausted(data),
2922 BUDGET_RESUMED => EventData::BudgetResumed(data),
2923 _ => EventData::unsupported(
2924 event_type.to_string(),
2925 serde_json::to_value(&data).unwrap_or(serde_json::Value::Null),
2926 ),
2927 }
2928 }
2929}
2930
2931#[derive(Debug, Clone, Serialize)]
2941#[cfg_attr(feature = "openapi", derive(ToSchema))]
2942pub struct EventRequest {
2943 #[serde(rename = "type")]
2945 pub event_type: String,
2946
2947 pub ts: DateTime<Utc>,
2949
2950 pub session_id: SessionId,
2952
2953 pub context: EventContext,
2955
2956 pub data: EventData,
2958
2959 #[serde(skip_serializing_if = "Option::is_none")]
2961 pub metadata: Option<serde_json::Value>,
2962
2963 #[serde(skip_serializing_if = "Option::is_none")]
2965 pub tags: Option<Vec<String>>,
2966}
2967
2968#[derive(Debug, Deserialize)]
2969struct RawEventRequest {
2970 #[serde(rename = "type")]
2971 event_type: String,
2972 ts: DateTime<Utc>,
2973 session_id: SessionId,
2974 context: EventContext,
2975 data: serde_json::Value,
2976 metadata: Option<serde_json::Value>,
2977 tags: Option<Vec<String>>,
2978}
2979
2980impl<'de> Deserialize<'de> for EventRequest {
2981 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2982 where
2983 D: Deserializer<'de>,
2984 {
2985 let raw = RawEventRequest::deserialize(deserializer)?;
2986 let data = deserialize_event_data(&raw.event_type, raw.data);
2987 Ok(Self {
2988 event_type: raw.event_type,
2989 ts: raw.ts,
2990 session_id: raw.session_id,
2991 context: raw.context,
2992 data,
2993 metadata: raw.metadata,
2994 tags: raw.tags,
2995 })
2996 }
2997}
2998
2999impl EventRequest {
3000 pub fn new(session_id: SessionId, context: EventContext, data: impl Into<EventData>) -> Self {
3004 let data = data.into();
3005 let event_type = data.event_type().to_string();
3006 Self {
3007 event_type,
3008 ts: Utc::now(),
3009 session_id,
3010 context,
3011 data,
3012 metadata: None,
3013 tags: None,
3014 }
3015 }
3016
3017 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
3019 self.metadata = Some(metadata);
3020 self
3021 }
3022
3023 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
3025 self.tags = Some(tags);
3026 self
3027 }
3028
3029 pub fn is_ephemeral(&self) -> bool {
3037 is_ephemeral_event_type(&self.event_type)
3038 }
3039
3040 pub fn into_event(self, id: EventId, sequence: i32) -> Event {
3042 Event {
3043 id,
3044 event_type: self.event_type,
3045 ts: self.ts,
3046 session_id: self.session_id,
3047 context: self.context,
3048 data: self.data,
3049 metadata: self.metadata,
3050 tags: self.tags,
3051 sequence: Some(sequence),
3052 }
3053 }
3054}
3055
3056pub struct EventBuilder {
3062 session_id: SessionId,
3063 context: EventContext,
3064}
3065
3066impl EventBuilder {
3067 pub fn new(session_id: SessionId) -> Self {
3068 Self {
3069 session_id,
3070 context: EventContext::empty(),
3071 }
3072 }
3073
3074 pub fn with_turn(mut self, turn_id: TurnId, input_message_id: MessageId) -> Self {
3075 self.context.turn_id = Some(turn_id);
3076 self.context.input_message_id = Some(input_message_id);
3077 self
3078 }
3079
3080 pub fn with_exec(mut self, exec_id: ExecId) -> Self {
3081 self.context.exec_id = Some(exec_id);
3082 self
3083 }
3084
3085 pub fn build(self, data: impl Into<EventData>) -> Event {
3086 Event::new(self.session_id, self.context, data)
3087 }
3088}
3089
3090#[cfg(test)]
3095mod tests {
3096 use super::*;
3097 use crate::driver_registry::PromptCacheStrategy;
3098 use serde_json::json;
3099 use std::collections::HashMap;
3100
3101 #[test]
3102 fn test_event_creation() {
3103 let session_id = SessionId::new();
3104 let context = EventContext::empty();
3105 let data = InputMessageData::new(Message::user("test"));
3106
3107 let event = Event::new(session_id, context, data);
3108
3109 assert_eq!(event.event_type, "input.message");
3110 assert_eq!(event.session_uuid(), session_id.uuid());
3111 assert!(event.is_input_event());
3112 assert!(event.is_message_event());
3113 }
3114
3115 #[test]
3116 fn test_event_context_from_atom_context() {
3117 let session_id = SessionId::new();
3118 let turn_id = TurnId::new();
3119 let input_message_id = MessageId::new();
3120
3121 let atom_ctx = AtomContext::new(session_id, turn_id, input_message_id);
3122 let context = EventContext::from_atom_context(&atom_ctx);
3123
3124 assert_eq!(context.turn_id, Some(turn_id));
3125 assert_eq!(context.input_message_id, Some(input_message_id));
3126 assert_eq!(context.exec_id, Some(atom_ctx.exec_id));
3127 }
3128
3129 #[test]
3130 fn test_event_serialization() {
3131 let session_id = SessionId::new();
3132 let context = EventContext::empty();
3133 let event = Event::new(
3134 session_id,
3135 context,
3136 InputMessageData::new(Message::user("test")),
3137 );
3138
3139 let json = serde_json::to_string(&event).unwrap();
3140
3141 assert!(json.contains("\"type\":\"input.message\""));
3142 assert!(json.contains("\"session_id\""));
3143 assert!(json.contains("\"context\""));
3144 assert!(json.contains("\"data\""));
3145 }
3146
3147 #[test]
3148 fn transcript_repaired_is_valid_filter_event_type() {
3149 assert!(VALID_EVENT_TYPES.contains(&TRANSCRIPT_REPAIRED));
3150 }
3151
3152 #[test]
3156 fn capability_usage_is_valid_filter_event_type() {
3157 assert!(VALID_EVENT_TYPES.contains(&CAPABILITY_USAGE));
3158 }
3159
3160 #[test]
3161 fn test_event_builder() {
3162 let session_id = SessionId::new();
3163 let turn_id = TurnId::new();
3164 let input_message_id = MessageId::new();
3165 let exec_id = ExecId::new();
3166
3167 let event = EventBuilder::new(session_id)
3168 .with_turn(turn_id, input_message_id)
3169 .with_exec(exec_id)
3170 .build(ReasonStartedData {
3171 harness_id: HarnessId::from_seed(1),
3172 agent_id: Some(AgentId::new()),
3173 metadata: Some(ModelMetadata {
3174 model: "gpt-4o".to_string(),
3175 model_id: None,
3176 provider_id: None,
3177 }),
3178 });
3179
3180 assert_eq!(event.event_type, "reason.started");
3181 assert_eq!(event.session_id, session_id);
3182 assert_eq!(event.context.turn_id, Some(turn_id));
3183 assert_eq!(event.context.exec_id, Some(exec_id));
3184 }
3185
3186 #[test]
3187 fn test_reason_completed_data() {
3188 let data = ReasonCompletedData::success("Hello world", true, 2, Some(1000), None);
3189 assert!(data.success);
3190 assert_eq!(data.text_preview, Some("Hello world".to_string()));
3191 assert!(data.has_tool_calls);
3192 assert_eq!(data.tool_call_count, 2);
3193 assert_eq!(data.duration_ms, Some(1000));
3194 assert!(data.usage.is_none());
3195
3196 let data = ReasonCompletedData::failure("Network error".to_string(), Some(500));
3197 assert!(!data.success);
3198 assert_eq!(data.error, Some("Network error".to_string()));
3199 assert_eq!(data.duration_ms, Some(500));
3200 }
3201
3202 #[test]
3203 fn test_input_output_event_types() {
3204 assert_eq!(INPUT_MESSAGE, "input.message");
3205 assert_eq!(OUTPUT_MESSAGE_STARTED, "output.message.started");
3206 assert_eq!(OUTPUT_MESSAGE_DELTA, "output.message.delta");
3207 assert_eq!(OUTPUT_MESSAGE_COMPLETED, "output.message.completed");
3208 }
3209
3210 #[test]
3211 fn test_turn_event_types() {
3212 assert_eq!(TURN_STARTED, "turn.started");
3213 assert_eq!(TURN_COMPLETED, "turn.completed");
3214 assert_eq!(TURN_FAILED, "turn.failed");
3215 assert_eq!(TURN_CANCELLED, "turn.cancelled");
3216 }
3217
3218 #[test]
3219 fn test_turn_cancelled_data() {
3220 let data = TurnCancelledData {
3221 turn_id: TurnId::from_uuid(Uuid::now_v7()),
3222 reason: Some("User requested cancellation".to_string()),
3223 usage: Some(TokenUsage::new(100, 50)),
3224 };
3225
3226 let event_data: EventData = data.into();
3227 assert_eq!(event_data.event_type(), TURN_CANCELLED);
3228 }
3229
3230 #[test]
3231 fn test_tool_event_types() {
3232 assert_eq!(TOOL_STARTED, "tool.started");
3233 assert_eq!(TOOL_COMPLETED, "tool.completed");
3234 }
3235
3236 #[test]
3237 fn test_llm_generation_event_type() {
3238 assert_eq!(LLM_GENERATION, "llm.generation");
3239 }
3240
3241 #[test]
3242 fn test_llm_generation_data_success() {
3243 let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")];
3244 let tools = vec![ToolDefinitionSummary {
3245 name: "get_weather".to_string(),
3246 display_name: None,
3247 category: None,
3248 capability_id: None,
3249 capability_name: None,
3250 description: "Get weather for a city".to_string(),
3251 }];
3252 let tool_calls = vec![];
3253 let data = LlmGenerationData::success(
3254 messages.clone(),
3255 tools,
3256 Some("Hi there!".to_string()),
3257 tool_calls,
3258 "gpt-4o".to_string(),
3259 Some("openai".to_string()),
3260 Some(TokenUsage {
3261 input_tokens: 10,
3262 output_tokens: 5,
3263 cache_read_tokens: None,
3264 cache_creation_tokens: None,
3265 actual_cost_usd: None,
3266 estimated_cost_usd: None,
3267 effective_cost_usd: None,
3268 }),
3269 Some(100),
3270 Some(25), );
3272
3273 assert_eq!(data.messages.len(), 2);
3274 assert_eq!(data.tools.len(), 1);
3275 assert_eq!(data.tools[0].name, "get_weather");
3276 assert_eq!(data.output.text, Some("Hi there!".to_string()));
3277 assert!(data.output.tool_calls.is_empty());
3278 assert!(data.metadata.success);
3279 assert_eq!(data.metadata.model, "gpt-4o");
3280 assert_eq!(data.metadata.provider, Some("openai".to_string()));
3281 assert!(data.metadata.error.is_none());
3282 assert_eq!(data.metadata.finish_reasons, Some(vec!["stop".to_string()]));
3284 assert!(data.metadata.response_id.is_none());
3285 }
3286
3287 #[test]
3288 fn test_llm_generation_data_with_full_metadata() {
3289 let messages = vec![Message::user("Hello")];
3290 let data = LlmGenerationData::success_with_metadata(
3291 messages,
3292 vec![],
3293 Some("Hi!".to_string()),
3294 vec![],
3295 "claude-3-opus".to_string(),
3296 Some("anthropic".to_string()),
3297 Some(TokenUsage {
3298 input_tokens: 5,
3299 output_tokens: 3,
3300 cache_read_tokens: None,
3301 cache_creation_tokens: None,
3302 actual_cost_usd: None,
3303 estimated_cost_usd: None,
3304 effective_cost_usd: None,
3305 }),
3306 Some(50),
3307 Some(25), Some(vec!["end_turn".to_string()]),
3309 Some("msg_12345".to_string()),
3310 );
3311
3312 assert!(data.metadata.success);
3313 assert_eq!(data.metadata.model, "claude-3-opus");
3314 assert_eq!(data.metadata.provider, Some("anthropic".to_string()));
3315 assert_eq!(data.metadata.time_to_first_token_ms, Some(25));
3316 assert_eq!(
3317 data.metadata.finish_reasons,
3318 Some(vec!["end_turn".to_string()])
3319 );
3320 assert_eq!(data.metadata.response_id, Some("msg_12345".to_string()));
3321 }
3322
3323 #[test]
3324 fn test_llm_generation_data_failure() {
3325 let messages = vec![Message::user("Hello")];
3326 let data = LlmGenerationData::failure(
3327 messages,
3328 vec![],
3329 "gpt-4o".to_string(),
3330 Some("openai".to_string()),
3331 "Rate limit exceeded".to_string(),
3332 Some(50),
3333 None, );
3335
3336 assert!(!data.metadata.success);
3337 assert_eq!(data.metadata.error, Some("Rate limit exceeded".to_string()));
3338 assert!(data.output.text.is_none());
3339 assert!(data.output.tool_calls.is_empty());
3340 }
3341
3342 #[test]
3343 fn test_llm_generation_event_data() {
3344 let data = LlmGenerationData::success(
3345 vec![Message::user("test")],
3346 vec![],
3347 Some("response".to_string()),
3348 vec![],
3349 "model".to_string(),
3350 None,
3351 None,
3352 None,
3353 None, );
3355
3356 let event_data: EventData = data.into();
3357 assert_eq!(event_data.event_type(), LLM_GENERATION);
3358 }
3359
3360 #[test]
3361 fn test_llm_generation_is_durable_not_ephemeral() {
3362 let session_id = SessionId::new();
3363 let data = LlmGenerationData::success(
3364 vec![Message::user("test")],
3365 vec![],
3366 Some("response".to_string()),
3367 vec![],
3368 "model".to_string(),
3369 None,
3370 None,
3371 None,
3372 None,
3373 );
3374
3375 let request = EventRequest::new(session_id, EventContext::empty(), data);
3376 assert!(!request.is_ephemeral());
3377 }
3378
3379 #[test]
3380 fn test_delta_events_are_ephemeral() {
3381 let session_id = SessionId::new();
3382 let turn_id = TurnId::new();
3383
3384 let output_delta = EventRequest::new(
3385 session_id,
3386 EventContext::empty(),
3387 OutputMessageDeltaData {
3388 turn_id,
3389 message_id: MessageId::new(),
3390 delta: "hel".to_string(),
3391 accumulated: "hel".to_string(),
3392 phase: None,
3393 },
3394 );
3395 assert!(output_delta.is_ephemeral());
3396
3397 let thinking_delta = EventRequest::new(
3398 session_id,
3399 EventContext::empty(),
3400 ReasonThinkingDeltaData {
3401 turn_id,
3402 delta: "step".to_string(),
3403 accumulated: "step".to_string(),
3404 },
3405 );
3406 assert!(thinking_delta.is_ephemeral());
3407
3408 let tool_delta = EventRequest::new(
3409 session_id,
3410 EventContext::empty(),
3411 ToolOutputDeltaData {
3412 tool_call_id: "call_123".to_string(),
3413 tool_name: "bash".to_string(),
3414 delta: "line".to_string(),
3415 stream: "stdout".to_string(),
3416 },
3417 );
3418 assert!(tool_delta.is_ephemeral());
3419 }
3420
3421 #[test]
3422 fn test_llm_generation_data_with_request_options() {
3423 let mut provider_options = HashMap::new();
3424 provider_options.insert(
3425 "openai".to_string(),
3426 json!({ "previous_response_id": true }),
3427 );
3428
3429 let data = LlmGenerationData::success(
3430 vec![Message::user("Hello")],
3431 vec![],
3432 Some("Hi".to_string()),
3433 vec![],
3434 "gpt-5.4".to_string(),
3435 Some("openai".to_string()),
3436 None,
3437 Some(42),
3438 Some(12),
3439 )
3440 .with_request_options(LlmRequestOptions {
3441 prompt_cache: Some(LlmPromptCacheInfo {
3442 enabled: true,
3443 strategy: PromptCacheStrategy::Auto,
3444 provider_mode: Some("prompt_cache_key".to_string()),
3445 }),
3446 tool_search: Some(LlmToolSearchInfo {
3447 enabled: true,
3448 threshold: 8,
3449 }),
3450 provider_options,
3451 metadata: Default::default(),
3452 });
3453
3454 let json = serde_json::to_value(&data).unwrap();
3455 assert_eq!(
3456 json["metadata"]["request_options"]["prompt_cache"]["provider_mode"],
3457 "prompt_cache_key"
3458 );
3459 assert_eq!(
3460 json["metadata"]["request_options"]["tool_search"]["threshold"],
3461 8
3462 );
3463 assert_eq!(
3464 json["metadata"]["request_options"]["provider_options"]["openai"]["previous_response_id"],
3465 true
3466 );
3467 }
3468
3469 #[test]
3470 fn test_extended_thinking_event_types() {
3471 assert_eq!(REASON_THINKING_STARTED, "reason.thinking.started");
3472 assert_eq!(REASON_THINKING_DELTA, "reason.thinking.delta");
3473 assert_eq!(REASON_THINKING_COMPLETED, "reason.thinking.completed");
3474 }
3475
3476 #[test]
3477 fn test_output_message_started_data() {
3478 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3479 let data = OutputMessageStartedData {
3480 turn_id,
3481 message_id: MessageId::new(),
3482 model: Some("claude-4-opus".to_string()),
3483 iteration: None,
3484 phase: None,
3485 };
3486
3487 let event_data: EventData = data.into();
3488 assert_eq!(event_data.event_type(), OUTPUT_MESSAGE_STARTED);
3489
3490 let json = serde_json::to_string(&event_data).unwrap();
3492 assert!(json.contains("turn_id"));
3493 assert!(json.contains("claude-4-opus"));
3494 }
3495
3496 #[test]
3497 fn test_output_message_started_data_without_model() {
3498 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3499 let data = OutputMessageStartedData {
3500 turn_id,
3501 message_id: MessageId::new(),
3502 model: None,
3503 iteration: None,
3504 phase: None,
3505 };
3506
3507 let json = serde_json::to_string(&data).unwrap();
3509 assert!(!json.contains("model"));
3510 }
3511
3512 #[test]
3513 fn test_reason_thinking_started_data() {
3514 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3515 let data = ReasonThinkingStartedData {
3516 turn_id,
3517 model: Some("claude-4-opus".to_string()),
3518 };
3519
3520 let event_data: EventData = data.into();
3521 assert_eq!(event_data.event_type(), REASON_THINKING_STARTED);
3522
3523 let json = serde_json::to_string(&event_data).unwrap();
3525 assert!(json.contains("turn_id"));
3526 assert!(json.contains("claude-4-opus"));
3527 }
3528
3529 #[test]
3530 fn test_reason_thinking_delta_data() {
3531 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3532 let data = ReasonThinkingDeltaData {
3533 turn_id,
3534 delta: "thinking step 1".to_string(),
3535 accumulated: "thinking step 1".to_string(),
3536 };
3537
3538 let event_data: EventData = data.into();
3539 assert_eq!(event_data.event_type(), REASON_THINKING_DELTA);
3540
3541 let json = serde_json::to_string(&event_data).unwrap();
3543 assert!(json.contains("turn_id"));
3544 assert!(json.contains("delta"));
3545 assert!(json.contains("accumulated"));
3546 }
3547
3548 #[test]
3549 fn test_reason_thinking_completed_data() {
3550 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3551 let data = ReasonThinkingCompletedData {
3552 turn_id,
3553 thinking: "Full thinking content here".to_string(),
3554 };
3555
3556 let event_data: EventData = data.into();
3557 assert_eq!(event_data.event_type(), REASON_THINKING_COMPLETED);
3558
3559 let json = serde_json::to_string(&event_data).unwrap();
3561 assert!(json.contains("turn_id"));
3562 assert!(json.contains("thinking"));
3563 }
3564
3565 #[test]
3566 fn test_output_message_delta_data() {
3567 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3568 let data = OutputMessageDeltaData {
3569 turn_id,
3570 message_id: MessageId::new(),
3571 delta: "Hello".to_string(),
3572 accumulated: "Hello".to_string(),
3573 phase: None,
3574 };
3575
3576 let event_data: EventData = data.into();
3577 assert_eq!(event_data.event_type(), OUTPUT_MESSAGE_DELTA);
3578
3579 let json = serde_json::to_string(&event_data).unwrap();
3581 assert!(json.contains("turn_id"));
3582 assert!(json.contains("delta"));
3583 assert!(json.contains("accumulated"));
3584 }
3585
3586 #[test]
3587 fn test_output_message_lifecycle_shares_message_id() {
3588 let turn_id = TurnId::new();
3589 let message_id = MessageId::new();
3590 let next_message_id = MessageId::new();
3591
3592 let started = OutputMessageStartedData {
3593 turn_id,
3594 message_id,
3595 model: None,
3596 iteration: Some(1),
3597 phase: None,
3598 };
3599 let next_started = OutputMessageStartedData {
3600 turn_id,
3601 message_id: next_message_id,
3602 model: None,
3603 iteration: Some(2),
3604 phase: None,
3605 };
3606 let delta = OutputMessageDeltaData {
3607 turn_id,
3608 message_id,
3609 delta: "Hello".to_string(),
3610 accumulated: "Hello".to_string(),
3611 phase: None,
3612 };
3613 let replaced = OutputMessageReplacedData {
3614 turn_id,
3615 message_id,
3616 guardrail_capability_id: "guardrails".to_string(),
3617 guardrail_id: "example".to_string(),
3618 reason_code: "blocked".to_string(),
3619 replacement: "Safe response".to_string(),
3620 };
3621 let completed = OutputMessageCompletedData::new(
3622 Message::assistant("Safe response").with_id(message_id),
3623 );
3624
3625 assert_eq!(started.message_id, message_id);
3626 assert_eq!(delta.message_id, message_id);
3627 assert_eq!(replaced.message_id, message_id);
3628 assert_eq!(completed.message.id, message_id);
3629 assert_ne!(started.message_id, next_started.message_id);
3630
3631 for value in [
3632 serde_json::to_value(started).unwrap(),
3633 serde_json::to_value(delta).unwrap(),
3634 serde_json::to_value(replaced).unwrap(),
3635 ] {
3636 assert_eq!(value["message_id"], message_id.to_string());
3637 }
3638 }
3639
3640 #[test]
3641 fn test_output_message_phase_hint_serde() {
3642 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3646
3647 let started_none = OutputMessageStartedData {
3648 turn_id,
3649 message_id: MessageId::new(),
3650 model: None,
3651 iteration: None,
3652 phase: None,
3653 };
3654 assert!(
3655 !serde_json::to_string(&started_none)
3656 .unwrap()
3657 .contains("phase")
3658 );
3659
3660 let delta_commentary = OutputMessageDeltaData {
3661 turn_id,
3662 message_id: MessageId::new(),
3663 delta: "one moment".to_string(),
3664 accumulated: "one moment".to_string(),
3665 phase: Some(crate::message::ExecutionPhase::Commentary),
3666 };
3667 let json = serde_json::to_value(&delta_commentary).unwrap();
3668 assert_eq!(json["phase"], "commentary");
3669
3670 let back: OutputMessageDeltaData = serde_json::from_value(json).unwrap();
3672 assert_eq!(back.phase, Some(crate::message::ExecutionPhase::Commentary));
3673
3674 let delta_final = OutputMessageDeltaData {
3675 turn_id,
3676 message_id: MessageId::new(),
3677 delta: "done".to_string(),
3678 accumulated: "done".to_string(),
3679 phase: Some(crate::message::ExecutionPhase::FinalAnswer),
3680 };
3681 assert_eq!(
3682 serde_json::to_value(&delta_final).unwrap()["phase"],
3683 "final_answer"
3684 );
3685 }
3686
3687 #[test]
3688 fn test_output_message_delta_deserialization_preserves_fields() {
3689 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3692 let data = OutputMessageDeltaData {
3693 turn_id,
3694 message_id: MessageId::new(),
3695 delta: "Hello world".to_string(),
3696 accumulated: "Hello world".to_string(),
3697 phase: None,
3698 };
3699
3700 let json = serde_json::to_value(EventData::OutputMessageDelta(data.clone())).unwrap();
3702
3703 let deserialized = deserialize_event_data(OUTPUT_MESSAGE_DELTA, json);
3705
3706 match deserialized {
3708 EventData::OutputMessageDelta(td) => {
3709 assert_eq!(td.turn_id, turn_id);
3710 assert_eq!(td.delta, "Hello world");
3711 assert_eq!(td.accumulated, "Hello world");
3712 }
3713 _ => panic!("Expected OutputMessageDelta, got different variant"),
3714 }
3715 }
3716
3717 #[test]
3718 fn test_output_message_started_deserialization() {
3719 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3720 let data = OutputMessageStartedData {
3721 turn_id,
3722 message_id: MessageId::new(),
3723 model: Some("claude-3".to_string()),
3724 iteration: None,
3725 phase: None,
3726 };
3727
3728 let json = serde_json::to_value(EventData::OutputMessageStarted(data.clone())).unwrap();
3730
3731 let deserialized = deserialize_event_data(OUTPUT_MESSAGE_STARTED, json);
3733
3734 match deserialized {
3736 EventData::OutputMessageStarted(at) => {
3737 assert_eq!(at.turn_id, turn_id);
3738 assert_eq!(at.model, Some("claude-3".to_string()));
3739 }
3740 _ => panic!("Expected OutputMessageStarted, got different variant"),
3741 }
3742 }
3743
3744 #[test]
3745 fn test_reason_thinking_started_deserialization() {
3746 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3751 let data = ReasonThinkingStartedData {
3752 turn_id,
3753 model: Some("claude-3".to_string()),
3754 };
3755
3756 let json = serde_json::to_value(&data).unwrap();
3758
3759 let deserialized = deserialize_event_data(REASON_THINKING_STARTED, json);
3761
3762 match deserialized {
3764 EventData::ReasonThinkingStarted(at) => {
3765 assert_eq!(at.turn_id, turn_id);
3766 assert_eq!(at.model, Some("claude-3".to_string()));
3767 }
3768 other => panic!("Expected ReasonThinkingStarted, got {}", other.event_type()),
3769 }
3770 }
3771
3772 #[test]
3773 fn test_llm_generation_with_ttft() {
3774 let messages = vec![Message::user("Hello")];
3775 let data = LlmGenerationData::success_with_metadata(
3776 messages,
3777 vec![],
3778 Some("Hi!".to_string()),
3779 vec![],
3780 "gpt-4o".to_string(),
3781 Some("openai".to_string()),
3782 Some(TokenUsage {
3783 input_tokens: 10,
3784 output_tokens: 5,
3785 cache_read_tokens: None,
3786 cache_creation_tokens: None,
3787 actual_cost_usd: None,
3788 estimated_cost_usd: None,
3789 effective_cost_usd: None,
3790 }),
3791 Some(500), Some(120), Some(vec!["stop".to_string()]),
3794 None,
3795 );
3796
3797 assert!(data.metadata.success);
3798 assert_eq!(data.metadata.duration_ms, Some(500));
3799 assert_eq!(data.metadata.time_to_first_token_ms, Some(120));
3800 }
3801
3802 #[test]
3803 fn test_llm_generation_ttft_serialization() {
3804 let messages = vec![Message::user("test")];
3805 let data = LlmGenerationData::success_with_metadata(
3806 messages,
3807 vec![],
3808 Some("response".to_string()),
3809 vec![],
3810 "model".to_string(),
3811 None,
3812 None,
3813 Some(1000),
3814 Some(150), None,
3816 None,
3817 );
3818
3819 let json = serde_json::to_string(&data).unwrap();
3820 assert!(json.contains("time_to_first_token_ms"));
3821 assert!(json.contains("150"));
3822 }
3823
3824 #[test]
3825 fn test_reason_item_data_event_type_and_serialization() {
3826 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3827 let data = ReasonItemData {
3828 turn_id,
3829 provider: "openai".to_string(),
3830 model: Some("gpt-5.5".to_string()),
3831 item_id: "rs_abc".to_string(),
3832 encrypted_content: Some("OPAQUE_BLOB".to_string()),
3833 summary: vec!["safe summary".to_string()],
3834 token_count: Some(123),
3835 };
3836
3837 let event_data: EventData = data.into();
3838 assert_eq!(event_data.event_type(), REASON_ITEM);
3839
3840 let json = serde_json::to_string(&event_data).unwrap();
3841 assert!(json.contains("turn_id"));
3842 assert!(json.contains("openai"));
3843 assert!(json.contains("rs_abc"));
3844 assert!(json.contains("OPAQUE_BLOB"));
3845 assert!(json.contains("safe summary"));
3846 }
3847
3848 #[test]
3849 fn test_event_deserialize_reason_item_uses_event_type_dispatch() {
3850 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3851 let payload = serde_json::json!({
3852 "id": EventId::new().to_string(),
3853 "type": REASON_ITEM,
3854 "ts": Utc::now().to_rfc3339(),
3855 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
3856 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
3857 "data": {
3858 "turn_id": turn_id.to_string(),
3859 "provider": "openai",
3860 "model": "gpt-5",
3861 "item_id": "rs_event",
3862 "encrypted_content": "ENC",
3863 "summary": ["safe"],
3864 "token_count": 9
3865 }
3866 });
3867
3868 let event: Event = serde_json::from_value(payload).expect("event deserializes");
3869 match event.data {
3870 EventData::ReasonItem(data) => {
3871 assert_eq!(data.turn_id, turn_id);
3872 assert_eq!(data.provider, "openai");
3873 assert_eq!(data.item_id, "rs_event");
3874 assert_eq!(data.token_count, Some(9));
3875 }
3876 other => panic!("expected reason.item data, got {}", other.event_type()),
3877 }
3878 }
3879
3880 #[test]
3881 fn test_event_request_deserialize_reason_item_uses_event_type_dispatch() {
3882 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3883 let payload = serde_json::json!({
3884 "type": REASON_ITEM,
3885 "ts": Utc::now().to_rfc3339(),
3886 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
3887 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
3888 "data": {
3889 "turn_id": turn_id.to_string(),
3890 "provider": "openai",
3891 "item_id": "rs_request",
3892 "encrypted_content": "ENC",
3893 "summary": ["safe"]
3894 }
3895 });
3896
3897 let req: EventRequest = serde_json::from_value(payload).expect("request deserializes");
3898 match req.data {
3899 EventData::ReasonItem(data) => {
3900 assert_eq!(data.turn_id, turn_id);
3901 assert_eq!(data.provider, "openai");
3902 assert_eq!(data.item_id, "rs_request");
3903 }
3904 other => panic!("expected reason.item data, got {}", other.event_type()),
3905 }
3906 }
3907
3908 #[test]
3909 fn test_reason_item_data_round_trip_uses_typed_dispatch() {
3910 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3914 let data = ReasonItemData {
3915 turn_id,
3916 provider: "openai".to_string(),
3917 model: Some("gpt-5".to_string()),
3918 item_id: "rs_xyz".to_string(),
3919 encrypted_content: Some("ENC".to_string()),
3920 summary: vec![],
3921 token_count: None,
3922 };
3923
3924 let json = serde_json::to_value(&data).unwrap();
3925 let deserialized = deserialize_event_data(REASON_ITEM, json);
3926
3927 match deserialized {
3928 EventData::ReasonItem(out) => {
3929 assert_eq!(out.turn_id, turn_id);
3930 assert_eq!(out.provider, "openai");
3931 assert_eq!(out.item_id, "rs_xyz");
3932 assert_eq!(out.encrypted_content.as_deref(), Some("ENC"));
3933 }
3934 other => panic!("Expected ReasonItem, got {}", other.event_type()),
3935 }
3936 }
3937
3938 #[test]
3947 fn test_reason_item_resolves_via_type_dispatch_despite_overlap() {
3948 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3953 let json = serde_json::json!({
3954 "turn_id": turn_id.to_string(),
3955 "provider": "openai",
3956 "model": "gpt-5",
3957 "item_id": "rs_keep",
3958 "encrypted_content": "ENC",
3959 "summary": ["s"],
3960 "token_count": 7,
3961 });
3962
3963 let as_thinking: ReasonThinkingStartedData =
3968 serde_json::from_value(json.clone()).expect("thinking ignores extra fields");
3969 assert_eq!(as_thinking.turn_id, turn_id);
3970 assert_eq!(as_thinking.model.as_deref(), Some("gpt-5"));
3971
3972 let as_item: ReasonItemData =
3973 serde_json::from_value(json.clone()).expect("ReasonItem accepts payload");
3974 assert_eq!(as_item.item_id, "rs_keep");
3975 assert_eq!(as_item.provider, "openai");
3976
3977 let event_data = deserialize_event_data(REASON_ITEM, json);
3980 match event_data {
3981 EventData::ReasonItem(out) => {
3982 assert_eq!(out.item_id, "rs_keep");
3983 assert_eq!(out.provider, "openai");
3984 }
3985 other => panic!(
3986 "Typed dispatcher must select ReasonItem for {REASON_ITEM}, got {}",
3987 other.event_type()
3988 ),
3989 }
3990 }
3991
3992 #[test]
3999 fn test_reason_item_data_excludes_plaintext_reasoning() {
4000 let turn_id = TurnId::from_uuid(Uuid::now_v7());
4001 let data = ReasonItemData {
4002 turn_id,
4003 provider: "openai".to_string(),
4004 model: Some("gpt-5".to_string()),
4005 item_id: "rs_secret".to_string(),
4006 encrypted_content: Some("opaque_blob_thinking_content_reasoning_text".to_string()),
4010 summary: vec!["safe summary mentioning content and thinking".to_string()],
4011 token_count: Some(1),
4012 };
4013
4014 let value = serde_json::to_value(&data).expect("serializable");
4015 let object = value.as_object().expect("data serializes to JSON object");
4016 for forbidden in [
4017 "content",
4018 "reasoning_text",
4019 "thinking",
4020 "reasoning_content",
4021 "raw_reasoning",
4022 ] {
4023 assert!(
4024 !object.contains_key(forbidden),
4025 "ReasonItemData JSON must not expose `{forbidden}` key, got: {object:?}",
4026 );
4027 }
4028 assert!(object.contains_key("encrypted_content"));
4030 assert!(object.contains_key("summary"));
4031 }
4032
4033 #[test]
4034 fn test_llm_generation_ttft_omitted_when_none() {
4035 let messages = vec![Message::user("test")];
4036 let data = LlmGenerationData::success(
4037 messages,
4038 vec![],
4039 Some("response".to_string()),
4040 vec![],
4041 "model".to_string(),
4042 None,
4043 None,
4044 None,
4045 None, );
4047
4048 assert!(data.metadata.time_to_first_token_ms.is_none());
4050
4051 let json = serde_json::to_string(&data).unwrap();
4053 assert!(!json.contains("time_to_first_token_ms"));
4054 }
4055}
4056
4057#[cfg(test)]
4066mod contract_tests {
4067 use super::*;
4068 use insta::{assert_json_snapshot, with_settings};
4069
4070 fn test_session_id() -> SessionId {
4072 SessionId::from_uuid(uuid::Uuid::from_u128(
4073 0x0000_0000_0000_0000_0000_0000_0000_0001,
4074 ))
4075 }
4076
4077 fn test_turn_id() -> TurnId {
4078 TurnId::from_uuid(uuid::Uuid::from_u128(
4079 0x0000_0000_0000_0000_0000_0000_0000_0002,
4080 ))
4081 }
4082
4083 fn test_message_id() -> MessageId {
4084 MessageId::from_uuid(uuid::Uuid::from_u128(
4085 0x0000_0000_0000_0000_0000_0000_0000_0003,
4086 ))
4087 }
4088
4089 fn test_agent_id() -> AgentId {
4090 AgentId::from_uuid(uuid::Uuid::from_u128(
4091 0x0000_0000_0000_0000_0000_0000_0000_0004,
4092 ))
4093 }
4094
4095 fn test_harness_id() -> HarnessId {
4096 HarnessId::from_uuid(uuid::Uuid::from_u128(
4097 0x0000_0000_0000_0000_0000_0000_0000_0005,
4098 ))
4099 }
4100
4101 #[test]
4108 fn snapshot_input_message() {
4109 let data = InputMessageData::new(Message::user("Hello, world!"));
4110 with_settings!({
4111 sort_maps => true,
4112 }, {
4113 assert_json_snapshot!("event_data_input_message", data, {
4115 ".message.id" => "[MESSAGE_ID]",
4116 ".message.created_at" => "[TIMESTAMP]"
4117 });
4118 });
4119 }
4120
4121 #[test]
4122 fn snapshot_output_message_started() {
4123 let data = OutputMessageStartedData {
4124 turn_id: test_turn_id(),
4125 message_id: test_message_id(),
4126 model: Some("gpt-4o".to_string()),
4127 iteration: None,
4128 phase: None,
4129 };
4130 with_settings!({
4131 sort_maps => true,
4132 }, {
4133 assert_json_snapshot!("event_data_output_message_started", data);
4134 });
4135 }
4136
4137 #[test]
4138 fn snapshot_output_message_delta() {
4139 let data = OutputMessageDeltaData {
4140 turn_id: test_turn_id(),
4141 message_id: test_message_id(),
4142 delta: "Hello".to_string(),
4143 accumulated: "Hello".to_string(),
4144 phase: None,
4145 };
4146 with_settings!({
4147 sort_maps => true,
4148 }, {
4149 assert_json_snapshot!("event_data_output_message_delta", data);
4150 });
4151 }
4152
4153 #[test]
4154 fn snapshot_output_message_completed() {
4155 let data = OutputMessageCompletedData::new(Message::assistant("Hello!"));
4156 with_settings!({
4157 sort_maps => true,
4158 }, {
4159 assert_json_snapshot!("event_data_output_message_completed", data, {
4161 ".message.id" => "[MESSAGE_ID]",
4162 ".message.created_at" => "[TIMESTAMP]"
4163 });
4164 });
4165 }
4166
4167 #[test]
4168 fn snapshot_turn_started() {
4169 let data = TurnStartedData {
4170 turn_id: test_turn_id(),
4171 input_message_id: test_message_id(),
4172 input_content: Some("Hello".to_string()),
4173 };
4174 with_settings!({
4175 sort_maps => true,
4176 }, {
4177 assert_json_snapshot!("event_data_turn_started", data);
4178 });
4179 }
4180
4181 #[test]
4182 fn snapshot_turn_completed() {
4183 let data = TurnCompletedData {
4184 turn_id: test_turn_id(),
4185 iterations: 3,
4186 duration_ms: Some(1500),
4187 usage: Some(TokenUsage::new(100, 50)),
4188 input_content: None,
4189 final_message_id: Some(test_message_id()),
4190 final_answer_preview: Some("Done.".to_string()),
4191 time_to_first_token_ms: Some(120),
4192 tool_call_count: Some(2),
4193 llm_call_count: Some(3),
4194 status: Some("completed".to_string()),
4195 };
4196 with_settings!({
4197 sort_maps => true,
4198 }, {
4199 assert_json_snapshot!("event_data_turn_completed", data);
4200 });
4201 }
4202
4203 #[test]
4204 fn snapshot_turn_failed() {
4205 let data = TurnFailedData {
4206 turn_id: test_turn_id(),
4207 error: "Rate limit exceeded".to_string(),
4208 error_code: Some("RATE_LIMIT".to_string()),
4209 error_fields: None,
4210 error_disclosure: None,
4211 };
4212 with_settings!({
4213 sort_maps => true,
4214 }, {
4215 assert_json_snapshot!("event_data_turn_failed", data);
4216 });
4217 }
4218
4219 #[test]
4220 fn snapshot_turn_cancelled() {
4221 let data = TurnCancelledData {
4222 turn_id: test_turn_id(),
4223 reason: Some("User requested".to_string()),
4224 usage: Some(TokenUsage::new(50, 25)),
4225 };
4226 with_settings!({
4227 sort_maps => true,
4228 }, {
4229 assert_json_snapshot!("event_data_turn_cancelled", data);
4230 });
4231 }
4232
4233 #[test]
4234 fn snapshot_reason_started() {
4235 let data = ReasonStartedData {
4236 harness_id: test_harness_id(),
4237 agent_id: Some(test_agent_id()),
4238 metadata: Some(ModelMetadata {
4239 model: "gpt-4o".to_string(),
4240 model_id: None,
4241 provider_id: None,
4242 }),
4243 };
4244 with_settings!({
4245 sort_maps => true,
4246 }, {
4247 assert_json_snapshot!("event_data_reason_started", data);
4248 });
4249 }
4250
4251 #[test]
4252 fn snapshot_reason_completed() {
4253 let data = ReasonCompletedData::success(
4254 "Hello world",
4255 true,
4256 2,
4257 Some(1000),
4258 Some(TokenUsage::new(100, 50)),
4259 );
4260 with_settings!({
4261 sort_maps => true,
4262 }, {
4263 assert_json_snapshot!("event_data_reason_completed", data);
4264 });
4265 }
4266
4267 #[test]
4268 fn snapshot_act_started() {
4269 let data = ActStartedData {
4270 tool_calls: vec![ToolCallSummary {
4271 id: "tc_1".to_string(),
4272 name: "get_weather".to_string(),
4273 display_name: None,
4274 narration: None,
4275 completed_narration: None,
4276 }],
4277 headline: None,
4278 };
4279 with_settings!({
4280 sort_maps => true,
4281 }, {
4282 assert_json_snapshot!("event_data_act_started", data);
4283 });
4284 }
4285
4286 #[test]
4287 fn snapshot_act_completed() {
4288 let data = ActCompletedData {
4289 completed: true,
4290 success_count: 2,
4291 error_count: 0,
4292 duration_ms: Some(500),
4293 headline: None,
4294 };
4295 with_settings!({
4296 sort_maps => true,
4297 }, {
4298 assert_json_snapshot!("event_data_act_completed", data);
4299 });
4300 }
4301
4302 #[test]
4303 fn snapshot_tool_started() {
4304 let data = ToolStartedData {
4305 tool_call: ToolCall {
4306 id: "tc_1".to_string(),
4307 name: "get_weather".to_string(),
4308 arguments: serde_json::json!({"city": "London"}),
4309 },
4310 tool_call_fingerprint: None,
4311 display_name: None,
4312 narration: None,
4313 };
4314 with_settings!({
4315 sort_maps => true,
4316 }, {
4317 assert_json_snapshot!("event_data_tool_started", data);
4318 });
4319 }
4320
4321 #[test]
4322 fn snapshot_tool_completed() {
4323 let data = ToolCompletedData::success(
4324 "tc_1".to_string(),
4325 "get_weather".to_string(),
4326 vec![crate::message::ContentPart::text("Sunny, 22°C")],
4327 Some(250),
4328 );
4329 with_settings!({
4330 sort_maps => true,
4331 }, {
4332 assert_json_snapshot!("event_data_tool_completed", data);
4333 });
4334 }
4335
4336 #[test]
4337 fn snapshot_llm_generation() {
4338 let data = LlmGenerationData::success(
4339 vec![Message::user("Hello")],
4340 vec![ToolDefinitionSummary {
4341 name: "tool1".to_string(),
4342 display_name: None,
4343 category: None,
4344 capability_id: None,
4345 capability_name: None,
4346 description: "A tool".to_string(),
4347 }],
4348 Some("Hi there!".to_string()),
4349 vec![],
4350 "gpt-4o".to_string(),
4351 Some("openai".to_string()),
4352 Some(TokenUsage::new(10, 5)),
4353 Some(100),
4354 Some(25),
4355 );
4356 with_settings!({
4357 sort_maps => true,
4358 }, {
4359 assert_json_snapshot!("event_data_llm_generation", data, {
4361 ".messages[].id" => "[MESSAGE_ID]",
4362 ".messages[].created_at" => "[TIMESTAMP]"
4363 });
4364 });
4365 }
4366
4367 #[test]
4368 fn snapshot_reason_thinking_started() {
4369 let data = ReasonThinkingStartedData {
4370 turn_id: test_turn_id(),
4371 model: Some("claude-4-opus".to_string()),
4372 };
4373 with_settings!({
4374 sort_maps => true,
4375 }, {
4376 assert_json_snapshot!("event_data_reason_thinking_started", data);
4377 });
4378 }
4379
4380 #[test]
4381 fn snapshot_reason_thinking_delta() {
4382 let data = ReasonThinkingDeltaData {
4383 turn_id: test_turn_id(),
4384 delta: "Let me think...".to_string(),
4385 accumulated: "Let me think...".to_string(),
4386 };
4387 with_settings!({
4388 sort_maps => true,
4389 }, {
4390 assert_json_snapshot!("event_data_reason_thinking_delta", data);
4391 });
4392 }
4393
4394 #[test]
4395 fn snapshot_reason_thinking_completed() {
4396 let data = ReasonThinkingCompletedData {
4397 turn_id: test_turn_id(),
4398 thinking: "I need to consider...".to_string(),
4399 };
4400 with_settings!({
4401 sort_maps => true,
4402 }, {
4403 assert_json_snapshot!("event_data_reason_thinking_completed", data);
4404 });
4405 }
4406
4407 #[test]
4408 fn snapshot_reason_item() {
4409 let data = ReasonItemData {
4410 turn_id: test_turn_id(),
4411 provider: "openai".to_string(),
4412 model: Some("gpt-5.5".to_string()),
4413 item_id: "rs_test".to_string(),
4414 encrypted_content: Some("OPAQUE".to_string()),
4415 summary: vec!["safe summary".to_string()],
4416 token_count: Some(42),
4417 };
4418 with_settings!({
4419 sort_maps => true,
4420 }, {
4421 assert_json_snapshot!("event_data_reason_item", data);
4422 });
4423 }
4424
4425 #[test]
4426 fn snapshot_session_started() {
4427 let data = SessionStartedData {
4428 harness_id: test_harness_id(),
4429 agent_id: Some(test_agent_id()),
4430 model_id: None,
4431 };
4432 with_settings!({
4433 sort_maps => true,
4434 }, {
4435 assert_json_snapshot!("event_data_session_started", data);
4436 });
4437 }
4438
4439 #[test]
4440 fn snapshot_session_activated() {
4441 let data = SessionActivatedData {
4442 turn_id: test_turn_id(),
4443 input_message_id: test_message_id(),
4444 };
4445 with_settings!({
4446 sort_maps => true,
4447 }, {
4448 assert_json_snapshot!("event_data_session_activated", data);
4449 });
4450 }
4451
4452 #[test]
4453 fn snapshot_session_idled() {
4454 let data = SessionIdledData {
4455 turn_id: test_turn_id(),
4456 iterations: Some(3),
4457 usage: Some(TokenUsage::new(500, 200)),
4458 };
4459 with_settings!({
4460 sort_maps => true,
4461 }, {
4462 assert_json_snapshot!("event_data_session_idled", data);
4463 });
4464 }
4465
4466 #[test]
4467 fn snapshot_session_title_updated() {
4468 let data = SessionTitleUpdatedData {
4469 previous_title: Some("Old title".to_string()),
4470 title: "Automatic Session Titles".to_string(),
4471 };
4472 with_settings!({
4473 sort_maps => true,
4474 }, {
4475 assert_json_snapshot!("event_data_session_title_updated", data);
4476 });
4477
4478 let untitled = serde_json::to_value(SessionTitleUpdatedData {
4479 previous_title: None,
4480 title: "First title".to_string(),
4481 })
4482 .expect("serialize title event");
4483 assert!(untitled["previous_title"].is_null());
4484 }
4485
4486 #[test]
4492 fn tool_call_summary_with_display_name() {
4493 let summary = ToolCallSummary {
4494 id: "tc_1".to_string(),
4495 name: "get_weather".to_string(),
4496 display_name: Some("Get Weather".to_string()),
4497 narration: None,
4498 completed_narration: None,
4499 };
4500 let json = serde_json::to_value(&summary).unwrap();
4501 assert_eq!(json["display_name"], "Get Weather");
4502
4503 let deserialized: ToolCallSummary = serde_json::from_value(json).unwrap();
4505 assert_eq!(deserialized.display_name.as_deref(), Some("Get Weather"));
4506 }
4507
4508 #[test]
4509 fn tool_call_summary_without_display_name_omits_field() {
4510 let summary = ToolCallSummary {
4511 id: "tc_1".to_string(),
4512 name: "get_weather".to_string(),
4513 display_name: None,
4514 narration: None,
4515 completed_narration: None,
4516 };
4517 let json = serde_json::to_string(&summary).unwrap();
4518 assert!(!json.contains("display_name"));
4519
4520 let json_without = r#"{"id":"tc_1","name":"get_weather"}"#;
4522 let deserialized: ToolCallSummary = serde_json::from_str(json_without).unwrap();
4523 assert_eq!(deserialized.display_name, None);
4524 }
4525
4526 #[test]
4527 fn act_started_with_definitions_populates_display_names() {
4528 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4529
4530 let tool_calls = vec![
4531 ToolCall {
4532 id: "tc_1".to_string(),
4533 name: "get_weather".to_string(),
4534 arguments: serde_json::json!({}),
4535 },
4536 ToolCall {
4537 id: "tc_2".to_string(),
4538 name: "unknown_tool".to_string(),
4539 arguments: serde_json::json!({}),
4540 },
4541 ];
4542 let tool_defs = vec![crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4543 name: "get_weather".to_string(),
4544 display_name: Some("Get Weather".to_string()),
4545 description: "Gets weather".to_string(),
4546 parameters: serde_json::json!({}),
4547 policy: ToolPolicy::Auto,
4548 category: None,
4549 deferrable: DeferrablePolicy::default(),
4550 hints: crate::tool_types::ToolHints::default(),
4551 full_parameters: None,
4552 })];
4553
4554 let data = ActStartedData::with_definitions(&tool_calls, &tool_defs);
4555 assert_eq!(data.tool_calls.len(), 2);
4556 assert_eq!(
4557 data.tool_calls[0].display_name.as_deref(),
4558 Some("Get Weather")
4559 );
4560 assert_eq!(
4561 data.tool_calls[0].narration.as_deref(),
4562 Some("Running Get Weather")
4563 );
4564 assert_eq!(
4565 data.tool_calls[0].completed_narration.as_deref(),
4566 Some("Ran Get Weather")
4567 );
4568 assert_eq!(data.tool_calls[1].display_name, None);
4569 }
4570
4571 #[test]
4572 fn tool_completed_with_display_name_roundtrip() {
4573 let data = ToolCompletedData::success(
4574 "tc_1".to_string(),
4575 "get_weather".to_string(),
4576 vec![crate::message::ContentPart::text("Sunny")],
4577 Some(100),
4578 )
4579 .with_display_name(Some("Get Weather".to_string()));
4580
4581 assert_eq!(data.display_name.as_deref(), Some("Get Weather"));
4582
4583 let json = serde_json::to_value(&data).unwrap();
4584 assert_eq!(json["display_name"], "Get Weather");
4585
4586 let deserialized: ToolCompletedData = serde_json::from_value(json).unwrap();
4587 assert_eq!(deserialized.display_name.as_deref(), Some("Get Weather"));
4588 }
4589
4590 #[test]
4591 fn tool_started_display_name_serialization() {
4592 let data = ToolStartedData {
4593 tool_call: ToolCall {
4594 id: "tc_1".to_string(),
4595 name: "bash".to_string(),
4596 arguments: serde_json::json!({"command": "ls"}),
4597 },
4598 tool_call_fingerprint: None,
4599 display_name: Some("Bash".to_string()),
4600 narration: None,
4601 };
4602
4603 let json = serde_json::to_value(&data).unwrap();
4604 assert_eq!(json["display_name"], "Bash");
4605 }
4606
4607 #[test]
4608 fn tool_definition_summary_display_name() {
4609 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4610
4611 let def = crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4612 name: "read_file".to_string(),
4613 display_name: Some("Read File".to_string()),
4614 description: "Reads a file".to_string(),
4615 parameters: serde_json::json!({}),
4616 policy: ToolPolicy::Auto,
4617 category: None,
4618 deferrable: DeferrablePolicy::default(),
4619 hints: crate::tool_types::ToolHints::default(),
4620 full_parameters: None,
4621 });
4622
4623 let summary = ToolDefinitionSummary::from(&def);
4624 assert_eq!(summary.display_name.as_deref(), Some("Read File"));
4625
4626 let json = serde_json::to_value(&summary).unwrap();
4627 assert_eq!(json["display_name"], "Read File");
4628 }
4629
4630 #[test]
4637 fn forward_compat_unknown_fields_ignored() {
4638 let json = r#"{
4640 "turn_id": "turn_00000000000000000000000000000002",
4641 "iterations": 3,
4642 "duration_ms": 1500,
4643 "usage": {"input_tokens": 100, "output_tokens": 50},
4644 "future_field": "should be ignored",
4645 "another_new_field": 42
4646 }"#;
4647
4648 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4649 assert_eq!(data.iterations, 3);
4650 assert_eq!(data.duration_ms, Some(1500));
4651 }
4652
4653 #[test]
4654 fn forward_compat_unknown_event_type_becomes_unsupported() {
4655 let json = serde_json::json!({"some_field": "value"});
4657 let data = deserialize_event_data("future.event.type", json);
4658
4659 assert!(data.is_unsupported());
4660 assert_eq!(data.event_type(), "unsupported");
4661 }
4662
4663 #[test]
4664 fn forward_compat_unsupported_preserves_data() {
4665 let original = serde_json::json!({"key": "value", "nested": {"a": 1}});
4667 let data = deserialize_event_data("unknown.event", original.clone());
4668
4669 match data {
4670 EventData::Unsupported { event_type, data } => {
4671 assert_eq!(event_type, "unknown.event");
4672 assert_eq!(data, original);
4673 }
4674 _ => panic!("Expected Unsupported variant"),
4675 }
4676 }
4677
4678 #[test]
4679 fn forward_compat_optional_fields_absent() {
4680 let json = r#"{
4682 "turn_id": "turn_00000000000000000000000000000002",
4683 "iterations": 3
4684 }"#;
4685
4686 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4687 assert_eq!(data.iterations, 3);
4688 assert!(data.duration_ms.is_none());
4689 assert!(data.usage.is_none());
4690 assert!(data.input_content.is_none());
4691 assert!(data.final_message_id.is_none());
4692 assert!(data.final_answer_preview.is_none());
4693 assert!(data.time_to_first_token_ms.is_none());
4694 assert!(data.tool_call_count.is_none());
4695 assert!(data.llm_call_count.is_none());
4696 assert!(data.status.is_none());
4697 }
4698
4699 #[test]
4705 fn round_trip_all_event_data_types() {
4706 let test_cases: Vec<(&str, EventData)> = vec![
4708 (
4709 INPUT_MESSAGE,
4710 InputMessageData::new(Message::user("test")).into(),
4711 ),
4712 (
4713 OUTPUT_MESSAGE_STARTED,
4714 OutputMessageStartedData {
4715 turn_id: test_turn_id(),
4716 message_id: test_message_id(),
4717 model: None,
4718 iteration: None,
4719 phase: None,
4720 }
4721 .into(),
4722 ),
4723 (
4724 OUTPUT_MESSAGE_DELTA,
4725 OutputMessageDeltaData {
4726 turn_id: test_turn_id(),
4727 message_id: test_message_id(),
4728 delta: "x".to_string(),
4729 accumulated: "x".to_string(),
4730 phase: None,
4731 }
4732 .into(),
4733 ),
4734 (
4735 OUTPUT_MESSAGE_COMPLETED,
4736 OutputMessageCompletedData::new(Message::assistant("hi")).into(),
4737 ),
4738 (
4739 TURN_STARTED,
4740 TurnStartedData {
4741 turn_id: test_turn_id(),
4742 input_message_id: test_message_id(),
4743 input_content: None,
4744 }
4745 .into(),
4746 ),
4747 (
4748 TURN_COMPLETED,
4749 TurnCompletedData {
4750 turn_id: test_turn_id(),
4751 iterations: 1,
4752 duration_ms: None,
4753 usage: None,
4754 input_content: None,
4755 final_message_id: None,
4756 final_answer_preview: None,
4757 time_to_first_token_ms: None,
4758 tool_call_count: None,
4759 llm_call_count: None,
4760 status: None,
4761 }
4762 .into(),
4763 ),
4764 (
4765 TURN_FAILED,
4766 TurnFailedData {
4767 turn_id: test_turn_id(),
4768 error: "err".to_string(),
4769 error_code: None,
4770 error_fields: None,
4771 error_disclosure: None,
4772 }
4773 .into(),
4774 ),
4775 (
4776 TURN_CANCELLED,
4777 TurnCancelledData {
4778 turn_id: test_turn_id(),
4779 reason: None,
4780 usage: None,
4781 }
4782 .into(),
4783 ),
4784 (
4785 TURN_SEALED,
4786 TurnSealedData {
4787 turn_id: test_turn_id(),
4788 reason: "no_progress".to_string(),
4789 detail: Some("sealed".to_string()),
4790 iterations: Some(3),
4791 usage: None,
4792 }
4793 .into(),
4794 ),
4795 (
4796 REASON_STARTED,
4797 ReasonStartedData {
4798 harness_id: test_harness_id(),
4799 agent_id: Some(test_agent_id()),
4800 metadata: None,
4801 }
4802 .into(),
4803 ),
4804 (
4805 REASON_COMPLETED,
4806 ReasonCompletedData::success("", false, 0, None, None).into(),
4807 ),
4808 (
4809 ACT_STARTED,
4810 ActStartedData {
4811 tool_calls: vec![],
4812 headline: None,
4813 }
4814 .into(),
4815 ),
4816 (
4817 ACT_COMPLETED,
4818 ActCompletedData {
4819 completed: true,
4820 success_count: 0,
4821 error_count: 0,
4822 duration_ms: None,
4823 headline: None,
4824 }
4825 .into(),
4826 ),
4827 (
4828 SESSION_STARTED,
4829 SessionStartedData {
4830 harness_id: test_harness_id(),
4831 agent_id: Some(test_agent_id()),
4832 model_id: None,
4833 }
4834 .into(),
4835 ),
4836 (
4837 SESSION_ACTIVATED,
4838 SessionActivatedData {
4839 turn_id: test_turn_id(),
4840 input_message_id: test_message_id(),
4841 }
4842 .into(),
4843 ),
4844 (
4845 SESSION_IDLED,
4846 SessionIdledData {
4847 turn_id: test_turn_id(),
4848 iterations: None,
4849 usage: None,
4850 }
4851 .into(),
4852 ),
4853 (
4854 SESSION_TITLE_UPDATED,
4855 SessionTitleUpdatedData {
4856 previous_title: Some("Old title".to_string()),
4857 title: "New title".to_string(),
4858 }
4859 .into(),
4860 ),
4861 ];
4862
4863 for (event_type, original) in test_cases {
4864 let json = serde_json::to_value(&original).unwrap();
4866 let deserialized = deserialize_event_data(event_type, json);
4868 assert_eq!(
4870 original.event_type(),
4871 deserialized.event_type(),
4872 "Event type mismatch for {}",
4873 event_type
4874 );
4875 }
4876 }
4877
4878 #[test]
4884 fn event_structure_has_required_fields() {
4885 let session_id = test_session_id();
4886 let context = EventContext::turn(test_turn_id(), test_message_id());
4887 let event = Event::new(
4888 session_id,
4889 context,
4890 InputMessageData::new(Message::user("test")),
4891 );
4892
4893 let json = serde_json::to_value(&event).unwrap();
4895 assert!(json.get("id").is_some(), "Missing id field");
4896 assert!(json.get("type").is_some(), "Missing type field");
4897 assert!(json.get("ts").is_some(), "Missing ts field");
4898 assert!(json.get("session_id").is_some(), "Missing session_id field");
4899 assert!(json.get("context").is_some(), "Missing context field");
4900 assert!(json.get("data").is_some(), "Missing data field");
4901 }
4902
4903 #[test]
4904 fn event_context_span_fields() {
4905 let context = EventContext::empty().with_span(
4906 "trace123".to_string(),
4907 "span456".to_string(),
4908 Some("parent789".to_string()),
4909 );
4910
4911 let json = serde_json::to_value(&context).unwrap();
4912 assert_eq!(
4913 json.get("trace_id").and_then(|v| v.as_str()),
4914 Some("trace123")
4915 );
4916 assert_eq!(
4917 json.get("span_id").and_then(|v| v.as_str()),
4918 Some("span456")
4919 );
4920 assert_eq!(
4921 json.get("parent_span_id").and_then(|v| v.as_str()),
4922 Some("parent789")
4923 );
4924 }
4925
4926 #[test]
4927 fn is_unsupported_returns_false_for_known_types() {
4928 let data = InputMessageData::new(Message::user("test"));
4929 let event_data: EventData = data.into();
4930 assert!(!event_data.is_unsupported());
4931 }
4932
4933 #[test]
4934 fn is_unsupported_returns_true_for_unsupported() {
4935 let data = deserialize_event_data("unknown.type", serde_json::json!({}));
4936 assert!(data.is_unsupported());
4937 }
4938}