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";
114
115pub const SCHEDULE_TRIGGERED: &str = "schedule.triggered";
117
118pub const TASK_CREATED: &str = "task.created";
125pub const TASK_UPDATED: &str = "task.updated";
126pub const TASK_MESSAGE_SENT: &str = "task.message.sent";
127pub const TASK_MESSAGE_RECEIVED: &str = "task.message.received";
128
129pub const CONTEXT_COMPACTING: &str = "context.compacting";
131pub const CONTEXT_COMPACTED: &str = "context.compacted";
132
133pub const FILE_WRITTEN: &str = "file.written";
135
136pub const BUDGET_WARNING: &str = "budget.warning";
138pub const BUDGET_PAUSED: &str = "budget.paused";
139pub const BUDGET_EXHAUSTED: &str = "budget.exhausted";
140pub const BUDGET_RESUMED: &str = "budget.resumed";
141
142pub const VOICE_SESSION_STARTED: &str = "voice.session.started";
144pub const VOICE_INPUT_TRANSCRIPT_DELTA: &str = "voice.input_transcript.delta";
145pub const VOICE_INPUT_TRANSCRIPT_COMPLETED: &str = "voice.input_transcript.completed";
146pub const VOICE_OUTPUT_TRANSCRIPT_DELTA: &str = "voice.output_transcript.delta";
147pub const VOICE_OUTPUT_TRANSCRIPT_COMPLETED: &str = "voice.output_transcript.completed";
148pub const VOICE_SESSION_ENDED: &str = "voice.session.ended";
149pub const VOICE_SESSION_FAILED: &str = "voice.session.failed";
150
151pub const VALID_EVENT_TYPES: &[&str] = &[
155 INPUT_MESSAGE,
156 OUTPUT_MESSAGE_STARTED,
157 OUTPUT_MESSAGE_DELTA,
158 OUTPUT_MESSAGE_COMPLETED,
159 OUTPUT_MESSAGE_REPLACED,
160 TURN_STARTED,
161 TURN_COMPLETED,
162 TURN_FAILED,
163 TURN_SEALED,
164 TURN_CANCELLED,
165 REASON_STARTED,
166 REASON_COMPLETED,
167 REASON_RECOVERED,
168 ACT_STARTED,
169 ACT_COMPLETED,
170 TOOL_STARTED,
171 TOOL_COMPLETED,
172 TOOL_PROGRESS,
173 TOOL_OUTPUT_DELTA,
174 TOOL_CALL_REQUESTED,
175 TRANSCRIPT_REPAIRED,
176 TOOL_CALL_REPAIRED,
177 LLM_GENERATION,
178 REASON_THINKING_STARTED,
179 REASON_THINKING_DELTA,
180 REASON_THINKING_COMPLETED,
181 REASON_ITEM,
182 SESSION_STARTED,
183 SESSION_ACTIVATED,
184 SESSION_IDLED,
185 SCHEDULE_TRIGGERED,
186 CONTEXT_COMPACTING,
187 CONTEXT_COMPACTED,
188 BUDGET_WARNING,
189 BUDGET_PAUSED,
190 BUDGET_EXHAUSTED,
191 BUDGET_RESUMED,
192 VOICE_SESSION_STARTED,
193 VOICE_INPUT_TRANSCRIPT_DELTA,
194 VOICE_INPUT_TRANSCRIPT_COMPLETED,
195 VOICE_OUTPUT_TRANSCRIPT_DELTA,
196 VOICE_OUTPUT_TRANSCRIPT_COMPLETED,
197 VOICE_SESSION_ENDED,
198 VOICE_SESSION_FAILED,
199 FILE_WRITTEN,
200 CAPABILITY_USAGE,
201];
202
203use crate::atoms::AtomContext;
208
209#[derive(Debug, Clone, Serialize, Deserialize, Default)]
216#[cfg_attr(feature = "openapi", derive(ToSchema))]
217pub struct EventContext {
218 #[serde(skip_serializing_if = "Option::is_none")]
220 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "turn_01933b5a00007000800000000000001"))]
221 pub turn_id: Option<TurnId>,
222
223 #[serde(skip_serializing_if = "Option::is_none")]
225 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "message_01933b5a00007000800000000000001"))]
226 pub input_message_id: Option<MessageId>,
227
228 #[serde(skip_serializing_if = "Option::is_none")]
230 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "exec_01933b5a00007000800000000000001"))]
231 pub exec_id: Option<ExecId>,
232
233 #[serde(skip_serializing_if = "Option::is_none")]
236 pub trace_id: Option<String>,
237
238 #[serde(skip_serializing_if = "Option::is_none")]
241 pub span_id: Option<String>,
242
243 #[serde(skip_serializing_if = "Option::is_none")]
246 pub parent_span_id: Option<String>,
247}
248
249impl EventContext {
250 pub fn empty() -> Self {
252 Self::default()
253 }
254
255 pub fn from_atom_context(ctx: &AtomContext) -> Self {
257 Self {
258 turn_id: Some(ctx.turn_id),
259 input_message_id: Some(ctx.input_message_id),
260 exec_id: Some(ctx.exec_id),
261 trace_id: None,
262 span_id: None,
263 parent_span_id: None,
264 }
265 }
266
267 pub fn turn(turn_id: TurnId, input_message_id: MessageId) -> Self {
269 Self {
270 turn_id: Some(turn_id),
271 input_message_id: Some(input_message_id),
272 exec_id: None,
273 trace_id: None,
274 span_id: None,
275 parent_span_id: None,
276 }
277 }
278
279 pub fn with_span(
281 mut self,
282 trace_id: String,
283 span_id: String,
284 parent_span_id: Option<String>,
285 ) -> Self {
286 self.trace_id = Some(trace_id);
287 self.span_id = Some(span_id);
288 self.parent_span_id = parent_span_id;
289 self
290 }
291}
292
293#[derive(Debug, Clone, Serialize)]
309#[cfg_attr(feature = "openapi", derive(ToSchema))]
310pub struct Event {
311 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "event_01933b5a00007000800000000000001"))]
313 pub id: EventId,
314
315 #[serde(rename = "type")]
317 pub event_type: String,
318
319 pub ts: DateTime<Utc>,
321
322 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "session_01933b5a00007000800000000000001"))]
324 pub session_id: SessionId,
325
326 pub context: EventContext,
328
329 pub data: EventData,
332
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub metadata: Option<serde_json::Value>,
336
337 #[serde(skip_serializing_if = "Option::is_none")]
339 pub tags: Option<Vec<String>>,
340
341 #[serde(skip_serializing_if = "Option::is_none")]
343 pub sequence: Option<i32>,
344}
345
346#[derive(Debug, Deserialize)]
347struct RawEvent {
348 id: EventId,
349 #[serde(rename = "type")]
350 event_type: String,
351 ts: DateTime<Utc>,
352 session_id: SessionId,
353 context: EventContext,
354 data: serde_json::Value,
355 metadata: Option<serde_json::Value>,
356 tags: Option<Vec<String>>,
357 sequence: Option<i32>,
358}
359
360impl<'de> Deserialize<'de> for Event {
361 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
362 where
363 D: Deserializer<'de>,
364 {
365 let raw = RawEvent::deserialize(deserializer)?;
366 let data = deserialize_event_data(&raw.event_type, raw.data);
367 Ok(Self {
368 id: raw.id,
369 event_type: raw.event_type,
370 ts: raw.ts,
371 session_id: raw.session_id,
372 context: raw.context,
373 data,
374 metadata: raw.metadata,
375 tags: raw.tags,
376 sequence: raw.sequence,
377 })
378 }
379}
380
381impl Event {
382 pub fn new(session_id: SessionId, context: EventContext, data: impl Into<EventData>) -> Self {
386 let data = data.into();
387 let event_type = data.event_type().to_string();
388 Self {
389 id: EventId::new(),
390 event_type,
391 ts: Utc::now(),
392 session_id,
393 context,
394 data,
395 metadata: None,
396 tags: None,
397 sequence: None,
398 }
399 }
400
401 pub fn with_id(
403 id: EventId,
404 session_id: SessionId,
405 context: EventContext,
406 data: impl Into<EventData>,
407 ) -> Self {
408 let data = data.into();
409 let event_type = data.event_type().to_string();
410 Self {
411 id,
412 event_type,
413 ts: Utc::now(),
414 session_id,
415 context,
416 data,
417 metadata: None,
418 tags: None,
419 sequence: None,
420 }
421 }
422
423 pub fn with_sequence(mut self, sequence: i32) -> Self {
425 self.sequence = Some(sequence);
426 self
427 }
428
429 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
431 self.metadata = Some(metadata);
432 self
433 }
434
435 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
437 self.tags = Some(tags);
438 self
439 }
440
441 pub fn session_uuid(&self) -> Uuid {
443 self.session_id.uuid()
444 }
445
446 pub fn is_message_event(&self) -> bool {
448 self.event_type == INPUT_MESSAGE || self.event_type == OUTPUT_MESSAGE_COMPLETED
449 }
450
451 pub fn is_ephemeral(&self) -> bool {
459 is_ephemeral_event_type(&self.event_type)
460 }
461
462 pub fn is_input_event(&self) -> bool {
464 self.event_type.starts_with("input.")
465 }
466
467 pub fn is_output_event(&self) -> bool {
469 self.event_type.starts_with("output.")
470 }
471
472 pub fn is_atom_event(&self) -> bool {
474 matches!(
475 self.event_type.as_str(),
476 REASON_STARTED
477 | REASON_COMPLETED
478 | REASON_RECOVERED
479 | ACT_STARTED
480 | ACT_COMPLETED
481 | TOOL_STARTED
482 | TOOL_COMPLETED
483 | TOOL_PROGRESS
484 | TOOL_CALL_REQUESTED
485 | TRANSCRIPT_REPAIRED
486 )
487 }
488
489 pub fn is_turn_event(&self) -> bool {
491 self.event_type.starts_with("turn.")
492 }
493
494 pub fn is_session_event(&self) -> bool {
496 self.event_type.starts_with("session.")
497 }
498
499 pub fn is_unsupported(&self) -> bool {
502 self.data.is_unsupported()
503 }
504}
505
506use crate::message::{ContentPart, ExecutionPhase, Message};
511use crate::tool_narration::{
512 ToolNarrationPhase, render_group_headline_with_locale, render_tool_narration_with_locale,
513};
514use crate::tool_types::ToolCall;
515
516#[derive(Debug, Clone, Serialize, Deserialize)]
518#[cfg_attr(feature = "openapi", derive(ToSchema))]
519pub struct ModelMetadata {
520 pub model: String,
522
523 #[serde(skip_serializing_if = "Option::is_none")]
525 pub model_id: Option<Uuid>,
526
527 #[serde(skip_serializing_if = "Option::is_none")]
529 pub provider_id: Option<Uuid>,
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize, Default)]
559#[cfg_attr(feature = "openapi", derive(ToSchema))]
560pub struct TokenUsage {
561 pub input_tokens: u32,
564 pub output_tokens: u32,
566 #[serde(skip_serializing_if = "Option::is_none")]
568 pub cache_read_tokens: Option<u32>,
569 #[serde(skip_serializing_if = "Option::is_none")]
571 pub cache_creation_tokens: Option<u32>,
572
573 #[serde(skip_serializing_if = "Option::is_none")]
577 pub actual_cost_usd: Option<f64>,
578
579 #[serde(skip_serializing_if = "Option::is_none")]
584 pub estimated_cost_usd: Option<f64>,
585
586 #[serde(skip_serializing_if = "Option::is_none")]
589 pub effective_cost_usd: Option<f64>,
590}
591
592impl TokenUsage {
593 pub fn new(input_tokens: u32, output_tokens: u32) -> Self {
595 Self {
596 input_tokens,
597 output_tokens,
598 cache_read_tokens: None,
599 cache_creation_tokens: None,
600 actual_cost_usd: None,
601 estimated_cost_usd: None,
602 effective_cost_usd: None,
603 }
604 }
605
606 pub fn with_cache(
608 input_tokens: u32,
609 output_tokens: u32,
610 cache_read_tokens: Option<u32>,
611 cache_creation_tokens: Option<u32>,
612 ) -> Self {
613 Self {
614 input_tokens,
615 output_tokens,
616 cache_read_tokens,
617 cache_creation_tokens,
618 actual_cost_usd: None,
619 estimated_cost_usd: None,
620 effective_cost_usd: None,
621 }
622 }
623
624 pub fn with_cost(
628 mut self,
629 actual_cost_usd: Option<f64>,
630 estimated_cost_usd: Option<f64>,
631 ) -> Self {
632 self.actual_cost_usd = actual_cost_usd;
633 self.estimated_cost_usd = estimated_cost_usd;
634 self
635 }
636
637 pub fn with_effective_cost(mut self, effective_cost_usd: Option<f64>) -> Self {
641 self.effective_cost_usd = effective_cost_usd;
642 self
643 }
644
645 pub fn effective_cost_usd(&self) -> Option<f64> {
649 self.effective_cost_usd
650 .or(self.actual_cost_usd.or(self.estimated_cost_usd))
651 }
652
653 pub fn total_tokens(&self) -> u32 {
655 self.input_tokens + self.output_tokens
656 }
657
658 pub fn add(&mut self, other: &TokenUsage) {
660 let current_cost = self.effective_cost_usd();
665 self.input_tokens += other.input_tokens;
666 self.output_tokens += other.output_tokens;
667 if let Some(cache) = other.cache_read_tokens {
668 *self.cache_read_tokens.get_or_insert(0) += cache;
669 }
670 if let Some(cache) = other.cache_creation_tokens {
671 *self.cache_creation_tokens.get_or_insert(0) += cache;
672 }
673 if let Some(cost) = other.actual_cost_usd {
674 *self.actual_cost_usd.get_or_insert(0.0) += cost;
675 }
676 if let Some(cost) = other.estimated_cost_usd {
677 *self.estimated_cost_usd.get_or_insert(0.0) += cost;
678 }
679 if let Some(cost) = other.effective_cost_usd() {
680 *self
681 .effective_cost_usd
682 .get_or_insert(current_cost.unwrap_or(0.0)) += cost;
683 }
684 }
685}
686
687#[cfg(test)]
688mod token_usage_tests {
689 use super::TokenUsage;
690
691 #[test]
692 fn aggregate_effective_cost_preserves_mixed_actual_and_estimated_total() {
693 let aggregate = TokenUsage::with_cache(30, 15, None, None)
694 .with_cost(Some(1.0), Some(2.0))
695 .with_effective_cost(Some(3.0));
696
697 assert_eq!(aggregate.actual_cost_usd, Some(1.0));
698 assert_eq!(aggregate.estimated_cost_usd, Some(2.0));
699 assert_eq!(aggregate.effective_cost_usd(), Some(3.0));
700 }
701
702 #[test]
703 fn generation_effective_cost_still_prefers_actual_over_estimated() {
704 let generation = TokenUsage::new(10, 5).with_cost(Some(1.0), Some(2.0));
705
706 assert_eq!(generation.effective_cost_usd(), Some(1.0));
707 }
708
709 #[test]
710 fn add_seeds_effective_cost_from_accumulator_implicit_cost() {
711 let mut aggregate = TokenUsage::new(10, 5).with_cost(Some(1.0), Some(10.0));
712 let generation = TokenUsage::new(20, 10).with_cost(None, Some(2.0));
713
714 aggregate.add(&generation);
715
716 assert_eq!(aggregate.actual_cost_usd, Some(1.0));
717 assert_eq!(aggregate.estimated_cost_usd, Some(12.0));
718 assert_eq!(aggregate.effective_cost_usd(), Some(3.0));
719 }
720
721 #[test]
722 fn add_effective_cost_does_not_double_count_when_actual_is_mutated() {
723 let mut aggregate = TokenUsage::new(10, 5).with_cost(Some(1.0), None);
728 let generation = TokenUsage::new(20, 10).with_cost(Some(3.0), None);
729
730 aggregate.add(&generation);
731
732 assert_eq!(aggregate.actual_cost_usd, Some(4.0));
733 assert_eq!(aggregate.effective_cost_usd(), Some(4.0));
734 }
735}
736
737#[derive(Debug, Clone, Serialize, Deserialize)]
739#[cfg_attr(feature = "openapi", derive(ToSchema))]
740pub struct InputMessageData {
741 pub message: Message,
743}
744
745impl InputMessageData {
746 pub fn new(message: Message) -> Self {
747 Self { message }
748 }
749}
750
751#[derive(Debug, Clone, Serialize, Deserialize)]
760#[cfg_attr(feature = "openapi", derive(ToSchema))]
761pub struct OutputMessageStartedData {
762 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
764 pub turn_id: TurnId,
765
766 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_550e8400e29b41d4a716446655440000"))]
769 pub message_id: MessageId,
770
771 #[serde(skip_serializing_if = "Option::is_none")]
773 pub model: Option<String>,
774
775 #[serde(skip_serializing_if = "Option::is_none")]
778 pub iteration: Option<u32>,
779
780 #[serde(default, skip_serializing_if = "Option::is_none")]
790 pub phase: Option<ExecutionPhase>,
791}
792
793#[derive(Debug, Clone, Serialize, Deserialize)]
798#[cfg_attr(feature = "openapi", derive(ToSchema))]
799pub struct OutputMessageDeltaData {
800 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
802 pub turn_id: TurnId,
803
804 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_550e8400e29b41d4a716446655440000"))]
807 pub message_id: MessageId,
808
809 pub delta: String,
811
812 pub accumulated: String,
814
815 #[serde(default, skip_serializing_if = "Option::is_none")]
826 pub phase: Option<ExecutionPhase>,
827}
828
829#[derive(Debug, Clone, Serialize, Deserialize)]
831#[cfg_attr(feature = "openapi", derive(ToSchema))]
832pub struct OutputMessageCompletedData {
833 pub message: Message,
835
836 #[serde(skip_serializing_if = "Option::is_none")]
838 pub metadata: Option<ModelMetadata>,
839
840 #[serde(skip_serializing_if = "Option::is_none")]
842 pub usage: Option<TokenUsage>,
843
844 #[serde(default, skip_serializing_if = "Option::is_none")]
846 pub error_code: Option<String>,
847
848 #[serde(default, skip_serializing_if = "Option::is_none")]
850 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
851 pub error_fields: Option<UserFacingErrorFields>,
852
853 #[serde(default, skip_serializing_if = "Option::is_none")]
857 pub error_disclosure: Option<String>,
858}
859
860impl OutputMessageCompletedData {
861 pub fn new(message: Message) -> Self {
862 Self {
863 message,
864 metadata: None,
865 usage: None,
866 error_code: None,
867 error_fields: None,
868 error_disclosure: None,
869 }
870 }
871
872 pub fn with_metadata(mut self, metadata: ModelMetadata) -> Self {
873 self.metadata = Some(metadata);
874 self
875 }
876
877 pub fn with_usage(mut self, usage: TokenUsage) -> Self {
878 self.usage = Some(usage);
879 self
880 }
881
882 pub fn with_user_facing_error(mut self, error: &UserFacingError) -> Self {
883 error.apply_to_event_fields(&mut self.error_code, &mut self.error_fields);
884 self
885 }
886
887 pub fn with_error_disclosure(mut self, mode: crate::ErrorDisclosure) -> Self {
888 self.error_disclosure = Some(mode.as_str().to_string());
889 self
890 }
891}
892
893#[derive(Debug, Clone, Serialize, Deserialize)]
900#[cfg_attr(feature = "openapi", derive(ToSchema))]
901pub struct OutputMessageReplacedData {
902 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
904 pub turn_id: TurnId,
905
906 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_550e8400e29b41d4a716446655440000"))]
909 pub message_id: MessageId,
910
911 pub guardrail_capability_id: String,
914
915 pub guardrail_id: String,
917
918 pub reason_code: String,
921
922 pub replacement: String,
924}
925
926#[derive(Debug, Clone, Serialize, Deserialize)]
932#[cfg_attr(feature = "openapi", derive(ToSchema))]
933pub struct ReasonStartedData {
934 pub harness_id: HarnessId,
936
937 #[serde(skip_serializing_if = "Option::is_none")]
939 pub agent_id: Option<AgentId>,
940
941 #[serde(skip_serializing_if = "Option::is_none")]
943 pub metadata: Option<ModelMetadata>,
944}
945
946#[derive(Debug, Clone, Serialize, Deserialize)]
948#[cfg_attr(feature = "openapi", derive(ToSchema))]
949pub struct ReasonCompletedData {
950 pub success: bool,
952
953 #[serde(skip_serializing_if = "Option::is_none")]
955 pub text_preview: Option<String>,
956
957 pub has_tool_calls: bool,
959
960 pub tool_call_count: u32,
962
963 #[serde(skip_serializing_if = "Option::is_none")]
965 pub error: Option<String>,
966
967 #[serde(skip_serializing_if = "Option::is_none")]
969 pub duration_ms: Option<u64>,
970
971 #[serde(skip_serializing_if = "Option::is_none")]
973 pub usage: Option<TokenUsage>,
974}
975
976impl ReasonCompletedData {
977 pub fn success(
978 text: &str,
979 has_tool_calls: bool,
980 tool_call_count: u32,
981 duration_ms: Option<u64>,
982 usage: Option<TokenUsage>,
983 ) -> Self {
984 let text_preview = if text.is_empty() {
985 None
986 } else {
987 Some(text.chars().take(200).collect())
988 };
989
990 Self {
991 success: true,
992 text_preview,
993 has_tool_calls,
994 tool_call_count,
995 error: None,
996 duration_ms,
997 usage,
998 }
999 }
1000
1001 pub fn failure(error: String, duration_ms: Option<u64>) -> Self {
1002 Self {
1003 success: false,
1004 text_preview: None,
1005 has_tool_calls: false,
1006 tool_call_count: 0,
1007 error: Some(error),
1008 duration_ms,
1009 usage: None,
1010 }
1011 }
1012}
1013
1014#[derive(Debug, Clone, Serialize, Deserialize)]
1016#[cfg_attr(feature = "openapi", derive(ToSchema))]
1017#[serde(rename_all = "snake_case")]
1018pub enum RecoveryMode {
1019 Finalize,
1022 Restart,
1024}
1025
1026#[derive(Debug, Clone, Serialize, Deserialize)]
1032#[cfg_attr(feature = "openapi", derive(ToSchema))]
1033pub struct ReasonRecoveredData {
1034 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1036 pub turn_id: TurnId,
1037
1038 pub mode: RecoveryMode,
1040
1041 pub accumulated_len: usize,
1043}
1044
1045#[derive(Debug, Clone, Serialize, Deserialize)]
1047#[serde(rename_all = "snake_case")]
1048#[cfg_attr(feature = "openapi", derive(ToSchema))]
1049pub enum CapabilityUsageKind {
1050 Configured,
1051 Resolved,
1052 Exposed,
1053 Invoked,
1054 EffectRan,
1055}
1056
1057#[derive(Debug, Clone, Serialize, Deserialize)]
1061#[cfg_attr(feature = "openapi", derive(ToSchema))]
1062pub struct CapabilityUsageRecord {
1063 pub capability_id: String,
1065 #[serde(default, skip_serializing_if = "Option::is_none")]
1067 pub capability_name: Option<String>,
1068 pub usage_kind: CapabilityUsageKind,
1070 #[serde(default, skip_serializing_if = "Option::is_none")]
1072 pub tool_name: Option<String>,
1073 #[serde(default, skip_serializing_if = "Option::is_none")]
1075 pub usage_count: Option<u64>,
1076 #[serde(default, skip_serializing_if = "Option::is_none")]
1078 pub duration_ms: Option<u64>,
1079}
1080
1081#[derive(Debug, Clone, Serialize, Deserialize)]
1083#[cfg_attr(feature = "openapi", derive(ToSchema))]
1084pub struct CapabilityUsageData {
1085 pub records: Vec<CapabilityUsageRecord>,
1086}
1087
1088#[derive(Debug, Clone, Serialize, Deserialize)]
1090#[cfg_attr(feature = "openapi", derive(ToSchema))]
1091pub struct ToolCallSummary {
1092 pub id: String,
1093 pub name: String,
1094 #[serde(default, skip_serializing_if = "Option::is_none")]
1096 pub display_name: Option<String>,
1097 #[serde(default, skip_serializing_if = "Option::is_none")]
1099 pub narration: Option<String>,
1100}
1101
1102impl From<&ToolCall> for ToolCallSummary {
1103 fn from(tc: &ToolCall) -> Self {
1104 Self {
1105 id: tc.id.clone(),
1106 name: tc.name.clone(),
1107 display_name: None,
1108 narration: None,
1109 }
1110 }
1111}
1112
1113#[derive(Debug, Clone, Serialize, Deserialize)]
1115#[cfg_attr(feature = "openapi", derive(ToSchema))]
1116pub struct ToolDefinitionSummary {
1117 pub name: String,
1119 #[serde(default, skip_serializing_if = "Option::is_none")]
1121 pub display_name: Option<String>,
1122 #[serde(default, skip_serializing_if = "Option::is_none")]
1124 pub category: Option<String>,
1125 #[serde(default, skip_serializing_if = "Option::is_none")]
1127 pub capability_id: Option<String>,
1128 #[serde(default, skip_serializing_if = "Option::is_none")]
1130 pub capability_name: Option<String>,
1131 pub description: String,
1133}
1134
1135impl From<&crate::tool_types::ToolDefinition> for ToolDefinitionSummary {
1136 fn from(tool: &crate::tool_types::ToolDefinition) -> Self {
1137 let capability_attribution = tool.capability_attribution();
1138 Self {
1139 name: tool.name().to_string(),
1140 display_name: tool.display_name().map(|s| s.to_string()),
1141 category: tool.category().map(|s| s.to_string()),
1142 capability_id: capability_attribution.map(|(id, _)| id.to_string()),
1143 capability_name: capability_attribution.and_then(|(_, name)| name.map(str::to_string)),
1144 description: tool.description().to_string(),
1145 }
1146 }
1147}
1148
1149#[derive(Debug, Clone, Serialize, Deserialize)]
1151#[cfg_attr(feature = "openapi", derive(ToSchema))]
1152pub struct ActStartedData {
1153 pub tool_calls: Vec<ToolCallSummary>,
1155 #[serde(default, skip_serializing_if = "Option::is_none")]
1157 pub headline: Option<String>,
1158}
1159
1160impl ActStartedData {
1161 pub fn new(tool_calls: &[ToolCall]) -> Self {
1162 Self::new_with_locale(tool_calls, None)
1163 }
1164
1165 pub fn new_with_locale(tool_calls: &[ToolCall], locale: Option<&str>) -> Self {
1166 Self {
1167 tool_calls: tool_calls.iter().map(ToolCallSummary::from).collect(),
1168 headline: render_group_headline_with_locale(
1169 tool_calls,
1170 &[],
1171 ToolNarrationPhase::Started,
1172 locale,
1173 ),
1174 }
1175 }
1176
1177 pub fn with_definitions(
1179 tool_calls: &[ToolCall],
1180 tool_defs: &[crate::tool_types::ToolDefinition],
1181 ) -> Self {
1182 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
1183 }
1184
1185 pub fn with_definitions_and_locale(
1186 tool_calls: &[ToolCall],
1187 tool_defs: &[crate::tool_types::ToolDefinition],
1188 locale: Option<&str>,
1189 ) -> Self {
1190 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
1191 tool_defs.iter().map(|d| (d.name(), d)).collect();
1192 Self {
1193 tool_calls: tool_calls
1194 .iter()
1195 .map(|tc| {
1196 let tool_def = def_map.get(tc.name.as_str()).copied();
1197 let display_name = localized_tool_display_name(
1198 &tc.name,
1199 tool_def.and_then(|d| d.display_name()),
1200 locale,
1201 );
1202 ToolCallSummary {
1203 id: tc.id.clone(),
1204 name: tc.name.clone(),
1205 display_name,
1206 narration: Some(render_tool_narration_with_locale(
1207 tool_def,
1208 tc,
1209 ToolNarrationPhase::Started,
1210 locale,
1211 )),
1212 }
1213 })
1214 .collect(),
1215 headline: render_group_headline_with_locale(
1216 tool_calls,
1217 tool_defs,
1218 ToolNarrationPhase::Started,
1219 locale,
1220 ),
1221 }
1222 }
1223}
1224
1225#[derive(Debug, Clone, Serialize, Deserialize)]
1227#[cfg_attr(feature = "openapi", derive(ToSchema))]
1228pub struct ActCompletedData {
1229 pub completed: bool,
1231
1232 pub success_count: u32,
1234
1235 pub error_count: u32,
1237
1238 #[serde(skip_serializing_if = "Option::is_none")]
1240 pub duration_ms: Option<u64>,
1241 #[serde(default, skip_serializing_if = "Option::is_none")]
1243 pub headline: Option<String>,
1244}
1245
1246#[derive(Debug, Clone, Serialize, Deserialize)]
1248#[cfg_attr(feature = "openapi", derive(ToSchema))]
1249pub struct ToolStartedData {
1250 pub tool_call: ToolCall,
1252 #[serde(default, skip_serializing_if = "Option::is_none")]
1254 pub tool_call_fingerprint: Option<String>,
1255 #[serde(default, skip_serializing_if = "Option::is_none")]
1257 pub display_name: Option<String>,
1258 #[serde(default, skip_serializing_if = "Option::is_none")]
1260 pub narration: Option<String>,
1261}
1262
1263#[derive(Debug, Clone, Serialize, Deserialize)]
1265#[cfg_attr(feature = "openapi", derive(ToSchema))]
1266pub struct ToolCompletedData {
1267 pub tool_call_id: String,
1269
1270 pub tool_name: String,
1272
1273 #[serde(default, skip_serializing_if = "Option::is_none")]
1275 pub tool_call_fingerprint: Option<String>,
1276
1277 #[serde(default, skip_serializing_if = "Option::is_none")]
1279 pub tool_result_fingerprint: Option<String>,
1280
1281 #[serde(default, skip_serializing_if = "Option::is_none")]
1283 pub display_name: Option<String>,
1284
1285 pub success: bool,
1287
1288 pub status: String,
1290
1291 #[serde(skip_serializing_if = "Option::is_none")]
1293 pub result: Option<Vec<ContentPart>>,
1294
1295 #[serde(skip_serializing_if = "Option::is_none")]
1297 pub error: Option<String>,
1298
1299 #[serde(skip_serializing_if = "Option::is_none")]
1301 pub duration_ms: Option<u64>,
1302
1303 #[serde(default, skip_serializing_if = "Option::is_none")]
1305 pub capability_id: Option<String>,
1306
1307 #[serde(default, skip_serializing_if = "Option::is_none")]
1309 pub capability_name: Option<String>,
1310
1311 #[serde(default, skip_serializing_if = "Option::is_none")]
1313 pub narration: Option<String>,
1314}
1315
1316impl ToolCompletedData {
1317 pub fn success(
1318 tool_call_id: String,
1319 tool_name: String,
1320 result: Vec<ContentPart>,
1321 duration_ms: Option<u64>,
1322 ) -> Self {
1323 Self {
1324 tool_call_id,
1325 tool_name,
1326 tool_call_fingerprint: None,
1327 tool_result_fingerprint: None,
1328 display_name: None,
1329 success: true,
1330 status: "success".to_string(),
1331 result: Some(result),
1332 error: None,
1333 duration_ms,
1334 capability_id: None,
1335 capability_name: None,
1336 narration: None,
1337 }
1338 }
1339
1340 pub fn failure(
1341 tool_call_id: String,
1342 tool_name: String,
1343 status: String,
1344 error: String,
1345 duration_ms: Option<u64>,
1346 ) -> Self {
1347 Self {
1348 tool_call_id,
1349 tool_name,
1350 tool_call_fingerprint: None,
1351 tool_result_fingerprint: None,
1352 display_name: None,
1353 success: false,
1354 status,
1355 result: None,
1356 error: Some(error),
1357 duration_ms,
1358 capability_id: None,
1359 capability_name: None,
1360 narration: None,
1361 }
1362 }
1363
1364 pub fn with_display_name(mut self, display_name: Option<String>) -> Self {
1366 self.display_name = display_name;
1367 self
1368 }
1369
1370 pub fn with_fingerprints(
1371 mut self,
1372 tool_call_fingerprint: String,
1373 tool_result_fingerprint: String,
1374 ) -> Self {
1375 self.tool_call_fingerprint = Some(tool_call_fingerprint);
1376 self.tool_result_fingerprint = Some(tool_result_fingerprint);
1377 self
1378 }
1379
1380 pub fn with_narration(mut self, narration: Option<String>) -> Self {
1382 self.narration = narration;
1383 self
1384 }
1385
1386 pub fn with_capability_attribution(
1388 mut self,
1389 capability_id: Option<String>,
1390 capability_name: Option<String>,
1391 ) -> Self {
1392 self.capability_id = capability_id;
1393 self.capability_name = capability_name;
1394 self
1395 }
1396}
1397
1398#[derive(Debug, Clone, Serialize, Deserialize)]
1404#[cfg_attr(feature = "openapi", derive(ToSchema))]
1405pub struct ToolProgressData {
1406 pub tool_call_id: String,
1408
1409 pub tool_name: String,
1411
1412 pub message: String,
1414
1415 #[serde(default, skip_serializing_if = "Option::is_none")]
1417 pub display_name: Option<String>,
1418}
1419
1420#[derive(Debug, Clone, Serialize, Deserialize)]
1430#[cfg_attr(feature = "openapi", derive(ToSchema))]
1431pub struct ToolOutputDeltaData {
1432 pub tool_call_id: String,
1434
1435 pub tool_name: String,
1437
1438 pub delta: String,
1440
1441 pub stream: String,
1443}
1444
1445#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1447#[cfg_attr(feature = "openapi", derive(ToSchema))]
1448#[serde(rename_all = "snake_case")]
1449pub enum TranscriptRepairAction {
1450 Replay,
1452 Synthesize,
1454}
1455
1456#[derive(Debug, Clone, Serialize, Deserialize)]
1462#[cfg_attr(feature = "openapi", derive(ToSchema))]
1463pub struct TranscriptRepairedData {
1464 pub tool_call_id: String,
1466
1467 #[serde(default, skip_serializing_if = "Option::is_none")]
1469 pub tool_name: Option<String>,
1470
1471 pub action: TranscriptRepairAction,
1473}
1474
1475#[derive(Debug, Clone, Serialize, Deserialize)]
1481#[cfg_attr(feature = "openapi", derive(ToSchema))]
1482pub struct ToolCallRepairedData {
1483 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1485 pub turn_id: TurnId,
1486
1487 pub tool_call_id: String,
1489
1490 pub tool_name: String,
1492
1493 pub outcome: String,
1495}
1496
1497#[derive(Debug, Clone, Serialize, Deserialize)]
1502#[cfg_attr(feature = "openapi", derive(ToSchema))]
1503pub struct ToolCallRequestedData {
1504 pub tool_calls: Vec<ToolCall>,
1506 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1508 pub tool_summaries: Vec<ToolCallSummary>,
1509 #[serde(default, skip_serializing_if = "Option::is_none")]
1511 pub headline: Option<String>,
1512}
1513
1514impl ToolCallRequestedData {
1515 pub fn with_definitions(
1516 tool_calls: &[ToolCall],
1517 tool_defs: &[crate::tool_types::ToolDefinition],
1518 ) -> Self {
1519 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
1520 }
1521
1522 pub fn with_definitions_and_locale(
1523 tool_calls: &[ToolCall],
1524 tool_defs: &[crate::tool_types::ToolDefinition],
1525 locale: Option<&str>,
1526 ) -> Self {
1527 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
1528 tool_defs.iter().map(|d| (d.name(), d)).collect();
1529
1530 let tool_summaries = tool_calls
1531 .iter()
1532 .map(|tool_call| {
1533 let tool_def = def_map.get(tool_call.name.as_str()).copied();
1534 ToolCallSummary {
1535 id: tool_call.id.clone(),
1536 name: tool_call.name.clone(),
1537 display_name: localized_tool_display_name(
1538 &tool_call.name,
1539 tool_def.and_then(|def| def.display_name()),
1540 locale,
1541 ),
1542 narration: Some(render_tool_narration_with_locale(
1543 tool_def,
1544 tool_call,
1545 ToolNarrationPhase::Waiting,
1546 locale,
1547 )),
1548 }
1549 })
1550 .collect();
1551
1552 Self {
1553 tool_calls: tool_calls.to_vec(),
1554 tool_summaries,
1555 headline: render_group_headline_with_locale(
1556 tool_calls,
1557 tool_defs,
1558 ToolNarrationPhase::Waiting,
1559 locale,
1560 ),
1561 }
1562 }
1563}
1564
1565#[derive(Debug, Clone, Serialize, Deserialize)]
1571#[cfg_attr(feature = "openapi", derive(ToSchema))]
1572pub struct LlmGenerationOutput {
1573 #[serde(skip_serializing_if = "Option::is_none")]
1575 pub text: Option<String>,
1576
1577 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1579 pub tool_calls: Vec<ToolCall>,
1580}
1581
1582#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1587#[cfg_attr(feature = "openapi", derive(ToSchema))]
1588pub struct LlmRequestOptions {
1589 #[serde(skip_serializing_if = "Option::is_none")]
1591 pub prompt_cache: Option<LlmPromptCacheInfo>,
1592 #[serde(skip_serializing_if = "Option::is_none")]
1594 pub tool_search: Option<LlmToolSearchInfo>,
1595 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1597 pub provider_options: HashMap<String, Value>,
1598 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1601 pub metadata: HashMap<String, String>,
1602}
1603
1604impl LlmRequestOptions {
1605 pub fn is_empty(&self) -> bool {
1606 self.prompt_cache.is_none()
1607 && self.tool_search.is_none()
1608 && self.provider_options.is_empty()
1609 && self.metadata.is_empty()
1610 }
1611}
1612
1613#[derive(Debug, Clone, Serialize, Deserialize)]
1615#[cfg_attr(feature = "openapi", derive(ToSchema))]
1616pub struct LlmPromptCacheInfo {
1617 pub enabled: bool,
1619 pub strategy: crate::driver_registry::PromptCacheStrategy,
1621 #[serde(skip_serializing_if = "Option::is_none")]
1623 pub provider_mode: Option<String>,
1624}
1625
1626#[derive(Debug, Clone, Serialize, Deserialize)]
1628#[cfg_attr(feature = "openapi", derive(ToSchema))]
1629pub struct LlmToolSearchInfo {
1630 pub enabled: bool,
1632 pub threshold: usize,
1634}
1635
1636#[derive(Debug, Clone, Serialize, Deserialize)]
1638#[cfg_attr(feature = "openapi", derive(ToSchema))]
1639pub struct LlmGenerationMetadata {
1640 #[cfg_attr(feature = "openapi", schema(example = "claude-sonnet-4-5"))]
1642 pub model: String,
1643
1644 #[serde(skip_serializing_if = "Option::is_none")]
1646 #[cfg_attr(feature = "openapi", schema(example = "anthropic"))]
1647 pub provider: Option<String>,
1648
1649 #[serde(skip_serializing_if = "Option::is_none")]
1651 pub usage: Option<TokenUsage>,
1652
1653 #[serde(skip_serializing_if = "Option::is_none")]
1655 #[cfg_attr(feature = "openapi", schema(example = 1_842u64))]
1656 pub duration_ms: Option<u64>,
1657
1658 #[serde(skip_serializing_if = "Option::is_none")]
1660 #[cfg_attr(feature = "openapi", schema(example = 312u64))]
1661 pub time_to_first_token_ms: Option<u64>,
1662
1663 #[cfg_attr(feature = "openapi", schema(example = true))]
1665 pub success: bool,
1666
1667 #[serde(skip_serializing_if = "Option::is_none")]
1669 #[cfg_attr(feature = "openapi", schema(example = "provider returned 503"))]
1670 pub error: Option<String>,
1671
1672 #[serde(skip_serializing_if = "Option::is_none")]
1675 #[cfg_attr(feature = "openapi", schema(example = json!(["tool_calls"])))]
1676 pub finish_reasons: Option<Vec<String>>,
1677
1678 #[serde(skip_serializing_if = "Option::is_none")]
1681 #[cfg_attr(feature = "openapi", schema(example = "msg_01ABCDef0123456789"))]
1682 pub response_id: Option<String>,
1683
1684 #[serde(skip_serializing_if = "Option::is_none")]
1687 pub retry: Option<LlmRetryInfo>,
1688
1689 #[serde(skip_serializing_if = "Option::is_none")]
1692 pub compaction: Option<LlmCompactionInfo>,
1693
1694 #[serde(skip_serializing_if = "Option::is_none")]
1696 pub request_options: Option<LlmRequestOptions>,
1697}
1698
1699#[derive(Debug, Clone, Serialize, Deserialize)]
1701#[cfg_attr(feature = "openapi", derive(ToSchema))]
1702pub struct LlmRetryInfo {
1703 pub attempts: u32,
1705
1706 pub total_wait_ms: u64,
1708}
1709
1710#[derive(Debug, Clone, Serialize, Deserialize)]
1715#[cfg_attr(feature = "openapi", derive(ToSchema))]
1716pub struct LlmCompactionInfo {
1717 pub compacted: bool,
1719
1720 #[serde(skip_serializing_if = "Option::is_none")]
1722 pub input_tokens_before: Option<u32>,
1723
1724 #[serde(skip_serializing_if = "Option::is_none")]
1726 pub input_tokens_after: Option<u32>,
1727
1728 #[serde(skip_serializing_if = "Option::is_none")]
1730 pub duration_ms: Option<u64>,
1731}
1732
1733impl LlmCompactionInfo {
1734 pub fn new(
1736 input_tokens_before: Option<u32>,
1737 input_tokens_after: Option<u32>,
1738 duration_ms: Option<u64>,
1739 ) -> Self {
1740 Self {
1741 compacted: true,
1742 input_tokens_before,
1743 input_tokens_after,
1744 duration_ms,
1745 }
1746 }
1747}
1748
1749#[derive(Debug, Clone, Serialize, Deserialize)]
1754#[cfg_attr(feature = "openapi", derive(ToSchema))]
1755pub struct LlmGenerationData {
1756 pub messages: Vec<Message>,
1758
1759 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1761 pub tools: Vec<ToolDefinitionSummary>,
1762
1763 pub output: LlmGenerationOutput,
1765
1766 pub metadata: LlmGenerationMetadata,
1768}
1769
1770impl LlmGenerationData {
1771 #[allow(clippy::too_many_arguments)]
1773 pub fn success(
1774 messages: Vec<Message>,
1775 tools: Vec<ToolDefinitionSummary>,
1776 text: Option<String>,
1777 tool_calls: Vec<ToolCall>,
1778 model: String,
1779 provider: Option<String>,
1780 usage: Option<TokenUsage>,
1781 duration_ms: Option<u64>,
1782 time_to_first_token_ms: Option<u64>,
1783 ) -> Self {
1784 let finish_reasons = if !tool_calls.is_empty() {
1786 Some(vec!["tool_calls".to_string()])
1787 } else {
1788 Some(vec!["stop".to_string()])
1789 };
1790
1791 Self {
1792 messages,
1793 tools,
1794 output: LlmGenerationOutput { text, tool_calls },
1795 metadata: LlmGenerationMetadata {
1796 model,
1797 provider,
1798 usage,
1799 duration_ms,
1800 time_to_first_token_ms,
1801 success: true,
1802 error: None,
1803 finish_reasons,
1804 response_id: None,
1805 retry: None,
1806 compaction: None,
1807 request_options: None,
1808 },
1809 }
1810 }
1811
1812 #[allow(clippy::too_many_arguments)]
1814 pub fn success_with_metadata(
1815 messages: Vec<Message>,
1816 tools: Vec<ToolDefinitionSummary>,
1817 text: Option<String>,
1818 tool_calls: Vec<ToolCall>,
1819 model: String,
1820 provider: Option<String>,
1821 usage: Option<TokenUsage>,
1822 duration_ms: Option<u64>,
1823 time_to_first_token_ms: Option<u64>,
1824 finish_reasons: Option<Vec<String>>,
1825 response_id: Option<String>,
1826 ) -> Self {
1827 Self {
1828 messages,
1829 tools,
1830 output: LlmGenerationOutput { text, tool_calls },
1831 metadata: LlmGenerationMetadata {
1832 model,
1833 provider,
1834 usage,
1835 duration_ms,
1836 time_to_first_token_ms,
1837 success: true,
1838 error: None,
1839 finish_reasons,
1840 response_id,
1841 retry: None,
1842 compaction: None,
1843 request_options: None,
1844 },
1845 }
1846 }
1847
1848 #[allow(clippy::too_many_arguments)]
1850 pub fn success_with_retry(
1851 messages: Vec<Message>,
1852 tools: Vec<ToolDefinitionSummary>,
1853 text: Option<String>,
1854 tool_calls: Vec<ToolCall>,
1855 model: String,
1856 provider: Option<String>,
1857 usage: Option<TokenUsage>,
1858 duration_ms: Option<u64>,
1859 time_to_first_token_ms: Option<u64>,
1860 finish_reasons: Option<Vec<String>>,
1861 response_id: Option<String>,
1862 retry: Option<LlmRetryInfo>,
1863 ) -> Self {
1864 Self {
1865 messages,
1866 tools,
1867 output: LlmGenerationOutput { text, tool_calls },
1868 metadata: LlmGenerationMetadata {
1869 model,
1870 provider,
1871 usage,
1872 duration_ms,
1873 time_to_first_token_ms,
1874 success: true,
1875 error: None,
1876 finish_reasons,
1877 response_id,
1878 retry,
1879 compaction: None,
1880 request_options: None,
1881 },
1882 }
1883 }
1884
1885 pub fn failure(
1887 messages: Vec<Message>,
1888 tools: Vec<ToolDefinitionSummary>,
1889 model: String,
1890 provider: Option<String>,
1891 error: String,
1892 duration_ms: Option<u64>,
1893 time_to_first_token_ms: Option<u64>,
1894 ) -> Self {
1895 Self {
1896 messages,
1897 tools,
1898 output: LlmGenerationOutput {
1899 text: None,
1900 tool_calls: vec![],
1901 },
1902 metadata: LlmGenerationMetadata {
1903 model,
1904 provider,
1905 usage: None,
1906 duration_ms,
1907 time_to_first_token_ms,
1908 success: false,
1909 error: Some(error),
1910 finish_reasons: Some(vec!["error".to_string()]),
1911 response_id: None,
1912 retry: None,
1913 compaction: None,
1914 request_options: None,
1915 },
1916 }
1917 }
1918
1919 pub fn with_compaction(mut self, compaction: LlmCompactionInfo) -> Self {
1923 self.metadata.compaction = Some(compaction);
1924 self
1925 }
1926
1927 pub fn with_retry(mut self, retry: LlmRetryInfo) -> Self {
1929 self.metadata.retry = Some(retry);
1930 self
1931 }
1932
1933 pub fn with_request_options(mut self, request_options: LlmRequestOptions) -> Self {
1935 if !request_options.is_empty() {
1936 self.metadata.request_options = Some(request_options);
1937 }
1938 self
1939 }
1940}
1941
1942#[derive(Debug, Clone, Serialize, Deserialize)]
1952#[cfg_attr(feature = "openapi", derive(ToSchema))]
1953pub struct ReasonThinkingStartedData {
1954 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1956 pub turn_id: TurnId,
1957
1958 #[serde(skip_serializing_if = "Option::is_none")]
1960 pub model: Option<String>,
1961}
1962
1963#[derive(Debug, Clone, Serialize, Deserialize)]
1969#[cfg_attr(feature = "openapi", derive(ToSchema))]
1970pub struct ReasonThinkingDeltaData {
1971 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1973 pub turn_id: TurnId,
1974
1975 pub delta: String,
1977
1978 pub accumulated: String,
1980}
1981
1982#[derive(Debug, Clone, Serialize, Deserialize)]
1987#[cfg_attr(feature = "openapi", derive(ToSchema))]
1988pub struct ReasonThinkingCompletedData {
1989 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1991 pub turn_id: TurnId,
1992
1993 pub thinking: String,
1995}
1996
1997#[derive(Debug, Clone, Serialize, Deserialize)]
2005#[cfg_attr(feature = "openapi", derive(ToSchema))]
2006pub struct ReasonItemData {
2007 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2009 pub turn_id: TurnId,
2010
2011 pub provider: String,
2013
2014 #[serde(skip_serializing_if = "Option::is_none")]
2016 pub model: Option<String>,
2017
2018 pub item_id: String,
2020
2021 #[serde(skip_serializing_if = "Option::is_none")]
2023 pub encrypted_content: Option<String>,
2024
2025 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2028 pub summary: Vec<String>,
2029
2030 #[serde(skip_serializing_if = "Option::is_none")]
2032 pub token_count: Option<u32>,
2033}
2034
2035#[derive(Debug, Clone, Serialize, Deserialize)]
2041#[cfg_attr(feature = "openapi", derive(ToSchema))]
2042pub struct TurnStartedData {
2043 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2045 pub turn_id: TurnId,
2046
2047 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
2049 pub input_message_id: MessageId,
2050
2051 #[serde(skip_serializing_if = "Option::is_none")]
2053 pub input_content: Option<String>,
2054}
2055
2056#[derive(Debug, Clone, Serialize, Deserialize)]
2058#[cfg_attr(feature = "openapi", derive(ToSchema))]
2059pub struct TurnCompletedData {
2060 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2062 pub turn_id: TurnId,
2063
2064 pub iterations: u32,
2066
2067 #[serde(skip_serializing_if = "Option::is_none")]
2069 pub duration_ms: Option<u64>,
2070
2071 #[serde(skip_serializing_if = "Option::is_none")]
2073 pub usage: Option<TokenUsage>,
2074
2075 #[serde(skip_serializing_if = "Option::is_none")]
2077 pub input_content: Option<String>,
2078
2079 #[serde(skip_serializing_if = "Option::is_none")]
2081 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "message_01933b5a00007000800000000000001"))]
2082 pub final_message_id: Option<MessageId>,
2083
2084 #[serde(skip_serializing_if = "Option::is_none")]
2086 pub final_answer_preview: Option<String>,
2087
2088 #[serde(skip_serializing_if = "Option::is_none")]
2090 pub time_to_first_token_ms: Option<u64>,
2091
2092 #[serde(skip_serializing_if = "Option::is_none")]
2094 pub tool_call_count: Option<u32>,
2095
2096 #[serde(skip_serializing_if = "Option::is_none")]
2098 pub llm_call_count: Option<u32>,
2099
2100 #[serde(skip_serializing_if = "Option::is_none")]
2102 pub status: Option<String>,
2103}
2104
2105#[derive(Debug, Clone, Serialize, Deserialize)]
2107#[cfg_attr(feature = "openapi", derive(ToSchema))]
2108pub struct TurnFailedData {
2109 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2111 pub turn_id: TurnId,
2112
2113 pub error: String,
2115
2116 #[serde(default, skip_serializing_if = "Option::is_none")]
2118 pub error_code: Option<String>,
2119
2120 #[serde(default, skip_serializing_if = "Option::is_none")]
2122 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
2123 pub error_fields: Option<UserFacingErrorFields>,
2124
2125 #[serde(default, skip_serializing_if = "Option::is_none")]
2129 pub error_disclosure: Option<String>,
2130}
2131
2132#[derive(Debug, Clone, Serialize, Deserialize)]
2139#[cfg_attr(feature = "openapi", derive(ToSchema))]
2140pub struct TurnSealedData {
2141 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2143 pub turn_id: TurnId,
2144
2145 pub reason: String,
2148
2149 #[serde(default, skip_serializing_if = "Option::is_none")]
2151 pub detail: Option<String>,
2152
2153 #[serde(default, skip_serializing_if = "Option::is_none")]
2155 pub iterations: Option<u32>,
2156
2157 #[serde(default, skip_serializing_if = "Option::is_none")]
2159 pub usage: Option<TokenUsage>,
2160}
2161
2162#[derive(Debug, Clone, Serialize, Deserialize)]
2164#[cfg_attr(feature = "openapi", derive(ToSchema))]
2165pub struct TurnCancelledData {
2166 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2168 pub turn_id: TurnId,
2169
2170 #[serde(skip_serializing_if = "Option::is_none")]
2172 pub reason: Option<String>,
2173
2174 #[serde(skip_serializing_if = "Option::is_none")]
2176 pub usage: Option<TokenUsage>,
2177}
2178
2179#[derive(Debug, Clone, Serialize, Deserialize)]
2185#[cfg_attr(feature = "openapi", derive(ToSchema))]
2186pub struct SessionStartedData {
2187 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "harness_01933b5a00007000800000000000001"))]
2189 pub harness_id: HarnessId,
2190
2191 #[serde(skip_serializing_if = "Option::is_none")]
2193 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agent_01933b5a00007000800000000000001"))]
2194 pub agent_id: Option<AgentId>,
2195
2196 #[serde(skip_serializing_if = "Option::is_none")]
2198 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
2199 pub model_id: Option<ModelId>,
2200}
2201
2202#[derive(Debug, Clone, Serialize, Deserialize)]
2204#[cfg_attr(feature = "openapi", derive(ToSchema))]
2205pub struct SessionActivatedData {
2206 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2208 pub turn_id: TurnId,
2209
2210 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
2212 pub input_message_id: MessageId,
2213}
2214
2215#[derive(Debug, Clone, Serialize, Deserialize)]
2217#[cfg_attr(feature = "openapi", derive(ToSchema))]
2218pub struct SessionIdledData {
2219 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2221 pub turn_id: TurnId,
2222
2223 #[serde(skip_serializing_if = "Option::is_none")]
2225 pub iterations: Option<u32>,
2226
2227 #[serde(skip_serializing_if = "Option::is_none")]
2229 pub usage: Option<TokenUsage>,
2230}
2231
2232#[derive(Debug, Clone, Serialize, Deserialize)]
2241#[cfg_attr(feature = "openapi", derive(ToSchema))]
2242pub struct SessionTaskEventData {
2243 pub task: crate::session_task::SessionTask,
2244}
2245
2246#[derive(Debug, Clone, Serialize, Deserialize)]
2248#[cfg_attr(feature = "openapi", derive(ToSchema))]
2249pub struct TaskMessageEventData {
2250 pub task_id: String,
2251 pub message: crate::session_task::TaskMessage,
2252}
2253
2254#[derive(Debug, Clone, Serialize, Deserialize)]
2260#[cfg_attr(feature = "openapi", derive(ToSchema))]
2261#[serde(rename_all = "snake_case")]
2262pub enum CompactionReason {
2263 ProactiveBudget,
2265 RequestTooLarge,
2267 Manual,
2269}
2270
2271impl std::fmt::Display for CompactionReason {
2272 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2273 match self {
2274 Self::ProactiveBudget => write!(f, "proactive_budget"),
2275 Self::RequestTooLarge => write!(f, "request_too_large"),
2276 Self::Manual => write!(f, "manual"),
2277 }
2278 }
2279}
2280
2281#[derive(Debug, Clone, Serialize, Deserialize)]
2283#[cfg_attr(feature = "openapi", derive(ToSchema))]
2284pub struct ContextCompactingData {
2285 pub reason: CompactionReason,
2287 pub strategy: String,
2289 pub messages_before: usize,
2291}
2292
2293#[derive(Debug, Clone, Serialize, Deserialize)]
2295#[cfg_attr(feature = "openapi", derive(ToSchema))]
2296pub struct CompactionStepData {
2297 pub strategy: String,
2299 pub messages_after: usize,
2301 pub duration_ms: u64,
2303}
2304
2305#[derive(Debug, Clone, Serialize, Deserialize)]
2307#[cfg_attr(feature = "openapi", derive(ToSchema))]
2308pub struct ContextCompactedData {
2309 pub strategy_used: String,
2311 pub messages_before: usize,
2313 pub messages_after: usize,
2315 pub duration_ms: u64,
2317 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2319 pub steps: Vec<CompactionStepData>,
2320}
2321
2322#[derive(Debug, Clone, Serialize, Deserialize)]
2328#[cfg_attr(feature = "openapi", derive(ToSchema))]
2329pub struct FileWrittenData {
2330 pub path: String,
2332 pub operation: String,
2334 pub size_bytes: i64,
2336 pub created: bool,
2338}
2339
2340pub const FILE_OP_CREATE: &str = "create";
2342pub const FILE_OP_UPDATE: &str = "update";
2343
2344#[derive(Debug, Clone, Serialize, Deserialize)]
2350#[cfg_attr(feature = "openapi", derive(ToSchema))]
2351pub struct BudgetEventData {
2352 pub budget_id: String,
2354 pub balance: f64,
2356 pub limit: f64,
2358 pub currency: String,
2360 #[serde(skip_serializing_if = "Option::is_none")]
2362 pub message: Option<String>,
2363 #[serde(skip_serializing_if = "Option::is_none")]
2365 pub soft_limit: Option<f64>,
2366}
2367
2368#[derive(Debug, Clone, Serialize, Deserialize)]
2374#[cfg_attr(feature = "openapi", derive(ToSchema))]
2375pub struct VoiceSessionStartedData {
2376 #[cfg_attr(
2378 feature = "openapi",
2379 schema(example = "voice_01933b5a00007000800000000000001")
2380 )]
2381 pub voice_connection_id: String,
2382 #[cfg_attr(feature = "openapi", schema(example = "gpt-realtime"))]
2384 pub model: String,
2385 #[cfg_attr(feature = "openapi", schema(example = "alloy"))]
2387 pub voice: String,
2388 #[cfg_attr(feature = "openapi", schema(example = "medium"))]
2390 pub reasoning_effort: String,
2391 #[cfg_attr(feature = "openapi", schema(example = "webrtc"))]
2393 pub transport: String,
2394}
2395
2396#[derive(Debug, Clone, Serialize, Deserialize)]
2398#[cfg_attr(feature = "openapi", derive(ToSchema))]
2399pub struct VoiceTranscriptData {
2400 pub voice_connection_id: String,
2402 #[serde(default, skip_serializing_if = "Option::is_none")]
2404 pub item_id: Option<String>,
2405 #[serde(default, skip_serializing_if = "Option::is_none")]
2407 pub response_id: Option<String>,
2408 #[serde(default, skip_serializing_if = "Option::is_none")]
2410 pub phase: Option<String>,
2411 #[serde(default, skip_serializing_if = "String::is_empty")]
2413 pub delta: String,
2414 pub accumulated: String,
2416}
2417
2418#[derive(Debug, Clone, Serialize, Deserialize)]
2420#[cfg_attr(feature = "openapi", derive(ToSchema))]
2421pub struct VoiceSessionEndedData {
2422 #[cfg_attr(
2424 feature = "openapi",
2425 schema(example = "voice_01933b5a00007000800000000000001")
2426 )]
2427 pub voice_connection_id: String,
2428 #[serde(default, skip_serializing_if = "Option::is_none")]
2430 #[cfg_attr(
2431 feature = "openapi",
2432 schema(example = "User hung up after refund confirmed.")
2433 )]
2434 pub reason: Option<String>,
2435 #[serde(default, skip_serializing_if = "Option::is_none")]
2438 #[cfg_attr(feature = "openapi", schema(example = 184_500_u64))]
2439 pub duration_ms: Option<u64>,
2440}
2441
2442#[derive(Debug, Clone, Serialize, Deserialize)]
2444#[cfg_attr(feature = "openapi", derive(ToSchema))]
2445pub struct VoiceSessionFailedData {
2446 #[cfg_attr(
2448 feature = "openapi",
2449 schema(example = "voice_01933b5a00007000800000000000001")
2450 )]
2451 pub voice_connection_id: String,
2452 #[cfg_attr(
2454 feature = "openapi",
2455 schema(example = "realtime provider closed stream: 1011 internal_error")
2456 )]
2457 pub error: String,
2458}
2459
2460#[derive(Debug, Clone, Serialize)]
2507#[serde(untagged)]
2508#[cfg_attr(feature = "openapi", derive(ToSchema))]
2509#[cfg_attr(feature = "openapi", schema(
2510 title = "EventData",
2511 description = "Event-specific payload. The schema depends on the event type field.",
2512 example = json!({"message": {"id": "...", "role": "user", "content": []}})
2513))]
2514pub enum EventData {
2515 InputMessage(InputMessageData),
2517
2518 OutputMessageDelta(OutputMessageDeltaData),
2520 OutputMessageStarted(OutputMessageStartedData),
2521 OutputMessageReplaced(OutputMessageReplacedData),
2522 OutputMessageCompleted(OutputMessageCompletedData),
2523
2524 TurnStarted(TurnStartedData),
2526 TurnCompleted(TurnCompletedData),
2527 TurnFailed(TurnFailedData),
2528
2529 ReasonStarted(ReasonStartedData),
2531 ReasonCompleted(ReasonCompletedData),
2532 ReasonRecovered(ReasonRecoveredData),
2533 CapabilityUsage(CapabilityUsageData),
2534 ActStarted(ActStartedData),
2535 ActCompleted(ActCompletedData),
2536 ToolStarted(ToolStartedData),
2537 ToolCompleted(ToolCompletedData),
2538 ToolProgress(ToolProgressData),
2539 ToolOutputDelta(ToolOutputDeltaData),
2540 ToolCallRequested(ToolCallRequestedData),
2541
2542 TranscriptRepaired(TranscriptRepairedData),
2544 ToolCallRepaired(ToolCallRepairedData),
2545
2546 LlmGeneration(LlmGenerationData),
2548
2549 ReasonThinkingDelta(ReasonThinkingDeltaData),
2551 ReasonItem(ReasonItemData),
2552 ReasonThinkingStarted(ReasonThinkingStartedData),
2553 ReasonThinkingCompleted(ReasonThinkingCompletedData),
2554
2555 TurnSealed(TurnSealedData),
2556 TurnCancelled(TurnCancelledData),
2557
2558 SessionStarted(SessionStartedData),
2560 SessionActivated(SessionActivatedData),
2561 SessionIdled(SessionIdledData),
2562
2563 TaskCreated(SessionTaskEventData),
2565 TaskUpdated(SessionTaskEventData),
2566 TaskMessageSent(TaskMessageEventData),
2567 TaskMessageReceived(TaskMessageEventData),
2568
2569 ContextCompacting(ContextCompactingData),
2571 ContextCompacted(ContextCompactedData),
2572
2573 FileWritten(FileWrittenData),
2575
2576 BudgetWarning(BudgetEventData),
2578 BudgetPaused(BudgetEventData),
2579 BudgetExhausted(BudgetEventData),
2580 BudgetResumed(BudgetEventData),
2581
2582 VoiceSessionStarted(VoiceSessionStartedData),
2584 VoiceInputTranscriptDelta(VoiceTranscriptData),
2585 VoiceInputTranscriptCompleted(VoiceTranscriptData),
2586 VoiceOutputTranscriptDelta(VoiceTranscriptData),
2587 VoiceOutputTranscriptCompleted(VoiceTranscriptData),
2588 VoiceSessionEnded(VoiceSessionEndedData),
2589 VoiceSessionFailed(VoiceSessionFailedData),
2590
2591 #[serde(skip)]
2595 Unsupported {
2596 event_type: String,
2598 data: serde_json::Value,
2600 },
2601}
2602
2603impl EventData {
2604 pub fn is_unsupported(&self) -> bool {
2607 matches!(self, EventData::Unsupported { .. })
2608 }
2609
2610 pub fn unsupported(event_type: String, data: serde_json::Value) -> Self {
2613 tracing::warn!(
2614 event_type = %event_type,
2615 "Encountered unsupported event type - will be filtered from API responses"
2616 );
2617 EventData::Unsupported { event_type, data }
2618 }
2619}
2620
2621macro_rules! event_data_kinds {
2635 ($( $variant:ident($data:ty) = $type_const:path ),+ $(,)?) => {
2636 impl EventData {
2637 pub fn event_type(&self) -> &'static str {
2640 match self {
2641 $( EventData::$variant(_) => $type_const, )+
2642 EventData::Unsupported { .. } => "unsupported",
2643 }
2644 }
2645 }
2646
2647 pub fn deserialize_event_data(event_type: &str, data: serde_json::Value) -> EventData {
2659 let result = match event_type {
2660 $(
2661 $type_const => serde_json::from_value::<$data>(data.clone())
2662 .map(EventData::$variant),
2663 )+
2664 _ => return EventData::unsupported(event_type.to_string(), data),
2665 };
2666
2667 result.unwrap_or_else(|e| {
2668 tracing::warn!(
2669 event_type = %event_type,
2670 error = %e,
2671 "Failed to deserialize known event type - treating as unsupported"
2672 );
2673 EventData::Unsupported {
2674 event_type: event_type.to_string(),
2675 data,
2676 }
2677 })
2678 }
2679 };
2680}
2681
2682event_data_kinds! {
2683 InputMessage(InputMessageData) = INPUT_MESSAGE,
2685
2686 OutputMessageStarted(OutputMessageStartedData) = OUTPUT_MESSAGE_STARTED,
2688 OutputMessageDelta(OutputMessageDeltaData) = OUTPUT_MESSAGE_DELTA,
2689 OutputMessageReplaced(OutputMessageReplacedData) = OUTPUT_MESSAGE_REPLACED,
2690 OutputMessageCompleted(OutputMessageCompletedData) = OUTPUT_MESSAGE_COMPLETED,
2691
2692 TurnStarted(TurnStartedData) = TURN_STARTED,
2694 TurnCompleted(TurnCompletedData) = TURN_COMPLETED,
2695 TurnFailed(TurnFailedData) = TURN_FAILED,
2696 TurnSealed(TurnSealedData) = TURN_SEALED,
2697 TurnCancelled(TurnCancelledData) = TURN_CANCELLED,
2698
2699 ReasonStarted(ReasonStartedData) = REASON_STARTED,
2701 ReasonCompleted(ReasonCompletedData) = REASON_COMPLETED,
2702 ReasonRecovered(ReasonRecoveredData) = REASON_RECOVERED,
2703 CapabilityUsage(CapabilityUsageData) = CAPABILITY_USAGE,
2704 ActStarted(ActStartedData) = ACT_STARTED,
2705 ActCompleted(ActCompletedData) = ACT_COMPLETED,
2706 ToolStarted(ToolStartedData) = TOOL_STARTED,
2707 ToolCompleted(ToolCompletedData) = TOOL_COMPLETED,
2708 ToolProgress(ToolProgressData) = TOOL_PROGRESS,
2709 ToolOutputDelta(ToolOutputDeltaData) = TOOL_OUTPUT_DELTA,
2710 ToolCallRequested(ToolCallRequestedData) = TOOL_CALL_REQUESTED,
2711
2712 TranscriptRepaired(TranscriptRepairedData) = TRANSCRIPT_REPAIRED,
2714 ToolCallRepaired(ToolCallRepairedData) = TOOL_CALL_REPAIRED,
2715
2716 LlmGeneration(LlmGenerationData) = LLM_GENERATION,
2718
2719 ReasonThinkingStarted(ReasonThinkingStartedData) = REASON_THINKING_STARTED,
2721 ReasonThinkingDelta(ReasonThinkingDeltaData) = REASON_THINKING_DELTA,
2722 ReasonThinkingCompleted(ReasonThinkingCompletedData) = REASON_THINKING_COMPLETED,
2723 ReasonItem(ReasonItemData) = REASON_ITEM,
2724
2725 SessionStarted(SessionStartedData) = SESSION_STARTED,
2727 SessionActivated(SessionActivatedData) = SESSION_ACTIVATED,
2728 SessionIdled(SessionIdledData) = SESSION_IDLED,
2729
2730 ContextCompacting(ContextCompactingData) = CONTEXT_COMPACTING,
2732 ContextCompacted(ContextCompactedData) = CONTEXT_COMPACTED,
2733
2734 FileWritten(FileWrittenData) = FILE_WRITTEN,
2736
2737 BudgetWarning(BudgetEventData) = BUDGET_WARNING,
2739 BudgetPaused(BudgetEventData) = BUDGET_PAUSED,
2740 BudgetExhausted(BudgetEventData) = BUDGET_EXHAUSTED,
2741 BudgetResumed(BudgetEventData) = BUDGET_RESUMED,
2742
2743 VoiceSessionStarted(VoiceSessionStartedData) = VOICE_SESSION_STARTED,
2745 VoiceInputTranscriptDelta(VoiceTranscriptData) = VOICE_INPUT_TRANSCRIPT_DELTA,
2746 VoiceInputTranscriptCompleted(VoiceTranscriptData) = VOICE_INPUT_TRANSCRIPT_COMPLETED,
2747 VoiceOutputTranscriptDelta(VoiceTranscriptData) = VOICE_OUTPUT_TRANSCRIPT_DELTA,
2748 VoiceOutputTranscriptCompleted(VoiceTranscriptData) = VOICE_OUTPUT_TRANSCRIPT_COMPLETED,
2749 VoiceSessionEnded(VoiceSessionEndedData) = VOICE_SESSION_ENDED,
2750 VoiceSessionFailed(VoiceSessionFailedData) = VOICE_SESSION_FAILED,
2751
2752 TaskCreated(SessionTaskEventData) = TASK_CREATED,
2754 TaskUpdated(SessionTaskEventData) = TASK_UPDATED,
2755 TaskMessageSent(TaskMessageEventData) = TASK_MESSAGE_SENT,
2756 TaskMessageReceived(TaskMessageEventData) = TASK_MESSAGE_RECEIVED,
2757}
2758
2759macro_rules! impl_from_event_data {
2763 ($($data_type:ty => $variant:ident),* $(,)?) => {
2764 $(
2765 impl From<$data_type> for EventData {
2766 fn from(data: $data_type) -> Self {
2767 EventData::$variant(data)
2768 }
2769 }
2770 )*
2771 };
2772}
2773
2774impl_from_event_data! {
2776 InputMessageData => InputMessage,
2777 OutputMessageStartedData => OutputMessageStarted,
2778 OutputMessageDeltaData => OutputMessageDelta,
2779 OutputMessageReplacedData => OutputMessageReplaced,
2780 OutputMessageCompletedData => OutputMessageCompleted,
2781 TurnStartedData => TurnStarted,
2782 TurnCompletedData => TurnCompleted,
2783 TurnFailedData => TurnFailed,
2784 TurnSealedData => TurnSealed,
2785 TurnCancelledData => TurnCancelled,
2786 ReasonStartedData => ReasonStarted,
2787 ReasonCompletedData => ReasonCompleted,
2788 ReasonRecoveredData => ReasonRecovered,
2789 CapabilityUsageData => CapabilityUsage,
2790 ActStartedData => ActStarted,
2791 ActCompletedData => ActCompleted,
2792 ToolStartedData => ToolStarted,
2793 ToolCompletedData => ToolCompleted,
2794 ToolProgressData => ToolProgress,
2795 ToolOutputDeltaData => ToolOutputDelta,
2796 ToolCallRequestedData => ToolCallRequested,
2797 TranscriptRepairedData => TranscriptRepaired,
2798 ToolCallRepairedData => ToolCallRepaired,
2799 LlmGenerationData => LlmGeneration,
2800 ReasonThinkingStartedData => ReasonThinkingStarted,
2801 ReasonThinkingDeltaData => ReasonThinkingDelta,
2802 ReasonThinkingCompletedData => ReasonThinkingCompleted,
2803 ReasonItemData => ReasonItem,
2804 SessionStartedData => SessionStarted,
2805 SessionActivatedData => SessionActivated,
2806 SessionIdledData => SessionIdled,
2807 ContextCompactingData => ContextCompacting,
2808 ContextCompactedData => ContextCompacted,
2809 FileWrittenData => FileWritten,
2810 VoiceSessionStartedData => VoiceSessionStarted,
2811 VoiceSessionEndedData => VoiceSessionEnded,
2812 VoiceSessionFailedData => VoiceSessionFailed,
2813}
2814
2815impl EventData {
2816 pub fn voice_transcript_event(data: VoiceTranscriptData, event_type: &str) -> Self {
2817 match event_type {
2818 VOICE_INPUT_TRANSCRIPT_DELTA => EventData::VoiceInputTranscriptDelta(data),
2819 VOICE_INPUT_TRANSCRIPT_COMPLETED => EventData::VoiceInputTranscriptCompleted(data),
2820 VOICE_OUTPUT_TRANSCRIPT_DELTA => EventData::VoiceOutputTranscriptDelta(data),
2821 VOICE_OUTPUT_TRANSCRIPT_COMPLETED => EventData::VoiceOutputTranscriptCompleted(data),
2822 _ => EventData::unsupported(
2823 event_type.to_string(),
2824 serde_json::to_value(&data).unwrap_or(serde_json::Value::Null),
2825 ),
2826 }
2827 }
2828}
2829
2830impl EventData {
2833 pub fn budget_event(data: BudgetEventData, event_type: &str) -> Self {
2834 match event_type {
2835 BUDGET_WARNING => EventData::BudgetWarning(data),
2836 BUDGET_PAUSED => EventData::BudgetPaused(data),
2837 BUDGET_EXHAUSTED => EventData::BudgetExhausted(data),
2838 BUDGET_RESUMED => EventData::BudgetResumed(data),
2839 _ => EventData::unsupported(
2840 event_type.to_string(),
2841 serde_json::to_value(&data).unwrap_or(serde_json::Value::Null),
2842 ),
2843 }
2844 }
2845}
2846
2847#[derive(Debug, Clone, Serialize)]
2857#[cfg_attr(feature = "openapi", derive(ToSchema))]
2858pub struct EventRequest {
2859 #[serde(rename = "type")]
2861 pub event_type: String,
2862
2863 pub ts: DateTime<Utc>,
2865
2866 pub session_id: SessionId,
2868
2869 pub context: EventContext,
2871
2872 pub data: EventData,
2874
2875 #[serde(skip_serializing_if = "Option::is_none")]
2877 pub metadata: Option<serde_json::Value>,
2878
2879 #[serde(skip_serializing_if = "Option::is_none")]
2881 pub tags: Option<Vec<String>>,
2882}
2883
2884#[derive(Debug, Deserialize)]
2885struct RawEventRequest {
2886 #[serde(rename = "type")]
2887 event_type: String,
2888 ts: DateTime<Utc>,
2889 session_id: SessionId,
2890 context: EventContext,
2891 data: serde_json::Value,
2892 metadata: Option<serde_json::Value>,
2893 tags: Option<Vec<String>>,
2894}
2895
2896impl<'de> Deserialize<'de> for EventRequest {
2897 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2898 where
2899 D: Deserializer<'de>,
2900 {
2901 let raw = RawEventRequest::deserialize(deserializer)?;
2902 let data = deserialize_event_data(&raw.event_type, raw.data);
2903 Ok(Self {
2904 event_type: raw.event_type,
2905 ts: raw.ts,
2906 session_id: raw.session_id,
2907 context: raw.context,
2908 data,
2909 metadata: raw.metadata,
2910 tags: raw.tags,
2911 })
2912 }
2913}
2914
2915impl EventRequest {
2916 pub fn new(session_id: SessionId, context: EventContext, data: impl Into<EventData>) -> Self {
2920 let data = data.into();
2921 let event_type = data.event_type().to_string();
2922 Self {
2923 event_type,
2924 ts: Utc::now(),
2925 session_id,
2926 context,
2927 data,
2928 metadata: None,
2929 tags: None,
2930 }
2931 }
2932
2933 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
2935 self.metadata = Some(metadata);
2936 self
2937 }
2938
2939 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
2941 self.tags = Some(tags);
2942 self
2943 }
2944
2945 pub fn is_ephemeral(&self) -> bool {
2953 is_ephemeral_event_type(&self.event_type)
2954 }
2955
2956 pub fn into_event(self, id: EventId, sequence: i32) -> Event {
2958 Event {
2959 id,
2960 event_type: self.event_type,
2961 ts: self.ts,
2962 session_id: self.session_id,
2963 context: self.context,
2964 data: self.data,
2965 metadata: self.metadata,
2966 tags: self.tags,
2967 sequence: Some(sequence),
2968 }
2969 }
2970}
2971
2972pub struct EventBuilder {
2978 session_id: SessionId,
2979 context: EventContext,
2980}
2981
2982impl EventBuilder {
2983 pub fn new(session_id: SessionId) -> Self {
2984 Self {
2985 session_id,
2986 context: EventContext::empty(),
2987 }
2988 }
2989
2990 pub fn with_turn(mut self, turn_id: TurnId, input_message_id: MessageId) -> Self {
2991 self.context.turn_id = Some(turn_id);
2992 self.context.input_message_id = Some(input_message_id);
2993 self
2994 }
2995
2996 pub fn with_exec(mut self, exec_id: ExecId) -> Self {
2997 self.context.exec_id = Some(exec_id);
2998 self
2999 }
3000
3001 pub fn build(self, data: impl Into<EventData>) -> Event {
3002 Event::new(self.session_id, self.context, data)
3003 }
3004}
3005
3006#[cfg(test)]
3011mod tests {
3012 use super::*;
3013 use crate::driver_registry::PromptCacheStrategy;
3014 use serde_json::json;
3015 use std::collections::HashMap;
3016
3017 #[test]
3018 fn test_event_creation() {
3019 let session_id = SessionId::new();
3020 let context = EventContext::empty();
3021 let data = InputMessageData::new(Message::user("test"));
3022
3023 let event = Event::new(session_id, context, data);
3024
3025 assert_eq!(event.event_type, "input.message");
3026 assert_eq!(event.session_uuid(), session_id.uuid());
3027 assert!(event.is_input_event());
3028 assert!(event.is_message_event());
3029 }
3030
3031 #[test]
3032 fn test_event_context_from_atom_context() {
3033 let session_id = SessionId::new();
3034 let turn_id = TurnId::new();
3035 let input_message_id = MessageId::new();
3036
3037 let atom_ctx = AtomContext::new(session_id, turn_id, input_message_id);
3038 let context = EventContext::from_atom_context(&atom_ctx);
3039
3040 assert_eq!(context.turn_id, Some(turn_id));
3041 assert_eq!(context.input_message_id, Some(input_message_id));
3042 assert_eq!(context.exec_id, Some(atom_ctx.exec_id));
3043 }
3044
3045 #[test]
3046 fn test_event_serialization() {
3047 let session_id = SessionId::new();
3048 let context = EventContext::empty();
3049 let event = Event::new(
3050 session_id,
3051 context,
3052 InputMessageData::new(Message::user("test")),
3053 );
3054
3055 let json = serde_json::to_string(&event).unwrap();
3056
3057 assert!(json.contains("\"type\":\"input.message\""));
3058 assert!(json.contains("\"session_id\""));
3059 assert!(json.contains("\"context\""));
3060 assert!(json.contains("\"data\""));
3061 }
3062
3063 #[test]
3064 fn transcript_repaired_is_valid_filter_event_type() {
3065 assert!(VALID_EVENT_TYPES.contains(&TRANSCRIPT_REPAIRED));
3066 }
3067
3068 #[test]
3072 fn capability_usage_is_valid_filter_event_type() {
3073 assert!(VALID_EVENT_TYPES.contains(&CAPABILITY_USAGE));
3074 }
3075
3076 #[test]
3077 fn test_event_builder() {
3078 let session_id = SessionId::new();
3079 let turn_id = TurnId::new();
3080 let input_message_id = MessageId::new();
3081 let exec_id = ExecId::new();
3082
3083 let event = EventBuilder::new(session_id)
3084 .with_turn(turn_id, input_message_id)
3085 .with_exec(exec_id)
3086 .build(ReasonStartedData {
3087 harness_id: HarnessId::from_seed(1),
3088 agent_id: Some(AgentId::new()),
3089 metadata: Some(ModelMetadata {
3090 model: "gpt-4o".to_string(),
3091 model_id: None,
3092 provider_id: None,
3093 }),
3094 });
3095
3096 assert_eq!(event.event_type, "reason.started");
3097 assert_eq!(event.session_id, session_id);
3098 assert_eq!(event.context.turn_id, Some(turn_id));
3099 assert_eq!(event.context.exec_id, Some(exec_id));
3100 }
3101
3102 #[test]
3103 fn test_reason_completed_data() {
3104 let data = ReasonCompletedData::success("Hello world", true, 2, Some(1000), None);
3105 assert!(data.success);
3106 assert_eq!(data.text_preview, Some("Hello world".to_string()));
3107 assert!(data.has_tool_calls);
3108 assert_eq!(data.tool_call_count, 2);
3109 assert_eq!(data.duration_ms, Some(1000));
3110 assert!(data.usage.is_none());
3111
3112 let data = ReasonCompletedData::failure("Network error".to_string(), Some(500));
3113 assert!(!data.success);
3114 assert_eq!(data.error, Some("Network error".to_string()));
3115 assert_eq!(data.duration_ms, Some(500));
3116 }
3117
3118 #[test]
3119 fn test_input_output_event_types() {
3120 assert_eq!(INPUT_MESSAGE, "input.message");
3121 assert_eq!(OUTPUT_MESSAGE_STARTED, "output.message.started");
3122 assert_eq!(OUTPUT_MESSAGE_DELTA, "output.message.delta");
3123 assert_eq!(OUTPUT_MESSAGE_COMPLETED, "output.message.completed");
3124 }
3125
3126 #[test]
3127 fn test_turn_event_types() {
3128 assert_eq!(TURN_STARTED, "turn.started");
3129 assert_eq!(TURN_COMPLETED, "turn.completed");
3130 assert_eq!(TURN_FAILED, "turn.failed");
3131 assert_eq!(TURN_CANCELLED, "turn.cancelled");
3132 }
3133
3134 #[test]
3135 fn test_turn_cancelled_data() {
3136 let data = TurnCancelledData {
3137 turn_id: TurnId::from_uuid(Uuid::now_v7()),
3138 reason: Some("User requested cancellation".to_string()),
3139 usage: Some(TokenUsage::new(100, 50)),
3140 };
3141
3142 let event_data: EventData = data.into();
3143 assert_eq!(event_data.event_type(), TURN_CANCELLED);
3144 }
3145
3146 #[test]
3147 fn test_tool_event_types() {
3148 assert_eq!(TOOL_STARTED, "tool.started");
3149 assert_eq!(TOOL_COMPLETED, "tool.completed");
3150 }
3151
3152 #[test]
3153 fn test_llm_generation_event_type() {
3154 assert_eq!(LLM_GENERATION, "llm.generation");
3155 }
3156
3157 #[test]
3158 fn test_llm_generation_data_success() {
3159 let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")];
3160 let tools = vec![ToolDefinitionSummary {
3161 name: "get_weather".to_string(),
3162 display_name: None,
3163 category: None,
3164 capability_id: None,
3165 capability_name: None,
3166 description: "Get weather for a city".to_string(),
3167 }];
3168 let tool_calls = vec![];
3169 let data = LlmGenerationData::success(
3170 messages.clone(),
3171 tools,
3172 Some("Hi there!".to_string()),
3173 tool_calls,
3174 "gpt-4o".to_string(),
3175 Some("openai".to_string()),
3176 Some(TokenUsage {
3177 input_tokens: 10,
3178 output_tokens: 5,
3179 cache_read_tokens: None,
3180 cache_creation_tokens: None,
3181 actual_cost_usd: None,
3182 estimated_cost_usd: None,
3183 effective_cost_usd: None,
3184 }),
3185 Some(100),
3186 Some(25), );
3188
3189 assert_eq!(data.messages.len(), 2);
3190 assert_eq!(data.tools.len(), 1);
3191 assert_eq!(data.tools[0].name, "get_weather");
3192 assert_eq!(data.output.text, Some("Hi there!".to_string()));
3193 assert!(data.output.tool_calls.is_empty());
3194 assert!(data.metadata.success);
3195 assert_eq!(data.metadata.model, "gpt-4o");
3196 assert_eq!(data.metadata.provider, Some("openai".to_string()));
3197 assert!(data.metadata.error.is_none());
3198 assert_eq!(data.metadata.finish_reasons, Some(vec!["stop".to_string()]));
3200 assert!(data.metadata.response_id.is_none());
3201 }
3202
3203 #[test]
3204 fn test_llm_generation_data_with_full_metadata() {
3205 let messages = vec![Message::user("Hello")];
3206 let data = LlmGenerationData::success_with_metadata(
3207 messages,
3208 vec![],
3209 Some("Hi!".to_string()),
3210 vec![],
3211 "claude-3-opus".to_string(),
3212 Some("anthropic".to_string()),
3213 Some(TokenUsage {
3214 input_tokens: 5,
3215 output_tokens: 3,
3216 cache_read_tokens: None,
3217 cache_creation_tokens: None,
3218 actual_cost_usd: None,
3219 estimated_cost_usd: None,
3220 effective_cost_usd: None,
3221 }),
3222 Some(50),
3223 Some(25), Some(vec!["end_turn".to_string()]),
3225 Some("msg_12345".to_string()),
3226 );
3227
3228 assert!(data.metadata.success);
3229 assert_eq!(data.metadata.model, "claude-3-opus");
3230 assert_eq!(data.metadata.provider, Some("anthropic".to_string()));
3231 assert_eq!(data.metadata.time_to_first_token_ms, Some(25));
3232 assert_eq!(
3233 data.metadata.finish_reasons,
3234 Some(vec!["end_turn".to_string()])
3235 );
3236 assert_eq!(data.metadata.response_id, Some("msg_12345".to_string()));
3237 }
3238
3239 #[test]
3240 fn test_llm_generation_data_failure() {
3241 let messages = vec![Message::user("Hello")];
3242 let data = LlmGenerationData::failure(
3243 messages,
3244 vec![],
3245 "gpt-4o".to_string(),
3246 Some("openai".to_string()),
3247 "Rate limit exceeded".to_string(),
3248 Some(50),
3249 None, );
3251
3252 assert!(!data.metadata.success);
3253 assert_eq!(data.metadata.error, Some("Rate limit exceeded".to_string()));
3254 assert!(data.output.text.is_none());
3255 assert!(data.output.tool_calls.is_empty());
3256 }
3257
3258 #[test]
3259 fn test_llm_generation_event_data() {
3260 let data = LlmGenerationData::success(
3261 vec![Message::user("test")],
3262 vec![],
3263 Some("response".to_string()),
3264 vec![],
3265 "model".to_string(),
3266 None,
3267 None,
3268 None,
3269 None, );
3271
3272 let event_data: EventData = data.into();
3273 assert_eq!(event_data.event_type(), LLM_GENERATION);
3274 }
3275
3276 #[test]
3277 fn test_llm_generation_is_durable_not_ephemeral() {
3278 let session_id = SessionId::new();
3279 let data = LlmGenerationData::success(
3280 vec![Message::user("test")],
3281 vec![],
3282 Some("response".to_string()),
3283 vec![],
3284 "model".to_string(),
3285 None,
3286 None,
3287 None,
3288 None,
3289 );
3290
3291 let request = EventRequest::new(session_id, EventContext::empty(), data);
3292 assert!(!request.is_ephemeral());
3293 }
3294
3295 #[test]
3296 fn test_delta_events_are_ephemeral() {
3297 let session_id = SessionId::new();
3298 let turn_id = TurnId::new();
3299
3300 let output_delta = EventRequest::new(
3301 session_id,
3302 EventContext::empty(),
3303 OutputMessageDeltaData {
3304 turn_id,
3305 message_id: MessageId::new(),
3306 delta: "hel".to_string(),
3307 accumulated: "hel".to_string(),
3308 phase: None,
3309 },
3310 );
3311 assert!(output_delta.is_ephemeral());
3312
3313 let thinking_delta = EventRequest::new(
3314 session_id,
3315 EventContext::empty(),
3316 ReasonThinkingDeltaData {
3317 turn_id,
3318 delta: "step".to_string(),
3319 accumulated: "step".to_string(),
3320 },
3321 );
3322 assert!(thinking_delta.is_ephemeral());
3323
3324 let tool_delta = EventRequest::new(
3325 session_id,
3326 EventContext::empty(),
3327 ToolOutputDeltaData {
3328 tool_call_id: "call_123".to_string(),
3329 tool_name: "bash".to_string(),
3330 delta: "line".to_string(),
3331 stream: "stdout".to_string(),
3332 },
3333 );
3334 assert!(tool_delta.is_ephemeral());
3335 }
3336
3337 #[test]
3338 fn test_llm_generation_data_with_request_options() {
3339 let mut provider_options = HashMap::new();
3340 provider_options.insert(
3341 "openai".to_string(),
3342 json!({ "previous_response_id": true }),
3343 );
3344
3345 let data = LlmGenerationData::success(
3346 vec![Message::user("Hello")],
3347 vec![],
3348 Some("Hi".to_string()),
3349 vec![],
3350 "gpt-5.4".to_string(),
3351 Some("openai".to_string()),
3352 None,
3353 Some(42),
3354 Some(12),
3355 )
3356 .with_request_options(LlmRequestOptions {
3357 prompt_cache: Some(LlmPromptCacheInfo {
3358 enabled: true,
3359 strategy: PromptCacheStrategy::Auto,
3360 provider_mode: Some("prompt_cache_key".to_string()),
3361 }),
3362 tool_search: Some(LlmToolSearchInfo {
3363 enabled: true,
3364 threshold: 8,
3365 }),
3366 provider_options,
3367 metadata: Default::default(),
3368 });
3369
3370 let json = serde_json::to_value(&data).unwrap();
3371 assert_eq!(
3372 json["metadata"]["request_options"]["prompt_cache"]["provider_mode"],
3373 "prompt_cache_key"
3374 );
3375 assert_eq!(
3376 json["metadata"]["request_options"]["tool_search"]["threshold"],
3377 8
3378 );
3379 assert_eq!(
3380 json["metadata"]["request_options"]["provider_options"]["openai"]["previous_response_id"],
3381 true
3382 );
3383 }
3384
3385 #[test]
3386 fn test_extended_thinking_event_types() {
3387 assert_eq!(REASON_THINKING_STARTED, "reason.thinking.started");
3388 assert_eq!(REASON_THINKING_DELTA, "reason.thinking.delta");
3389 assert_eq!(REASON_THINKING_COMPLETED, "reason.thinking.completed");
3390 }
3391
3392 #[test]
3393 fn test_output_message_started_data() {
3394 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3395 let data = OutputMessageStartedData {
3396 turn_id,
3397 message_id: MessageId::new(),
3398 model: Some("claude-4-opus".to_string()),
3399 iteration: None,
3400 phase: None,
3401 };
3402
3403 let event_data: EventData = data.into();
3404 assert_eq!(event_data.event_type(), OUTPUT_MESSAGE_STARTED);
3405
3406 let json = serde_json::to_string(&event_data).unwrap();
3408 assert!(json.contains("turn_id"));
3409 assert!(json.contains("claude-4-opus"));
3410 }
3411
3412 #[test]
3413 fn test_output_message_started_data_without_model() {
3414 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3415 let data = OutputMessageStartedData {
3416 turn_id,
3417 message_id: MessageId::new(),
3418 model: None,
3419 iteration: None,
3420 phase: None,
3421 };
3422
3423 let json = serde_json::to_string(&data).unwrap();
3425 assert!(!json.contains("model"));
3426 }
3427
3428 #[test]
3429 fn test_reason_thinking_started_data() {
3430 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3431 let data = ReasonThinkingStartedData {
3432 turn_id,
3433 model: Some("claude-4-opus".to_string()),
3434 };
3435
3436 let event_data: EventData = data.into();
3437 assert_eq!(event_data.event_type(), REASON_THINKING_STARTED);
3438
3439 let json = serde_json::to_string(&event_data).unwrap();
3441 assert!(json.contains("turn_id"));
3442 assert!(json.contains("claude-4-opus"));
3443 }
3444
3445 #[test]
3446 fn test_reason_thinking_delta_data() {
3447 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3448 let data = ReasonThinkingDeltaData {
3449 turn_id,
3450 delta: "thinking step 1".to_string(),
3451 accumulated: "thinking step 1".to_string(),
3452 };
3453
3454 let event_data: EventData = data.into();
3455 assert_eq!(event_data.event_type(), REASON_THINKING_DELTA);
3456
3457 let json = serde_json::to_string(&event_data).unwrap();
3459 assert!(json.contains("turn_id"));
3460 assert!(json.contains("delta"));
3461 assert!(json.contains("accumulated"));
3462 }
3463
3464 #[test]
3465 fn test_reason_thinking_completed_data() {
3466 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3467 let data = ReasonThinkingCompletedData {
3468 turn_id,
3469 thinking: "Full thinking content here".to_string(),
3470 };
3471
3472 let event_data: EventData = data.into();
3473 assert_eq!(event_data.event_type(), REASON_THINKING_COMPLETED);
3474
3475 let json = serde_json::to_string(&event_data).unwrap();
3477 assert!(json.contains("turn_id"));
3478 assert!(json.contains("thinking"));
3479 }
3480
3481 #[test]
3482 fn test_output_message_delta_data() {
3483 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3484 let data = OutputMessageDeltaData {
3485 turn_id,
3486 message_id: MessageId::new(),
3487 delta: "Hello".to_string(),
3488 accumulated: "Hello".to_string(),
3489 phase: None,
3490 };
3491
3492 let event_data: EventData = data.into();
3493 assert_eq!(event_data.event_type(), OUTPUT_MESSAGE_DELTA);
3494
3495 let json = serde_json::to_string(&event_data).unwrap();
3497 assert!(json.contains("turn_id"));
3498 assert!(json.contains("delta"));
3499 assert!(json.contains("accumulated"));
3500 }
3501
3502 #[test]
3503 fn test_output_message_lifecycle_shares_message_id() {
3504 let turn_id = TurnId::new();
3505 let message_id = MessageId::new();
3506 let next_message_id = MessageId::new();
3507
3508 let started = OutputMessageStartedData {
3509 turn_id,
3510 message_id,
3511 model: None,
3512 iteration: Some(1),
3513 phase: None,
3514 };
3515 let next_started = OutputMessageStartedData {
3516 turn_id,
3517 message_id: next_message_id,
3518 model: None,
3519 iteration: Some(2),
3520 phase: None,
3521 };
3522 let delta = OutputMessageDeltaData {
3523 turn_id,
3524 message_id,
3525 delta: "Hello".to_string(),
3526 accumulated: "Hello".to_string(),
3527 phase: None,
3528 };
3529 let replaced = OutputMessageReplacedData {
3530 turn_id,
3531 message_id,
3532 guardrail_capability_id: "guardrails".to_string(),
3533 guardrail_id: "example".to_string(),
3534 reason_code: "blocked".to_string(),
3535 replacement: "Safe response".to_string(),
3536 };
3537 let completed = OutputMessageCompletedData::new(
3538 Message::assistant("Safe response").with_id(message_id),
3539 );
3540
3541 assert_eq!(started.message_id, message_id);
3542 assert_eq!(delta.message_id, message_id);
3543 assert_eq!(replaced.message_id, message_id);
3544 assert_eq!(completed.message.id, message_id);
3545 assert_ne!(started.message_id, next_started.message_id);
3546
3547 for value in [
3548 serde_json::to_value(started).unwrap(),
3549 serde_json::to_value(delta).unwrap(),
3550 serde_json::to_value(replaced).unwrap(),
3551 ] {
3552 assert_eq!(value["message_id"], message_id.to_string());
3553 }
3554 }
3555
3556 #[test]
3557 fn test_output_message_phase_hint_serde() {
3558 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3562
3563 let started_none = OutputMessageStartedData {
3564 turn_id,
3565 message_id: MessageId::new(),
3566 model: None,
3567 iteration: None,
3568 phase: None,
3569 };
3570 assert!(
3571 !serde_json::to_string(&started_none)
3572 .unwrap()
3573 .contains("phase")
3574 );
3575
3576 let delta_commentary = OutputMessageDeltaData {
3577 turn_id,
3578 message_id: MessageId::new(),
3579 delta: "one moment".to_string(),
3580 accumulated: "one moment".to_string(),
3581 phase: Some(crate::message::ExecutionPhase::Commentary),
3582 };
3583 let json = serde_json::to_value(&delta_commentary).unwrap();
3584 assert_eq!(json["phase"], "commentary");
3585
3586 let back: OutputMessageDeltaData = serde_json::from_value(json).unwrap();
3588 assert_eq!(back.phase, Some(crate::message::ExecutionPhase::Commentary));
3589
3590 let delta_final = OutputMessageDeltaData {
3591 turn_id,
3592 message_id: MessageId::new(),
3593 delta: "done".to_string(),
3594 accumulated: "done".to_string(),
3595 phase: Some(crate::message::ExecutionPhase::FinalAnswer),
3596 };
3597 assert_eq!(
3598 serde_json::to_value(&delta_final).unwrap()["phase"],
3599 "final_answer"
3600 );
3601 }
3602
3603 #[test]
3604 fn test_output_message_delta_deserialization_preserves_fields() {
3605 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3608 let data = OutputMessageDeltaData {
3609 turn_id,
3610 message_id: MessageId::new(),
3611 delta: "Hello world".to_string(),
3612 accumulated: "Hello world".to_string(),
3613 phase: None,
3614 };
3615
3616 let json = serde_json::to_value(EventData::OutputMessageDelta(data.clone())).unwrap();
3618
3619 let deserialized = deserialize_event_data(OUTPUT_MESSAGE_DELTA, json);
3621
3622 match deserialized {
3624 EventData::OutputMessageDelta(td) => {
3625 assert_eq!(td.turn_id, turn_id);
3626 assert_eq!(td.delta, "Hello world");
3627 assert_eq!(td.accumulated, "Hello world");
3628 }
3629 _ => panic!("Expected OutputMessageDelta, got different variant"),
3630 }
3631 }
3632
3633 #[test]
3634 fn test_output_message_started_deserialization() {
3635 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3636 let data = OutputMessageStartedData {
3637 turn_id,
3638 message_id: MessageId::new(),
3639 model: Some("claude-3".to_string()),
3640 iteration: None,
3641 phase: None,
3642 };
3643
3644 let json = serde_json::to_value(EventData::OutputMessageStarted(data.clone())).unwrap();
3646
3647 let deserialized = deserialize_event_data(OUTPUT_MESSAGE_STARTED, json);
3649
3650 match deserialized {
3652 EventData::OutputMessageStarted(at) => {
3653 assert_eq!(at.turn_id, turn_id);
3654 assert_eq!(at.model, Some("claude-3".to_string()));
3655 }
3656 _ => panic!("Expected OutputMessageStarted, got different variant"),
3657 }
3658 }
3659
3660 #[test]
3661 fn test_reason_thinking_started_deserialization() {
3662 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3667 let data = ReasonThinkingStartedData {
3668 turn_id,
3669 model: Some("claude-3".to_string()),
3670 };
3671
3672 let json = serde_json::to_value(&data).unwrap();
3674
3675 let deserialized = deserialize_event_data(REASON_THINKING_STARTED, json);
3677
3678 match deserialized {
3680 EventData::ReasonThinkingStarted(at) => {
3681 assert_eq!(at.turn_id, turn_id);
3682 assert_eq!(at.model, Some("claude-3".to_string()));
3683 }
3684 other => panic!("Expected ReasonThinkingStarted, got {}", other.event_type()),
3685 }
3686 }
3687
3688 #[test]
3689 fn test_llm_generation_with_ttft() {
3690 let messages = vec![Message::user("Hello")];
3691 let data = LlmGenerationData::success_with_metadata(
3692 messages,
3693 vec![],
3694 Some("Hi!".to_string()),
3695 vec![],
3696 "gpt-4o".to_string(),
3697 Some("openai".to_string()),
3698 Some(TokenUsage {
3699 input_tokens: 10,
3700 output_tokens: 5,
3701 cache_read_tokens: None,
3702 cache_creation_tokens: None,
3703 actual_cost_usd: None,
3704 estimated_cost_usd: None,
3705 effective_cost_usd: None,
3706 }),
3707 Some(500), Some(120), Some(vec!["stop".to_string()]),
3710 None,
3711 );
3712
3713 assert!(data.metadata.success);
3714 assert_eq!(data.metadata.duration_ms, Some(500));
3715 assert_eq!(data.metadata.time_to_first_token_ms, Some(120));
3716 }
3717
3718 #[test]
3719 fn test_llm_generation_ttft_serialization() {
3720 let messages = vec![Message::user("test")];
3721 let data = LlmGenerationData::success_with_metadata(
3722 messages,
3723 vec![],
3724 Some("response".to_string()),
3725 vec![],
3726 "model".to_string(),
3727 None,
3728 None,
3729 Some(1000),
3730 Some(150), None,
3732 None,
3733 );
3734
3735 let json = serde_json::to_string(&data).unwrap();
3736 assert!(json.contains("time_to_first_token_ms"));
3737 assert!(json.contains("150"));
3738 }
3739
3740 #[test]
3741 fn test_reason_item_data_event_type_and_serialization() {
3742 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3743 let data = ReasonItemData {
3744 turn_id,
3745 provider: "openai".to_string(),
3746 model: Some("gpt-5.5".to_string()),
3747 item_id: "rs_abc".to_string(),
3748 encrypted_content: Some("OPAQUE_BLOB".to_string()),
3749 summary: vec!["safe summary".to_string()],
3750 token_count: Some(123),
3751 };
3752
3753 let event_data: EventData = data.into();
3754 assert_eq!(event_data.event_type(), REASON_ITEM);
3755
3756 let json = serde_json::to_string(&event_data).unwrap();
3757 assert!(json.contains("turn_id"));
3758 assert!(json.contains("openai"));
3759 assert!(json.contains("rs_abc"));
3760 assert!(json.contains("OPAQUE_BLOB"));
3761 assert!(json.contains("safe summary"));
3762 }
3763
3764 #[test]
3765 fn test_event_deserialize_reason_item_uses_event_type_dispatch() {
3766 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3767 let payload = serde_json::json!({
3768 "id": EventId::new().to_string(),
3769 "type": REASON_ITEM,
3770 "ts": Utc::now().to_rfc3339(),
3771 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
3772 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
3773 "data": {
3774 "turn_id": turn_id.to_string(),
3775 "provider": "openai",
3776 "model": "gpt-5",
3777 "item_id": "rs_event",
3778 "encrypted_content": "ENC",
3779 "summary": ["safe"],
3780 "token_count": 9
3781 }
3782 });
3783
3784 let event: Event = serde_json::from_value(payload).expect("event deserializes");
3785 match event.data {
3786 EventData::ReasonItem(data) => {
3787 assert_eq!(data.turn_id, turn_id);
3788 assert_eq!(data.provider, "openai");
3789 assert_eq!(data.item_id, "rs_event");
3790 assert_eq!(data.token_count, Some(9));
3791 }
3792 other => panic!("expected reason.item data, got {}", other.event_type()),
3793 }
3794 }
3795
3796 #[test]
3797 fn test_event_request_deserialize_reason_item_uses_event_type_dispatch() {
3798 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3799 let payload = serde_json::json!({
3800 "type": REASON_ITEM,
3801 "ts": Utc::now().to_rfc3339(),
3802 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
3803 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
3804 "data": {
3805 "turn_id": turn_id.to_string(),
3806 "provider": "openai",
3807 "item_id": "rs_request",
3808 "encrypted_content": "ENC",
3809 "summary": ["safe"]
3810 }
3811 });
3812
3813 let req: EventRequest = serde_json::from_value(payload).expect("request deserializes");
3814 match req.data {
3815 EventData::ReasonItem(data) => {
3816 assert_eq!(data.turn_id, turn_id);
3817 assert_eq!(data.provider, "openai");
3818 assert_eq!(data.item_id, "rs_request");
3819 }
3820 other => panic!("expected reason.item data, got {}", other.event_type()),
3821 }
3822 }
3823
3824 #[test]
3825 fn test_reason_item_data_round_trip_uses_typed_dispatch() {
3826 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3830 let data = ReasonItemData {
3831 turn_id,
3832 provider: "openai".to_string(),
3833 model: Some("gpt-5".to_string()),
3834 item_id: "rs_xyz".to_string(),
3835 encrypted_content: Some("ENC".to_string()),
3836 summary: vec![],
3837 token_count: None,
3838 };
3839
3840 let json = serde_json::to_value(&data).unwrap();
3841 let deserialized = deserialize_event_data(REASON_ITEM, json);
3842
3843 match deserialized {
3844 EventData::ReasonItem(out) => {
3845 assert_eq!(out.turn_id, turn_id);
3846 assert_eq!(out.provider, "openai");
3847 assert_eq!(out.item_id, "rs_xyz");
3848 assert_eq!(out.encrypted_content.as_deref(), Some("ENC"));
3849 }
3850 other => panic!("Expected ReasonItem, got {}", other.event_type()),
3851 }
3852 }
3853
3854 #[test]
3863 fn test_reason_item_resolves_via_type_dispatch_despite_overlap() {
3864 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3869 let json = serde_json::json!({
3870 "turn_id": turn_id.to_string(),
3871 "provider": "openai",
3872 "model": "gpt-5",
3873 "item_id": "rs_keep",
3874 "encrypted_content": "ENC",
3875 "summary": ["s"],
3876 "token_count": 7,
3877 });
3878
3879 let as_thinking: ReasonThinkingStartedData =
3884 serde_json::from_value(json.clone()).expect("thinking ignores extra fields");
3885 assert_eq!(as_thinking.turn_id, turn_id);
3886 assert_eq!(as_thinking.model.as_deref(), Some("gpt-5"));
3887
3888 let as_item: ReasonItemData =
3889 serde_json::from_value(json.clone()).expect("ReasonItem accepts payload");
3890 assert_eq!(as_item.item_id, "rs_keep");
3891 assert_eq!(as_item.provider, "openai");
3892
3893 let event_data = deserialize_event_data(REASON_ITEM, json);
3896 match event_data {
3897 EventData::ReasonItem(out) => {
3898 assert_eq!(out.item_id, "rs_keep");
3899 assert_eq!(out.provider, "openai");
3900 }
3901 other => panic!(
3902 "Typed dispatcher must select ReasonItem for {REASON_ITEM}, got {}",
3903 other.event_type()
3904 ),
3905 }
3906 }
3907
3908 #[test]
3915 fn test_reason_item_data_excludes_plaintext_reasoning() {
3916 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3917 let data = ReasonItemData {
3918 turn_id,
3919 provider: "openai".to_string(),
3920 model: Some("gpt-5".to_string()),
3921 item_id: "rs_secret".to_string(),
3922 encrypted_content: Some("opaque_blob_thinking_content_reasoning_text".to_string()),
3926 summary: vec!["safe summary mentioning content and thinking".to_string()],
3927 token_count: Some(1),
3928 };
3929
3930 let value = serde_json::to_value(&data).expect("serializable");
3931 let object = value.as_object().expect("data serializes to JSON object");
3932 for forbidden in [
3933 "content",
3934 "reasoning_text",
3935 "thinking",
3936 "reasoning_content",
3937 "raw_reasoning",
3938 ] {
3939 assert!(
3940 !object.contains_key(forbidden),
3941 "ReasonItemData JSON must not expose `{forbidden}` key, got: {object:?}",
3942 );
3943 }
3944 assert!(object.contains_key("encrypted_content"));
3946 assert!(object.contains_key("summary"));
3947 }
3948
3949 #[test]
3950 fn test_llm_generation_ttft_omitted_when_none() {
3951 let messages = vec![Message::user("test")];
3952 let data = LlmGenerationData::success(
3953 messages,
3954 vec![],
3955 Some("response".to_string()),
3956 vec![],
3957 "model".to_string(),
3958 None,
3959 None,
3960 None,
3961 None, );
3963
3964 assert!(data.metadata.time_to_first_token_ms.is_none());
3966
3967 let json = serde_json::to_string(&data).unwrap();
3969 assert!(!json.contains("time_to_first_token_ms"));
3970 }
3971}
3972
3973#[cfg(test)]
3982mod contract_tests {
3983 use super::*;
3984 use insta::{assert_json_snapshot, with_settings};
3985
3986 fn test_session_id() -> SessionId {
3988 SessionId::from_uuid(uuid::Uuid::from_u128(
3989 0x0000_0000_0000_0000_0000_0000_0000_0001,
3990 ))
3991 }
3992
3993 fn test_turn_id() -> TurnId {
3994 TurnId::from_uuid(uuid::Uuid::from_u128(
3995 0x0000_0000_0000_0000_0000_0000_0000_0002,
3996 ))
3997 }
3998
3999 fn test_message_id() -> MessageId {
4000 MessageId::from_uuid(uuid::Uuid::from_u128(
4001 0x0000_0000_0000_0000_0000_0000_0000_0003,
4002 ))
4003 }
4004
4005 fn test_agent_id() -> AgentId {
4006 AgentId::from_uuid(uuid::Uuid::from_u128(
4007 0x0000_0000_0000_0000_0000_0000_0000_0004,
4008 ))
4009 }
4010
4011 fn test_harness_id() -> HarnessId {
4012 HarnessId::from_uuid(uuid::Uuid::from_u128(
4013 0x0000_0000_0000_0000_0000_0000_0000_0005,
4014 ))
4015 }
4016
4017 #[test]
4024 fn snapshot_input_message() {
4025 let data = InputMessageData::new(Message::user("Hello, world!"));
4026 with_settings!({
4027 sort_maps => true,
4028 }, {
4029 assert_json_snapshot!("event_data_input_message", data, {
4031 ".message.id" => "[MESSAGE_ID]",
4032 ".message.created_at" => "[TIMESTAMP]"
4033 });
4034 });
4035 }
4036
4037 #[test]
4038 fn snapshot_output_message_started() {
4039 let data = OutputMessageStartedData {
4040 turn_id: test_turn_id(),
4041 message_id: test_message_id(),
4042 model: Some("gpt-4o".to_string()),
4043 iteration: None,
4044 phase: None,
4045 };
4046 with_settings!({
4047 sort_maps => true,
4048 }, {
4049 assert_json_snapshot!("event_data_output_message_started", data);
4050 });
4051 }
4052
4053 #[test]
4054 fn snapshot_output_message_delta() {
4055 let data = OutputMessageDeltaData {
4056 turn_id: test_turn_id(),
4057 message_id: test_message_id(),
4058 delta: "Hello".to_string(),
4059 accumulated: "Hello".to_string(),
4060 phase: None,
4061 };
4062 with_settings!({
4063 sort_maps => true,
4064 }, {
4065 assert_json_snapshot!("event_data_output_message_delta", data);
4066 });
4067 }
4068
4069 #[test]
4070 fn snapshot_output_message_completed() {
4071 let data = OutputMessageCompletedData::new(Message::assistant("Hello!"));
4072 with_settings!({
4073 sort_maps => true,
4074 }, {
4075 assert_json_snapshot!("event_data_output_message_completed", data, {
4077 ".message.id" => "[MESSAGE_ID]",
4078 ".message.created_at" => "[TIMESTAMP]"
4079 });
4080 });
4081 }
4082
4083 #[test]
4084 fn snapshot_turn_started() {
4085 let data = TurnStartedData {
4086 turn_id: test_turn_id(),
4087 input_message_id: test_message_id(),
4088 input_content: Some("Hello".to_string()),
4089 };
4090 with_settings!({
4091 sort_maps => true,
4092 }, {
4093 assert_json_snapshot!("event_data_turn_started", data);
4094 });
4095 }
4096
4097 #[test]
4098 fn snapshot_turn_completed() {
4099 let data = TurnCompletedData {
4100 turn_id: test_turn_id(),
4101 iterations: 3,
4102 duration_ms: Some(1500),
4103 usage: Some(TokenUsage::new(100, 50)),
4104 input_content: None,
4105 final_message_id: Some(test_message_id()),
4106 final_answer_preview: Some("Done.".to_string()),
4107 time_to_first_token_ms: Some(120),
4108 tool_call_count: Some(2),
4109 llm_call_count: Some(3),
4110 status: Some("completed".to_string()),
4111 };
4112 with_settings!({
4113 sort_maps => true,
4114 }, {
4115 assert_json_snapshot!("event_data_turn_completed", data);
4116 });
4117 }
4118
4119 #[test]
4120 fn snapshot_turn_failed() {
4121 let data = TurnFailedData {
4122 turn_id: test_turn_id(),
4123 error: "Rate limit exceeded".to_string(),
4124 error_code: Some("RATE_LIMIT".to_string()),
4125 error_fields: None,
4126 error_disclosure: None,
4127 };
4128 with_settings!({
4129 sort_maps => true,
4130 }, {
4131 assert_json_snapshot!("event_data_turn_failed", data);
4132 });
4133 }
4134
4135 #[test]
4136 fn snapshot_turn_cancelled() {
4137 let data = TurnCancelledData {
4138 turn_id: test_turn_id(),
4139 reason: Some("User requested".to_string()),
4140 usage: Some(TokenUsage::new(50, 25)),
4141 };
4142 with_settings!({
4143 sort_maps => true,
4144 }, {
4145 assert_json_snapshot!("event_data_turn_cancelled", data);
4146 });
4147 }
4148
4149 #[test]
4150 fn snapshot_reason_started() {
4151 let data = ReasonStartedData {
4152 harness_id: test_harness_id(),
4153 agent_id: Some(test_agent_id()),
4154 metadata: Some(ModelMetadata {
4155 model: "gpt-4o".to_string(),
4156 model_id: None,
4157 provider_id: None,
4158 }),
4159 };
4160 with_settings!({
4161 sort_maps => true,
4162 }, {
4163 assert_json_snapshot!("event_data_reason_started", data);
4164 });
4165 }
4166
4167 #[test]
4168 fn snapshot_reason_completed() {
4169 let data = ReasonCompletedData::success(
4170 "Hello world",
4171 true,
4172 2,
4173 Some(1000),
4174 Some(TokenUsage::new(100, 50)),
4175 );
4176 with_settings!({
4177 sort_maps => true,
4178 }, {
4179 assert_json_snapshot!("event_data_reason_completed", data);
4180 });
4181 }
4182
4183 #[test]
4184 fn snapshot_act_started() {
4185 let data = ActStartedData {
4186 tool_calls: vec![ToolCallSummary {
4187 id: "tc_1".to_string(),
4188 name: "get_weather".to_string(),
4189 display_name: None,
4190 narration: None,
4191 }],
4192 headline: None,
4193 };
4194 with_settings!({
4195 sort_maps => true,
4196 }, {
4197 assert_json_snapshot!("event_data_act_started", data);
4198 });
4199 }
4200
4201 #[test]
4202 fn snapshot_act_completed() {
4203 let data = ActCompletedData {
4204 completed: true,
4205 success_count: 2,
4206 error_count: 0,
4207 duration_ms: Some(500),
4208 headline: None,
4209 };
4210 with_settings!({
4211 sort_maps => true,
4212 }, {
4213 assert_json_snapshot!("event_data_act_completed", data);
4214 });
4215 }
4216
4217 #[test]
4218 fn snapshot_tool_started() {
4219 let data = ToolStartedData {
4220 tool_call: ToolCall {
4221 id: "tc_1".to_string(),
4222 name: "get_weather".to_string(),
4223 arguments: serde_json::json!({"city": "London"}),
4224 },
4225 tool_call_fingerprint: None,
4226 display_name: None,
4227 narration: None,
4228 };
4229 with_settings!({
4230 sort_maps => true,
4231 }, {
4232 assert_json_snapshot!("event_data_tool_started", data);
4233 });
4234 }
4235
4236 #[test]
4237 fn snapshot_tool_completed() {
4238 let data = ToolCompletedData::success(
4239 "tc_1".to_string(),
4240 "get_weather".to_string(),
4241 vec![crate::message::ContentPart::text("Sunny, 22°C")],
4242 Some(250),
4243 );
4244 with_settings!({
4245 sort_maps => true,
4246 }, {
4247 assert_json_snapshot!("event_data_tool_completed", data);
4248 });
4249 }
4250
4251 #[test]
4252 fn snapshot_llm_generation() {
4253 let data = LlmGenerationData::success(
4254 vec![Message::user("Hello")],
4255 vec![ToolDefinitionSummary {
4256 name: "tool1".to_string(),
4257 display_name: None,
4258 category: None,
4259 capability_id: None,
4260 capability_name: None,
4261 description: "A tool".to_string(),
4262 }],
4263 Some("Hi there!".to_string()),
4264 vec![],
4265 "gpt-4o".to_string(),
4266 Some("openai".to_string()),
4267 Some(TokenUsage::new(10, 5)),
4268 Some(100),
4269 Some(25),
4270 );
4271 with_settings!({
4272 sort_maps => true,
4273 }, {
4274 assert_json_snapshot!("event_data_llm_generation", data, {
4276 ".messages[].id" => "[MESSAGE_ID]",
4277 ".messages[].created_at" => "[TIMESTAMP]"
4278 });
4279 });
4280 }
4281
4282 #[test]
4283 fn snapshot_reason_thinking_started() {
4284 let data = ReasonThinkingStartedData {
4285 turn_id: test_turn_id(),
4286 model: Some("claude-4-opus".to_string()),
4287 };
4288 with_settings!({
4289 sort_maps => true,
4290 }, {
4291 assert_json_snapshot!("event_data_reason_thinking_started", data);
4292 });
4293 }
4294
4295 #[test]
4296 fn snapshot_reason_thinking_delta() {
4297 let data = ReasonThinkingDeltaData {
4298 turn_id: test_turn_id(),
4299 delta: "Let me think...".to_string(),
4300 accumulated: "Let me think...".to_string(),
4301 };
4302 with_settings!({
4303 sort_maps => true,
4304 }, {
4305 assert_json_snapshot!("event_data_reason_thinking_delta", data);
4306 });
4307 }
4308
4309 #[test]
4310 fn snapshot_reason_thinking_completed() {
4311 let data = ReasonThinkingCompletedData {
4312 turn_id: test_turn_id(),
4313 thinking: "I need to consider...".to_string(),
4314 };
4315 with_settings!({
4316 sort_maps => true,
4317 }, {
4318 assert_json_snapshot!("event_data_reason_thinking_completed", data);
4319 });
4320 }
4321
4322 #[test]
4323 fn snapshot_reason_item() {
4324 let data = ReasonItemData {
4325 turn_id: test_turn_id(),
4326 provider: "openai".to_string(),
4327 model: Some("gpt-5.5".to_string()),
4328 item_id: "rs_test".to_string(),
4329 encrypted_content: Some("OPAQUE".to_string()),
4330 summary: vec!["safe summary".to_string()],
4331 token_count: Some(42),
4332 };
4333 with_settings!({
4334 sort_maps => true,
4335 }, {
4336 assert_json_snapshot!("event_data_reason_item", data);
4337 });
4338 }
4339
4340 #[test]
4341 fn snapshot_session_started() {
4342 let data = SessionStartedData {
4343 harness_id: test_harness_id(),
4344 agent_id: Some(test_agent_id()),
4345 model_id: None,
4346 };
4347 with_settings!({
4348 sort_maps => true,
4349 }, {
4350 assert_json_snapshot!("event_data_session_started", data);
4351 });
4352 }
4353
4354 #[test]
4355 fn snapshot_session_activated() {
4356 let data = SessionActivatedData {
4357 turn_id: test_turn_id(),
4358 input_message_id: test_message_id(),
4359 };
4360 with_settings!({
4361 sort_maps => true,
4362 }, {
4363 assert_json_snapshot!("event_data_session_activated", data);
4364 });
4365 }
4366
4367 #[test]
4368 fn snapshot_session_idled() {
4369 let data = SessionIdledData {
4370 turn_id: test_turn_id(),
4371 iterations: Some(3),
4372 usage: Some(TokenUsage::new(500, 200)),
4373 };
4374 with_settings!({
4375 sort_maps => true,
4376 }, {
4377 assert_json_snapshot!("event_data_session_idled", data);
4378 });
4379 }
4380
4381 #[test]
4387 fn tool_call_summary_with_display_name() {
4388 let summary = ToolCallSummary {
4389 id: "tc_1".to_string(),
4390 name: "get_weather".to_string(),
4391 display_name: Some("Get Weather".to_string()),
4392 narration: None,
4393 };
4394 let json = serde_json::to_value(&summary).unwrap();
4395 assert_eq!(json["display_name"], "Get Weather");
4396
4397 let deserialized: ToolCallSummary = serde_json::from_value(json).unwrap();
4399 assert_eq!(deserialized.display_name.as_deref(), Some("Get Weather"));
4400 }
4401
4402 #[test]
4403 fn tool_call_summary_without_display_name_omits_field() {
4404 let summary = ToolCallSummary {
4405 id: "tc_1".to_string(),
4406 name: "get_weather".to_string(),
4407 display_name: None,
4408 narration: None,
4409 };
4410 let json = serde_json::to_string(&summary).unwrap();
4411 assert!(!json.contains("display_name"));
4412
4413 let json_without = r#"{"id":"tc_1","name":"get_weather"}"#;
4415 let deserialized: ToolCallSummary = serde_json::from_str(json_without).unwrap();
4416 assert_eq!(deserialized.display_name, None);
4417 }
4418
4419 #[test]
4420 fn act_started_with_definitions_populates_display_names() {
4421 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4422
4423 let tool_calls = vec![
4424 ToolCall {
4425 id: "tc_1".to_string(),
4426 name: "get_weather".to_string(),
4427 arguments: serde_json::json!({}),
4428 },
4429 ToolCall {
4430 id: "tc_2".to_string(),
4431 name: "unknown_tool".to_string(),
4432 arguments: serde_json::json!({}),
4433 },
4434 ];
4435 let tool_defs = vec![crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4436 name: "get_weather".to_string(),
4437 display_name: Some("Get Weather".to_string()),
4438 description: "Gets weather".to_string(),
4439 parameters: serde_json::json!({}),
4440 policy: ToolPolicy::Auto,
4441 category: None,
4442 deferrable: DeferrablePolicy::default(),
4443 hints: crate::tool_types::ToolHints::default(),
4444 full_parameters: None,
4445 })];
4446
4447 let data = ActStartedData::with_definitions(&tool_calls, &tool_defs);
4448 assert_eq!(data.tool_calls.len(), 2);
4449 assert_eq!(
4450 data.tool_calls[0].display_name.as_deref(),
4451 Some("Get Weather")
4452 );
4453 assert_eq!(data.tool_calls[1].display_name, None);
4454 }
4455
4456 #[test]
4457 fn tool_completed_with_display_name_roundtrip() {
4458 let data = ToolCompletedData::success(
4459 "tc_1".to_string(),
4460 "get_weather".to_string(),
4461 vec![crate::message::ContentPart::text("Sunny")],
4462 Some(100),
4463 )
4464 .with_display_name(Some("Get Weather".to_string()));
4465
4466 assert_eq!(data.display_name.as_deref(), Some("Get Weather"));
4467
4468 let json = serde_json::to_value(&data).unwrap();
4469 assert_eq!(json["display_name"], "Get Weather");
4470
4471 let deserialized: ToolCompletedData = serde_json::from_value(json).unwrap();
4472 assert_eq!(deserialized.display_name.as_deref(), Some("Get Weather"));
4473 }
4474
4475 #[test]
4476 fn tool_started_display_name_serialization() {
4477 let data = ToolStartedData {
4478 tool_call: ToolCall {
4479 id: "tc_1".to_string(),
4480 name: "bash".to_string(),
4481 arguments: serde_json::json!({"command": "ls"}),
4482 },
4483 tool_call_fingerprint: None,
4484 display_name: Some("Bash".to_string()),
4485 narration: None,
4486 };
4487
4488 let json = serde_json::to_value(&data).unwrap();
4489 assert_eq!(json["display_name"], "Bash");
4490 }
4491
4492 #[test]
4493 fn tool_definition_summary_display_name() {
4494 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4495
4496 let def = crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4497 name: "read_file".to_string(),
4498 display_name: Some("Read File".to_string()),
4499 description: "Reads a file".to_string(),
4500 parameters: serde_json::json!({}),
4501 policy: ToolPolicy::Auto,
4502 category: None,
4503 deferrable: DeferrablePolicy::default(),
4504 hints: crate::tool_types::ToolHints::default(),
4505 full_parameters: None,
4506 });
4507
4508 let summary = ToolDefinitionSummary::from(&def);
4509 assert_eq!(summary.display_name.as_deref(), Some("Read File"));
4510
4511 let json = serde_json::to_value(&summary).unwrap();
4512 assert_eq!(json["display_name"], "Read File");
4513 }
4514
4515 #[test]
4522 fn forward_compat_unknown_fields_ignored() {
4523 let json = r#"{
4525 "turn_id": "turn_00000000000000000000000000000002",
4526 "iterations": 3,
4527 "duration_ms": 1500,
4528 "usage": {"input_tokens": 100, "output_tokens": 50},
4529 "future_field": "should be ignored",
4530 "another_new_field": 42
4531 }"#;
4532
4533 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4534 assert_eq!(data.iterations, 3);
4535 assert_eq!(data.duration_ms, Some(1500));
4536 }
4537
4538 #[test]
4539 fn forward_compat_unknown_event_type_becomes_unsupported() {
4540 let json = serde_json::json!({"some_field": "value"});
4542 let data = deserialize_event_data("future.event.type", json);
4543
4544 assert!(data.is_unsupported());
4545 assert_eq!(data.event_type(), "unsupported");
4546 }
4547
4548 #[test]
4549 fn forward_compat_unsupported_preserves_data() {
4550 let original = serde_json::json!({"key": "value", "nested": {"a": 1}});
4552 let data = deserialize_event_data("unknown.event", original.clone());
4553
4554 match data {
4555 EventData::Unsupported { event_type, data } => {
4556 assert_eq!(event_type, "unknown.event");
4557 assert_eq!(data, original);
4558 }
4559 _ => panic!("Expected Unsupported variant"),
4560 }
4561 }
4562
4563 #[test]
4564 fn forward_compat_optional_fields_absent() {
4565 let json = r#"{
4567 "turn_id": "turn_00000000000000000000000000000002",
4568 "iterations": 3
4569 }"#;
4570
4571 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4572 assert_eq!(data.iterations, 3);
4573 assert!(data.duration_ms.is_none());
4574 assert!(data.usage.is_none());
4575 assert!(data.input_content.is_none());
4576 assert!(data.final_message_id.is_none());
4577 assert!(data.final_answer_preview.is_none());
4578 assert!(data.time_to_first_token_ms.is_none());
4579 assert!(data.tool_call_count.is_none());
4580 assert!(data.llm_call_count.is_none());
4581 assert!(data.status.is_none());
4582 }
4583
4584 #[test]
4590 fn round_trip_all_event_data_types() {
4591 let test_cases: Vec<(&str, EventData)> = vec![
4593 (
4594 INPUT_MESSAGE,
4595 InputMessageData::new(Message::user("test")).into(),
4596 ),
4597 (
4598 OUTPUT_MESSAGE_STARTED,
4599 OutputMessageStartedData {
4600 turn_id: test_turn_id(),
4601 message_id: test_message_id(),
4602 model: None,
4603 iteration: None,
4604 phase: None,
4605 }
4606 .into(),
4607 ),
4608 (
4609 OUTPUT_MESSAGE_DELTA,
4610 OutputMessageDeltaData {
4611 turn_id: test_turn_id(),
4612 message_id: test_message_id(),
4613 delta: "x".to_string(),
4614 accumulated: "x".to_string(),
4615 phase: None,
4616 }
4617 .into(),
4618 ),
4619 (
4620 OUTPUT_MESSAGE_COMPLETED,
4621 OutputMessageCompletedData::new(Message::assistant("hi")).into(),
4622 ),
4623 (
4624 TURN_STARTED,
4625 TurnStartedData {
4626 turn_id: test_turn_id(),
4627 input_message_id: test_message_id(),
4628 input_content: None,
4629 }
4630 .into(),
4631 ),
4632 (
4633 TURN_COMPLETED,
4634 TurnCompletedData {
4635 turn_id: test_turn_id(),
4636 iterations: 1,
4637 duration_ms: None,
4638 usage: None,
4639 input_content: None,
4640 final_message_id: None,
4641 final_answer_preview: None,
4642 time_to_first_token_ms: None,
4643 tool_call_count: None,
4644 llm_call_count: None,
4645 status: None,
4646 }
4647 .into(),
4648 ),
4649 (
4650 TURN_FAILED,
4651 TurnFailedData {
4652 turn_id: test_turn_id(),
4653 error: "err".to_string(),
4654 error_code: None,
4655 error_fields: None,
4656 error_disclosure: None,
4657 }
4658 .into(),
4659 ),
4660 (
4661 TURN_CANCELLED,
4662 TurnCancelledData {
4663 turn_id: test_turn_id(),
4664 reason: None,
4665 usage: None,
4666 }
4667 .into(),
4668 ),
4669 (
4670 TURN_SEALED,
4671 TurnSealedData {
4672 turn_id: test_turn_id(),
4673 reason: "no_progress".to_string(),
4674 detail: Some("sealed".to_string()),
4675 iterations: Some(3),
4676 usage: None,
4677 }
4678 .into(),
4679 ),
4680 (
4681 REASON_STARTED,
4682 ReasonStartedData {
4683 harness_id: test_harness_id(),
4684 agent_id: Some(test_agent_id()),
4685 metadata: None,
4686 }
4687 .into(),
4688 ),
4689 (
4690 REASON_COMPLETED,
4691 ReasonCompletedData::success("", false, 0, None, None).into(),
4692 ),
4693 (
4694 ACT_STARTED,
4695 ActStartedData {
4696 tool_calls: vec![],
4697 headline: None,
4698 }
4699 .into(),
4700 ),
4701 (
4702 ACT_COMPLETED,
4703 ActCompletedData {
4704 completed: true,
4705 success_count: 0,
4706 error_count: 0,
4707 duration_ms: None,
4708 headline: None,
4709 }
4710 .into(),
4711 ),
4712 (
4713 SESSION_STARTED,
4714 SessionStartedData {
4715 harness_id: test_harness_id(),
4716 agent_id: Some(test_agent_id()),
4717 model_id: None,
4718 }
4719 .into(),
4720 ),
4721 (
4722 SESSION_ACTIVATED,
4723 SessionActivatedData {
4724 turn_id: test_turn_id(),
4725 input_message_id: test_message_id(),
4726 }
4727 .into(),
4728 ),
4729 (
4730 SESSION_IDLED,
4731 SessionIdledData {
4732 turn_id: test_turn_id(),
4733 iterations: None,
4734 usage: None,
4735 }
4736 .into(),
4737 ),
4738 ];
4739
4740 for (event_type, original) in test_cases {
4741 let json = serde_json::to_value(&original).unwrap();
4743 let deserialized = deserialize_event_data(event_type, json);
4745 assert_eq!(
4747 original.event_type(),
4748 deserialized.event_type(),
4749 "Event type mismatch for {}",
4750 event_type
4751 );
4752 }
4753 }
4754
4755 #[test]
4761 fn event_structure_has_required_fields() {
4762 let session_id = test_session_id();
4763 let context = EventContext::turn(test_turn_id(), test_message_id());
4764 let event = Event::new(
4765 session_id,
4766 context,
4767 InputMessageData::new(Message::user("test")),
4768 );
4769
4770 let json = serde_json::to_value(&event).unwrap();
4772 assert!(json.get("id").is_some(), "Missing id field");
4773 assert!(json.get("type").is_some(), "Missing type field");
4774 assert!(json.get("ts").is_some(), "Missing ts field");
4775 assert!(json.get("session_id").is_some(), "Missing session_id field");
4776 assert!(json.get("context").is_some(), "Missing context field");
4777 assert!(json.get("data").is_some(), "Missing data field");
4778 }
4779
4780 #[test]
4781 fn event_context_span_fields() {
4782 let context = EventContext::empty().with_span(
4783 "trace123".to_string(),
4784 "span456".to_string(),
4785 Some("parent789".to_string()),
4786 );
4787
4788 let json = serde_json::to_value(&context).unwrap();
4789 assert_eq!(
4790 json.get("trace_id").and_then(|v| v.as_str()),
4791 Some("trace123")
4792 );
4793 assert_eq!(
4794 json.get("span_id").and_then(|v| v.as_str()),
4795 Some("span456")
4796 );
4797 assert_eq!(
4798 json.get("parent_span_id").and_then(|v| v.as_str()),
4799 Some("parent789")
4800 );
4801 }
4802
4803 #[test]
4804 fn is_unsupported_returns_false_for_known_types() {
4805 let data = InputMessageData::new(Message::user("test"));
4806 let event_data: EventData = data.into();
4807 assert!(!event_data.is_unsupported());
4808 }
4809
4810 #[test]
4811 fn is_unsupported_returns_true_for_unsupported() {
4812 let data = deserialize_event_data("unknown.type", serde_json::json!({}));
4813 assert!(data.is_unsupported());
4814 }
4815}