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, 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)]
540#[cfg_attr(feature = "openapi", derive(ToSchema))]
541pub struct TokenUsage {
542 pub input_tokens: u32,
544 pub output_tokens: u32,
546 #[serde(skip_serializing_if = "Option::is_none")]
548 pub cache_read_tokens: Option<u32>,
549 #[serde(skip_serializing_if = "Option::is_none")]
551 pub cache_creation_tokens: Option<u32>,
552
553 #[serde(skip_serializing_if = "Option::is_none")]
557 pub actual_cost_usd: Option<f64>,
558
559 #[serde(skip_serializing_if = "Option::is_none")]
564 pub estimated_cost_usd: Option<f64>,
565}
566
567impl TokenUsage {
568 pub fn new(input_tokens: u32, output_tokens: u32) -> Self {
570 Self {
571 input_tokens,
572 output_tokens,
573 cache_read_tokens: None,
574 cache_creation_tokens: None,
575 actual_cost_usd: None,
576 estimated_cost_usd: None,
577 }
578 }
579
580 pub fn with_cache(
582 input_tokens: u32,
583 output_tokens: u32,
584 cache_read_tokens: Option<u32>,
585 cache_creation_tokens: Option<u32>,
586 ) -> Self {
587 Self {
588 input_tokens,
589 output_tokens,
590 cache_read_tokens,
591 cache_creation_tokens,
592 actual_cost_usd: None,
593 estimated_cost_usd: None,
594 }
595 }
596
597 pub fn with_cost(
601 mut self,
602 actual_cost_usd: Option<f64>,
603 estimated_cost_usd: Option<f64>,
604 ) -> Self {
605 self.actual_cost_usd = actual_cost_usd;
606 self.estimated_cost_usd = estimated_cost_usd;
607 self
608 }
609
610 pub fn effective_cost_usd(&self) -> Option<f64> {
614 self.actual_cost_usd.or(self.estimated_cost_usd)
615 }
616
617 pub fn total_tokens(&self) -> u32 {
619 self.input_tokens + self.output_tokens
620 }
621
622 pub fn add(&mut self, other: &TokenUsage) {
624 self.input_tokens += other.input_tokens;
625 self.output_tokens += other.output_tokens;
626 if let Some(cache) = other.cache_read_tokens {
627 *self.cache_read_tokens.get_or_insert(0) += cache;
628 }
629 if let Some(cache) = other.cache_creation_tokens {
630 *self.cache_creation_tokens.get_or_insert(0) += cache;
631 }
632 if let Some(cost) = other.actual_cost_usd {
633 *self.actual_cost_usd.get_or_insert(0.0) += cost;
634 }
635 if let Some(cost) = other.estimated_cost_usd {
636 *self.estimated_cost_usd.get_or_insert(0.0) += cost;
637 }
638 }
639}
640
641#[derive(Debug, Clone, Serialize, Deserialize)]
643#[cfg_attr(feature = "openapi", derive(ToSchema))]
644pub struct InputMessageData {
645 pub message: Message,
647}
648
649impl InputMessageData {
650 pub fn new(message: Message) -> Self {
651 Self { message }
652 }
653}
654
655#[derive(Debug, Clone, Serialize, Deserialize)]
664#[cfg_attr(feature = "openapi", derive(ToSchema))]
665pub struct OutputMessageStartedData {
666 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
668 pub turn_id: TurnId,
669
670 #[serde(skip_serializing_if = "Option::is_none")]
672 pub model: Option<String>,
673
674 #[serde(skip_serializing_if = "Option::is_none")]
677 pub iteration: Option<u32>,
678}
679
680#[derive(Debug, Clone, Serialize, Deserialize)]
685#[cfg_attr(feature = "openapi", derive(ToSchema))]
686pub struct OutputMessageDeltaData {
687 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
689 pub turn_id: TurnId,
690
691 pub delta: String,
693
694 pub accumulated: String,
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize)]
700#[cfg_attr(feature = "openapi", derive(ToSchema))]
701pub struct OutputMessageCompletedData {
702 pub message: Message,
704
705 #[serde(skip_serializing_if = "Option::is_none")]
707 pub metadata: Option<ModelMetadata>,
708
709 #[serde(skip_serializing_if = "Option::is_none")]
711 pub usage: Option<TokenUsage>,
712
713 #[serde(default, skip_serializing_if = "Option::is_none")]
715 pub error_code: Option<String>,
716
717 #[serde(default, skip_serializing_if = "Option::is_none")]
719 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
720 pub error_fields: Option<UserFacingErrorFields>,
721
722 #[serde(default, skip_serializing_if = "Option::is_none")]
726 pub error_disclosure: Option<String>,
727}
728
729impl OutputMessageCompletedData {
730 pub fn new(message: Message) -> Self {
731 Self {
732 message,
733 metadata: None,
734 usage: None,
735 error_code: None,
736 error_fields: None,
737 error_disclosure: None,
738 }
739 }
740
741 pub fn with_metadata(mut self, metadata: ModelMetadata) -> Self {
742 self.metadata = Some(metadata);
743 self
744 }
745
746 pub fn with_usage(mut self, usage: TokenUsage) -> Self {
747 self.usage = Some(usage);
748 self
749 }
750
751 pub fn with_user_facing_error(mut self, error: &UserFacingError) -> Self {
752 error.apply_to_event_fields(&mut self.error_code, &mut self.error_fields);
753 self
754 }
755
756 pub fn with_error_disclosure(mut self, mode: crate::ErrorDisclosure) -> Self {
757 self.error_disclosure = Some(mode.as_str().to_string());
758 self
759 }
760}
761
762#[derive(Debug, Clone, Serialize, Deserialize)]
769#[cfg_attr(feature = "openapi", derive(ToSchema))]
770pub struct OutputMessageReplacedData {
771 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
773 pub turn_id: TurnId,
774
775 pub guardrail_capability_id: String,
778
779 pub guardrail_id: String,
781
782 pub reason_code: String,
785
786 pub replacement: String,
788}
789
790#[derive(Debug, Clone, Serialize, Deserialize)]
796#[cfg_attr(feature = "openapi", derive(ToSchema))]
797pub struct ReasonStartedData {
798 pub harness_id: HarnessId,
800
801 #[serde(skip_serializing_if = "Option::is_none")]
803 pub agent_id: Option<AgentId>,
804
805 #[serde(skip_serializing_if = "Option::is_none")]
807 pub metadata: Option<ModelMetadata>,
808}
809
810#[derive(Debug, Clone, Serialize, Deserialize)]
812#[cfg_attr(feature = "openapi", derive(ToSchema))]
813pub struct ReasonCompletedData {
814 pub success: bool,
816
817 #[serde(skip_serializing_if = "Option::is_none")]
819 pub text_preview: Option<String>,
820
821 pub has_tool_calls: bool,
823
824 pub tool_call_count: u32,
826
827 #[serde(skip_serializing_if = "Option::is_none")]
829 pub error: Option<String>,
830
831 #[serde(skip_serializing_if = "Option::is_none")]
833 pub duration_ms: Option<u64>,
834
835 #[serde(skip_serializing_if = "Option::is_none")]
837 pub usage: Option<TokenUsage>,
838}
839
840impl ReasonCompletedData {
841 pub fn success(
842 text: &str,
843 has_tool_calls: bool,
844 tool_call_count: u32,
845 duration_ms: Option<u64>,
846 usage: Option<TokenUsage>,
847 ) -> Self {
848 let text_preview = if text.is_empty() {
849 None
850 } else {
851 Some(text.chars().take(200).collect())
852 };
853
854 Self {
855 success: true,
856 text_preview,
857 has_tool_calls,
858 tool_call_count,
859 error: None,
860 duration_ms,
861 usage,
862 }
863 }
864
865 pub fn failure(error: String, duration_ms: Option<u64>) -> Self {
866 Self {
867 success: false,
868 text_preview: None,
869 has_tool_calls: false,
870 tool_call_count: 0,
871 error: Some(error),
872 duration_ms,
873 usage: None,
874 }
875 }
876}
877
878#[derive(Debug, Clone, Serialize, Deserialize)]
880#[cfg_attr(feature = "openapi", derive(ToSchema))]
881#[serde(rename_all = "snake_case")]
882pub enum RecoveryMode {
883 Finalize,
886 Restart,
888}
889
890#[derive(Debug, Clone, Serialize, Deserialize)]
896#[cfg_attr(feature = "openapi", derive(ToSchema))]
897pub struct ReasonRecoveredData {
898 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
900 pub turn_id: TurnId,
901
902 pub mode: RecoveryMode,
904
905 pub accumulated_len: usize,
907}
908
909#[derive(Debug, Clone, Serialize, Deserialize)]
911#[serde(rename_all = "snake_case")]
912#[cfg_attr(feature = "openapi", derive(ToSchema))]
913pub enum CapabilityUsageKind {
914 Configured,
915 Resolved,
916 Exposed,
917 Invoked,
918 EffectRan,
919}
920
921#[derive(Debug, Clone, Serialize, Deserialize)]
925#[cfg_attr(feature = "openapi", derive(ToSchema))]
926pub struct CapabilityUsageRecord {
927 pub capability_id: String,
929 #[serde(default, skip_serializing_if = "Option::is_none")]
931 pub capability_name: Option<String>,
932 pub usage_kind: CapabilityUsageKind,
934 #[serde(default, skip_serializing_if = "Option::is_none")]
936 pub tool_name: Option<String>,
937 #[serde(default, skip_serializing_if = "Option::is_none")]
939 pub usage_count: Option<u64>,
940 #[serde(default, skip_serializing_if = "Option::is_none")]
942 pub duration_ms: Option<u64>,
943}
944
945#[derive(Debug, Clone, Serialize, Deserialize)]
947#[cfg_attr(feature = "openapi", derive(ToSchema))]
948pub struct CapabilityUsageData {
949 pub records: Vec<CapabilityUsageRecord>,
950}
951
952#[derive(Debug, Clone, Serialize, Deserialize)]
954#[cfg_attr(feature = "openapi", derive(ToSchema))]
955pub struct ToolCallSummary {
956 pub id: String,
957 pub name: String,
958 #[serde(default, skip_serializing_if = "Option::is_none")]
960 pub display_name: Option<String>,
961 #[serde(default, skip_serializing_if = "Option::is_none")]
963 pub narration: Option<String>,
964}
965
966impl From<&ToolCall> for ToolCallSummary {
967 fn from(tc: &ToolCall) -> Self {
968 Self {
969 id: tc.id.clone(),
970 name: tc.name.clone(),
971 display_name: None,
972 narration: None,
973 }
974 }
975}
976
977#[derive(Debug, Clone, Serialize, Deserialize)]
979#[cfg_attr(feature = "openapi", derive(ToSchema))]
980pub struct ToolDefinitionSummary {
981 pub name: String,
983 #[serde(default, skip_serializing_if = "Option::is_none")]
985 pub display_name: Option<String>,
986 #[serde(default, skip_serializing_if = "Option::is_none")]
988 pub category: Option<String>,
989 #[serde(default, skip_serializing_if = "Option::is_none")]
991 pub capability_id: Option<String>,
992 #[serde(default, skip_serializing_if = "Option::is_none")]
994 pub capability_name: Option<String>,
995 pub description: String,
997}
998
999impl From<&crate::tool_types::ToolDefinition> for ToolDefinitionSummary {
1000 fn from(tool: &crate::tool_types::ToolDefinition) -> Self {
1001 let capability_attribution = tool.capability_attribution();
1002 Self {
1003 name: tool.name().to_string(),
1004 display_name: tool.display_name().map(|s| s.to_string()),
1005 category: tool.category().map(|s| s.to_string()),
1006 capability_id: capability_attribution.map(|(id, _)| id.to_string()),
1007 capability_name: capability_attribution.and_then(|(_, name)| name.map(str::to_string)),
1008 description: tool.description().to_string(),
1009 }
1010 }
1011}
1012
1013#[derive(Debug, Clone, Serialize, Deserialize)]
1015#[cfg_attr(feature = "openapi", derive(ToSchema))]
1016pub struct ActStartedData {
1017 pub tool_calls: Vec<ToolCallSummary>,
1019 #[serde(default, skip_serializing_if = "Option::is_none")]
1021 pub headline: Option<String>,
1022}
1023
1024impl ActStartedData {
1025 pub fn new(tool_calls: &[ToolCall]) -> Self {
1026 Self::new_with_locale(tool_calls, None)
1027 }
1028
1029 pub fn new_with_locale(tool_calls: &[ToolCall], locale: Option<&str>) -> Self {
1030 Self {
1031 tool_calls: tool_calls.iter().map(ToolCallSummary::from).collect(),
1032 headline: render_group_headline_with_locale(
1033 tool_calls,
1034 &[],
1035 ToolNarrationPhase::Started,
1036 locale,
1037 ),
1038 }
1039 }
1040
1041 pub fn with_definitions(
1043 tool_calls: &[ToolCall],
1044 tool_defs: &[crate::tool_types::ToolDefinition],
1045 ) -> Self {
1046 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
1047 }
1048
1049 pub fn with_definitions_and_locale(
1050 tool_calls: &[ToolCall],
1051 tool_defs: &[crate::tool_types::ToolDefinition],
1052 locale: Option<&str>,
1053 ) -> Self {
1054 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
1055 tool_defs.iter().map(|d| (d.name(), d)).collect();
1056 Self {
1057 tool_calls: tool_calls
1058 .iter()
1059 .map(|tc| {
1060 let tool_def = def_map.get(tc.name.as_str()).copied();
1061 let display_name = localized_tool_display_name(
1062 &tc.name,
1063 tool_def.and_then(|d| d.display_name()),
1064 locale,
1065 );
1066 ToolCallSummary {
1067 id: tc.id.clone(),
1068 name: tc.name.clone(),
1069 display_name,
1070 narration: Some(render_tool_narration_with_locale(
1071 tool_def,
1072 tc,
1073 ToolNarrationPhase::Started,
1074 locale,
1075 )),
1076 }
1077 })
1078 .collect(),
1079 headline: render_group_headline_with_locale(
1080 tool_calls,
1081 tool_defs,
1082 ToolNarrationPhase::Started,
1083 locale,
1084 ),
1085 }
1086 }
1087}
1088
1089#[derive(Debug, Clone, Serialize, Deserialize)]
1091#[cfg_attr(feature = "openapi", derive(ToSchema))]
1092pub struct ActCompletedData {
1093 pub completed: bool,
1095
1096 pub success_count: u32,
1098
1099 pub error_count: u32,
1101
1102 #[serde(skip_serializing_if = "Option::is_none")]
1104 pub duration_ms: Option<u64>,
1105 #[serde(default, skip_serializing_if = "Option::is_none")]
1107 pub headline: Option<String>,
1108}
1109
1110#[derive(Debug, Clone, Serialize, Deserialize)]
1112#[cfg_attr(feature = "openapi", derive(ToSchema))]
1113pub struct ToolStartedData {
1114 pub tool_call: ToolCall,
1116 #[serde(default, skip_serializing_if = "Option::is_none")]
1118 pub tool_call_fingerprint: Option<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 narration: Option<String>,
1125}
1126
1127#[derive(Debug, Clone, Serialize, Deserialize)]
1129#[cfg_attr(feature = "openapi", derive(ToSchema))]
1130pub struct ToolCompletedData {
1131 pub tool_call_id: String,
1133
1134 pub tool_name: String,
1136
1137 #[serde(default, skip_serializing_if = "Option::is_none")]
1139 pub tool_call_fingerprint: Option<String>,
1140
1141 #[serde(default, skip_serializing_if = "Option::is_none")]
1143 pub tool_result_fingerprint: Option<String>,
1144
1145 #[serde(default, skip_serializing_if = "Option::is_none")]
1147 pub display_name: Option<String>,
1148
1149 pub success: bool,
1151
1152 pub status: String,
1154
1155 #[serde(skip_serializing_if = "Option::is_none")]
1157 pub result: Option<Vec<ContentPart>>,
1158
1159 #[serde(skip_serializing_if = "Option::is_none")]
1161 pub error: Option<String>,
1162
1163 #[serde(skip_serializing_if = "Option::is_none")]
1165 pub duration_ms: Option<u64>,
1166
1167 #[serde(default, skip_serializing_if = "Option::is_none")]
1169 pub capability_id: Option<String>,
1170
1171 #[serde(default, skip_serializing_if = "Option::is_none")]
1173 pub capability_name: Option<String>,
1174
1175 #[serde(default, skip_serializing_if = "Option::is_none")]
1177 pub narration: Option<String>,
1178}
1179
1180impl ToolCompletedData {
1181 pub fn success(
1182 tool_call_id: String,
1183 tool_name: String,
1184 result: Vec<ContentPart>,
1185 duration_ms: Option<u64>,
1186 ) -> Self {
1187 Self {
1188 tool_call_id,
1189 tool_name,
1190 tool_call_fingerprint: None,
1191 tool_result_fingerprint: None,
1192 display_name: None,
1193 success: true,
1194 status: "success".to_string(),
1195 result: Some(result),
1196 error: None,
1197 duration_ms,
1198 capability_id: None,
1199 capability_name: None,
1200 narration: None,
1201 }
1202 }
1203
1204 pub fn failure(
1205 tool_call_id: String,
1206 tool_name: String,
1207 status: String,
1208 error: String,
1209 duration_ms: Option<u64>,
1210 ) -> Self {
1211 Self {
1212 tool_call_id,
1213 tool_name,
1214 tool_call_fingerprint: None,
1215 tool_result_fingerprint: None,
1216 display_name: None,
1217 success: false,
1218 status,
1219 result: None,
1220 error: Some(error),
1221 duration_ms,
1222 capability_id: None,
1223 capability_name: None,
1224 narration: None,
1225 }
1226 }
1227
1228 pub fn with_display_name(mut self, display_name: Option<String>) -> Self {
1230 self.display_name = display_name;
1231 self
1232 }
1233
1234 pub fn with_fingerprints(
1235 mut self,
1236 tool_call_fingerprint: String,
1237 tool_result_fingerprint: String,
1238 ) -> Self {
1239 self.tool_call_fingerprint = Some(tool_call_fingerprint);
1240 self.tool_result_fingerprint = Some(tool_result_fingerprint);
1241 self
1242 }
1243
1244 pub fn with_narration(mut self, narration: Option<String>) -> Self {
1246 self.narration = narration;
1247 self
1248 }
1249
1250 pub fn with_capability_attribution(
1252 mut self,
1253 capability_id: Option<String>,
1254 capability_name: Option<String>,
1255 ) -> Self {
1256 self.capability_id = capability_id;
1257 self.capability_name = capability_name;
1258 self
1259 }
1260}
1261
1262#[derive(Debug, Clone, Serialize, Deserialize)]
1268#[cfg_attr(feature = "openapi", derive(ToSchema))]
1269pub struct ToolProgressData {
1270 pub tool_call_id: String,
1272
1273 pub tool_name: String,
1275
1276 pub message: String,
1278
1279 #[serde(default, skip_serializing_if = "Option::is_none")]
1281 pub display_name: Option<String>,
1282}
1283
1284#[derive(Debug, Clone, Serialize, Deserialize)]
1294#[cfg_attr(feature = "openapi", derive(ToSchema))]
1295pub struct ToolOutputDeltaData {
1296 pub tool_call_id: String,
1298
1299 pub tool_name: String,
1301
1302 pub delta: String,
1304
1305 pub stream: String,
1307}
1308
1309#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1311#[cfg_attr(feature = "openapi", derive(ToSchema))]
1312#[serde(rename_all = "snake_case")]
1313pub enum TranscriptRepairAction {
1314 Replay,
1316 Synthesize,
1318}
1319
1320#[derive(Debug, Clone, Serialize, Deserialize)]
1326#[cfg_attr(feature = "openapi", derive(ToSchema))]
1327pub struct TranscriptRepairedData {
1328 pub tool_call_id: String,
1330
1331 #[serde(default, skip_serializing_if = "Option::is_none")]
1333 pub tool_name: Option<String>,
1334
1335 pub action: TranscriptRepairAction,
1337}
1338
1339#[derive(Debug, Clone, Serialize, Deserialize)]
1345#[cfg_attr(feature = "openapi", derive(ToSchema))]
1346pub struct ToolCallRepairedData {
1347 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1349 pub turn_id: TurnId,
1350
1351 pub tool_call_id: String,
1353
1354 pub tool_name: String,
1356
1357 pub outcome: String,
1359}
1360
1361#[derive(Debug, Clone, Serialize, Deserialize)]
1366#[cfg_attr(feature = "openapi", derive(ToSchema))]
1367pub struct ToolCallRequestedData {
1368 pub tool_calls: Vec<ToolCall>,
1370 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1372 pub tool_summaries: Vec<ToolCallSummary>,
1373 #[serde(default, skip_serializing_if = "Option::is_none")]
1375 pub headline: Option<String>,
1376}
1377
1378impl ToolCallRequestedData {
1379 pub fn with_definitions(
1380 tool_calls: &[ToolCall],
1381 tool_defs: &[crate::tool_types::ToolDefinition],
1382 ) -> Self {
1383 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
1384 }
1385
1386 pub fn with_definitions_and_locale(
1387 tool_calls: &[ToolCall],
1388 tool_defs: &[crate::tool_types::ToolDefinition],
1389 locale: Option<&str>,
1390 ) -> Self {
1391 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
1392 tool_defs.iter().map(|d| (d.name(), d)).collect();
1393
1394 let tool_summaries = tool_calls
1395 .iter()
1396 .map(|tool_call| {
1397 let tool_def = def_map.get(tool_call.name.as_str()).copied();
1398 ToolCallSummary {
1399 id: tool_call.id.clone(),
1400 name: tool_call.name.clone(),
1401 display_name: localized_tool_display_name(
1402 &tool_call.name,
1403 tool_def.and_then(|def| def.display_name()),
1404 locale,
1405 ),
1406 narration: Some(render_tool_narration_with_locale(
1407 tool_def,
1408 tool_call,
1409 ToolNarrationPhase::Waiting,
1410 locale,
1411 )),
1412 }
1413 })
1414 .collect();
1415
1416 Self {
1417 tool_calls: tool_calls.to_vec(),
1418 tool_summaries,
1419 headline: render_group_headline_with_locale(
1420 tool_calls,
1421 tool_defs,
1422 ToolNarrationPhase::Waiting,
1423 locale,
1424 ),
1425 }
1426 }
1427}
1428
1429#[derive(Debug, Clone, Serialize, Deserialize)]
1435#[cfg_attr(feature = "openapi", derive(ToSchema))]
1436pub struct LlmGenerationOutput {
1437 #[serde(skip_serializing_if = "Option::is_none")]
1439 pub text: Option<String>,
1440
1441 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1443 pub tool_calls: Vec<ToolCall>,
1444}
1445
1446#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1451#[cfg_attr(feature = "openapi", derive(ToSchema))]
1452pub struct LlmRequestOptions {
1453 #[serde(skip_serializing_if = "Option::is_none")]
1455 pub prompt_cache: Option<LlmPromptCacheInfo>,
1456 #[serde(skip_serializing_if = "Option::is_none")]
1458 pub tool_search: Option<LlmToolSearchInfo>,
1459 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1461 pub provider_options: HashMap<String, Value>,
1462 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1465 pub metadata: HashMap<String, String>,
1466}
1467
1468impl LlmRequestOptions {
1469 pub fn is_empty(&self) -> bool {
1470 self.prompt_cache.is_none()
1471 && self.tool_search.is_none()
1472 && self.provider_options.is_empty()
1473 && self.metadata.is_empty()
1474 }
1475}
1476
1477#[derive(Debug, Clone, Serialize, Deserialize)]
1479#[cfg_attr(feature = "openapi", derive(ToSchema))]
1480pub struct LlmPromptCacheInfo {
1481 pub enabled: bool,
1483 pub strategy: crate::driver_registry::PromptCacheStrategy,
1485 #[serde(skip_serializing_if = "Option::is_none")]
1487 pub provider_mode: Option<String>,
1488}
1489
1490#[derive(Debug, Clone, Serialize, Deserialize)]
1492#[cfg_attr(feature = "openapi", derive(ToSchema))]
1493pub struct LlmToolSearchInfo {
1494 pub enabled: bool,
1496 pub threshold: usize,
1498}
1499
1500#[derive(Debug, Clone, Serialize, Deserialize)]
1502#[cfg_attr(feature = "openapi", derive(ToSchema))]
1503pub struct LlmGenerationMetadata {
1504 #[cfg_attr(feature = "openapi", schema(example = "claude-sonnet-4-5"))]
1506 pub model: String,
1507
1508 #[serde(skip_serializing_if = "Option::is_none")]
1510 #[cfg_attr(feature = "openapi", schema(example = "anthropic"))]
1511 pub provider: Option<String>,
1512
1513 #[serde(skip_serializing_if = "Option::is_none")]
1515 pub usage: Option<TokenUsage>,
1516
1517 #[serde(skip_serializing_if = "Option::is_none")]
1519 #[cfg_attr(feature = "openapi", schema(example = 1_842u64))]
1520 pub duration_ms: Option<u64>,
1521
1522 #[serde(skip_serializing_if = "Option::is_none")]
1524 #[cfg_attr(feature = "openapi", schema(example = 312u64))]
1525 pub time_to_first_token_ms: Option<u64>,
1526
1527 #[cfg_attr(feature = "openapi", schema(example = true))]
1529 pub success: bool,
1530
1531 #[serde(skip_serializing_if = "Option::is_none")]
1533 #[cfg_attr(feature = "openapi", schema(example = "provider returned 503"))]
1534 pub error: Option<String>,
1535
1536 #[serde(skip_serializing_if = "Option::is_none")]
1539 #[cfg_attr(feature = "openapi", schema(example = json!(["tool_calls"])))]
1540 pub finish_reasons: Option<Vec<String>>,
1541
1542 #[serde(skip_serializing_if = "Option::is_none")]
1545 #[cfg_attr(feature = "openapi", schema(example = "msg_01ABCDef0123456789"))]
1546 pub response_id: Option<String>,
1547
1548 #[serde(skip_serializing_if = "Option::is_none")]
1551 pub retry: Option<LlmRetryInfo>,
1552
1553 #[serde(skip_serializing_if = "Option::is_none")]
1556 pub compaction: Option<LlmCompactionInfo>,
1557
1558 #[serde(skip_serializing_if = "Option::is_none")]
1560 pub request_options: Option<LlmRequestOptions>,
1561}
1562
1563#[derive(Debug, Clone, Serialize, Deserialize)]
1565#[cfg_attr(feature = "openapi", derive(ToSchema))]
1566pub struct LlmRetryInfo {
1567 pub attempts: u32,
1569
1570 pub total_wait_ms: u64,
1572}
1573
1574#[derive(Debug, Clone, Serialize, Deserialize)]
1579#[cfg_attr(feature = "openapi", derive(ToSchema))]
1580pub struct LlmCompactionInfo {
1581 pub compacted: bool,
1583
1584 #[serde(skip_serializing_if = "Option::is_none")]
1586 pub input_tokens_before: Option<u32>,
1587
1588 #[serde(skip_serializing_if = "Option::is_none")]
1590 pub input_tokens_after: Option<u32>,
1591
1592 #[serde(skip_serializing_if = "Option::is_none")]
1594 pub duration_ms: Option<u64>,
1595}
1596
1597impl LlmCompactionInfo {
1598 pub fn new(
1600 input_tokens_before: Option<u32>,
1601 input_tokens_after: Option<u32>,
1602 duration_ms: Option<u64>,
1603 ) -> Self {
1604 Self {
1605 compacted: true,
1606 input_tokens_before,
1607 input_tokens_after,
1608 duration_ms,
1609 }
1610 }
1611}
1612
1613#[derive(Debug, Clone, Serialize, Deserialize)]
1618#[cfg_attr(feature = "openapi", derive(ToSchema))]
1619pub struct LlmGenerationData {
1620 pub messages: Vec<Message>,
1622
1623 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1625 pub tools: Vec<ToolDefinitionSummary>,
1626
1627 pub output: LlmGenerationOutput,
1629
1630 pub metadata: LlmGenerationMetadata,
1632}
1633
1634impl LlmGenerationData {
1635 #[allow(clippy::too_many_arguments)]
1637 pub fn success(
1638 messages: Vec<Message>,
1639 tools: Vec<ToolDefinitionSummary>,
1640 text: Option<String>,
1641 tool_calls: Vec<ToolCall>,
1642 model: String,
1643 provider: Option<String>,
1644 usage: Option<TokenUsage>,
1645 duration_ms: Option<u64>,
1646 time_to_first_token_ms: Option<u64>,
1647 ) -> Self {
1648 let finish_reasons = if !tool_calls.is_empty() {
1650 Some(vec!["tool_calls".to_string()])
1651 } else {
1652 Some(vec!["stop".to_string()])
1653 };
1654
1655 Self {
1656 messages,
1657 tools,
1658 output: LlmGenerationOutput { text, tool_calls },
1659 metadata: LlmGenerationMetadata {
1660 model,
1661 provider,
1662 usage,
1663 duration_ms,
1664 time_to_first_token_ms,
1665 success: true,
1666 error: None,
1667 finish_reasons,
1668 response_id: None,
1669 retry: None,
1670 compaction: None,
1671 request_options: None,
1672 },
1673 }
1674 }
1675
1676 #[allow(clippy::too_many_arguments)]
1678 pub fn success_with_metadata(
1679 messages: Vec<Message>,
1680 tools: Vec<ToolDefinitionSummary>,
1681 text: Option<String>,
1682 tool_calls: Vec<ToolCall>,
1683 model: String,
1684 provider: Option<String>,
1685 usage: Option<TokenUsage>,
1686 duration_ms: Option<u64>,
1687 time_to_first_token_ms: Option<u64>,
1688 finish_reasons: Option<Vec<String>>,
1689 response_id: Option<String>,
1690 ) -> Self {
1691 Self {
1692 messages,
1693 tools,
1694 output: LlmGenerationOutput { text, tool_calls },
1695 metadata: LlmGenerationMetadata {
1696 model,
1697 provider,
1698 usage,
1699 duration_ms,
1700 time_to_first_token_ms,
1701 success: true,
1702 error: None,
1703 finish_reasons,
1704 response_id,
1705 retry: None,
1706 compaction: None,
1707 request_options: None,
1708 },
1709 }
1710 }
1711
1712 #[allow(clippy::too_many_arguments)]
1714 pub fn success_with_retry(
1715 messages: Vec<Message>,
1716 tools: Vec<ToolDefinitionSummary>,
1717 text: Option<String>,
1718 tool_calls: Vec<ToolCall>,
1719 model: String,
1720 provider: Option<String>,
1721 usage: Option<TokenUsage>,
1722 duration_ms: Option<u64>,
1723 time_to_first_token_ms: Option<u64>,
1724 finish_reasons: Option<Vec<String>>,
1725 response_id: Option<String>,
1726 retry: Option<LlmRetryInfo>,
1727 ) -> Self {
1728 Self {
1729 messages,
1730 tools,
1731 output: LlmGenerationOutput { text, tool_calls },
1732 metadata: LlmGenerationMetadata {
1733 model,
1734 provider,
1735 usage,
1736 duration_ms,
1737 time_to_first_token_ms,
1738 success: true,
1739 error: None,
1740 finish_reasons,
1741 response_id,
1742 retry,
1743 compaction: None,
1744 request_options: None,
1745 },
1746 }
1747 }
1748
1749 pub fn failure(
1751 messages: Vec<Message>,
1752 tools: Vec<ToolDefinitionSummary>,
1753 model: String,
1754 provider: Option<String>,
1755 error: String,
1756 duration_ms: Option<u64>,
1757 time_to_first_token_ms: Option<u64>,
1758 ) -> Self {
1759 Self {
1760 messages,
1761 tools,
1762 output: LlmGenerationOutput {
1763 text: None,
1764 tool_calls: vec![],
1765 },
1766 metadata: LlmGenerationMetadata {
1767 model,
1768 provider,
1769 usage: None,
1770 duration_ms,
1771 time_to_first_token_ms,
1772 success: false,
1773 error: Some(error),
1774 finish_reasons: Some(vec!["error".to_string()]),
1775 response_id: None,
1776 retry: None,
1777 compaction: None,
1778 request_options: None,
1779 },
1780 }
1781 }
1782
1783 pub fn with_compaction(mut self, compaction: LlmCompactionInfo) -> Self {
1787 self.metadata.compaction = Some(compaction);
1788 self
1789 }
1790
1791 pub fn with_retry(mut self, retry: LlmRetryInfo) -> Self {
1793 self.metadata.retry = Some(retry);
1794 self
1795 }
1796
1797 pub fn with_request_options(mut self, request_options: LlmRequestOptions) -> Self {
1799 if !request_options.is_empty() {
1800 self.metadata.request_options = Some(request_options);
1801 }
1802 self
1803 }
1804}
1805
1806#[derive(Debug, Clone, Serialize, Deserialize)]
1816#[cfg_attr(feature = "openapi", derive(ToSchema))]
1817pub struct ReasonThinkingStartedData {
1818 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1820 pub turn_id: TurnId,
1821
1822 #[serde(skip_serializing_if = "Option::is_none")]
1824 pub model: Option<String>,
1825}
1826
1827#[derive(Debug, Clone, Serialize, Deserialize)]
1833#[cfg_attr(feature = "openapi", derive(ToSchema))]
1834pub struct ReasonThinkingDeltaData {
1835 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1837 pub turn_id: TurnId,
1838
1839 pub delta: String,
1841
1842 pub accumulated: String,
1844}
1845
1846#[derive(Debug, Clone, Serialize, Deserialize)]
1851#[cfg_attr(feature = "openapi", derive(ToSchema))]
1852pub struct ReasonThinkingCompletedData {
1853 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1855 pub turn_id: TurnId,
1856
1857 pub thinking: String,
1859}
1860
1861#[derive(Debug, Clone, Serialize, Deserialize)]
1869#[cfg_attr(feature = "openapi", derive(ToSchema))]
1870pub struct ReasonItemData {
1871 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1873 pub turn_id: TurnId,
1874
1875 pub provider: String,
1877
1878 #[serde(skip_serializing_if = "Option::is_none")]
1880 pub model: Option<String>,
1881
1882 pub item_id: String,
1884
1885 #[serde(skip_serializing_if = "Option::is_none")]
1887 pub encrypted_content: Option<String>,
1888
1889 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1892 pub summary: Vec<String>,
1893
1894 #[serde(skip_serializing_if = "Option::is_none")]
1896 pub token_count: Option<u32>,
1897}
1898
1899#[derive(Debug, Clone, Serialize, Deserialize)]
1905#[cfg_attr(feature = "openapi", derive(ToSchema))]
1906pub struct TurnStartedData {
1907 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1909 pub turn_id: TurnId,
1910
1911 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
1913 pub input_message_id: MessageId,
1914
1915 #[serde(skip_serializing_if = "Option::is_none")]
1917 pub input_content: Option<String>,
1918}
1919
1920#[derive(Debug, Clone, Serialize, Deserialize)]
1922#[cfg_attr(feature = "openapi", derive(ToSchema))]
1923pub struct TurnCompletedData {
1924 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1926 pub turn_id: TurnId,
1927
1928 pub iterations: u32,
1930
1931 #[serde(skip_serializing_if = "Option::is_none")]
1933 pub duration_ms: Option<u64>,
1934
1935 #[serde(skip_serializing_if = "Option::is_none")]
1937 pub usage: Option<TokenUsage>,
1938
1939 #[serde(skip_serializing_if = "Option::is_none")]
1941 pub input_content: Option<String>,
1942
1943 #[serde(skip_serializing_if = "Option::is_none")]
1945 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "message_01933b5a00007000800000000000001"))]
1946 pub final_message_id: Option<MessageId>,
1947
1948 #[serde(skip_serializing_if = "Option::is_none")]
1950 pub final_answer_preview: Option<String>,
1951
1952 #[serde(skip_serializing_if = "Option::is_none")]
1954 pub time_to_first_token_ms: Option<u64>,
1955
1956 #[serde(skip_serializing_if = "Option::is_none")]
1958 pub tool_call_count: Option<u32>,
1959
1960 #[serde(skip_serializing_if = "Option::is_none")]
1962 pub llm_call_count: Option<u32>,
1963
1964 #[serde(skip_serializing_if = "Option::is_none")]
1966 pub status: Option<String>,
1967}
1968
1969#[derive(Debug, Clone, Serialize, Deserialize)]
1971#[cfg_attr(feature = "openapi", derive(ToSchema))]
1972pub struct TurnFailedData {
1973 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1975 pub turn_id: TurnId,
1976
1977 pub error: String,
1979
1980 #[serde(default, skip_serializing_if = "Option::is_none")]
1982 pub error_code: Option<String>,
1983
1984 #[serde(default, skip_serializing_if = "Option::is_none")]
1986 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
1987 pub error_fields: Option<UserFacingErrorFields>,
1988
1989 #[serde(default, skip_serializing_if = "Option::is_none")]
1993 pub error_disclosure: Option<String>,
1994}
1995
1996#[derive(Debug, Clone, Serialize, Deserialize)]
2003#[cfg_attr(feature = "openapi", derive(ToSchema))]
2004pub struct TurnSealedData {
2005 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2007 pub turn_id: TurnId,
2008
2009 pub reason: String,
2012
2013 #[serde(default, skip_serializing_if = "Option::is_none")]
2015 pub detail: Option<String>,
2016
2017 #[serde(default, skip_serializing_if = "Option::is_none")]
2019 pub iterations: Option<u32>,
2020
2021 #[serde(default, skip_serializing_if = "Option::is_none")]
2023 pub usage: Option<TokenUsage>,
2024}
2025
2026#[derive(Debug, Clone, Serialize, Deserialize)]
2028#[cfg_attr(feature = "openapi", derive(ToSchema))]
2029pub struct TurnCancelledData {
2030 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2032 pub turn_id: TurnId,
2033
2034 #[serde(skip_serializing_if = "Option::is_none")]
2036 pub reason: Option<String>,
2037
2038 #[serde(skip_serializing_if = "Option::is_none")]
2040 pub usage: Option<TokenUsage>,
2041}
2042
2043#[derive(Debug, Clone, Serialize, Deserialize)]
2049#[cfg_attr(feature = "openapi", derive(ToSchema))]
2050pub struct SessionStartedData {
2051 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "harness_01933b5a00007000800000000000001"))]
2053 pub harness_id: HarnessId,
2054
2055 #[serde(skip_serializing_if = "Option::is_none")]
2057 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agent_01933b5a00007000800000000000001"))]
2058 pub agent_id: Option<AgentId>,
2059
2060 #[serde(skip_serializing_if = "Option::is_none")]
2062 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
2063 pub model_id: Option<ModelId>,
2064}
2065
2066#[derive(Debug, Clone, Serialize, Deserialize)]
2068#[cfg_attr(feature = "openapi", derive(ToSchema))]
2069pub struct SessionActivatedData {
2070 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2072 pub turn_id: TurnId,
2073
2074 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
2076 pub input_message_id: MessageId,
2077}
2078
2079#[derive(Debug, Clone, Serialize, Deserialize)]
2081#[cfg_attr(feature = "openapi", derive(ToSchema))]
2082pub struct SessionIdledData {
2083 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2085 pub turn_id: TurnId,
2086
2087 #[serde(skip_serializing_if = "Option::is_none")]
2089 pub iterations: Option<u32>,
2090
2091 #[serde(skip_serializing_if = "Option::is_none")]
2093 pub usage: Option<TokenUsage>,
2094}
2095
2096#[derive(Debug, Clone, Serialize, Deserialize)]
2105#[cfg_attr(feature = "openapi", derive(ToSchema))]
2106pub struct SessionTaskEventData {
2107 pub task: crate::session_task::SessionTask,
2108}
2109
2110#[derive(Debug, Clone, Serialize, Deserialize)]
2112#[cfg_attr(feature = "openapi", derive(ToSchema))]
2113pub struct TaskMessageEventData {
2114 pub task_id: String,
2115 pub message: crate::session_task::TaskMessage,
2116}
2117
2118#[derive(Debug, Clone, Serialize, Deserialize)]
2124#[cfg_attr(feature = "openapi", derive(ToSchema))]
2125#[serde(rename_all = "snake_case")]
2126pub enum CompactionReason {
2127 ProactiveBudget,
2129 RequestTooLarge,
2131 Manual,
2133}
2134
2135impl std::fmt::Display for CompactionReason {
2136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2137 match self {
2138 Self::ProactiveBudget => write!(f, "proactive_budget"),
2139 Self::RequestTooLarge => write!(f, "request_too_large"),
2140 Self::Manual => write!(f, "manual"),
2141 }
2142 }
2143}
2144
2145#[derive(Debug, Clone, Serialize, Deserialize)]
2147#[cfg_attr(feature = "openapi", derive(ToSchema))]
2148pub struct ContextCompactingData {
2149 pub reason: CompactionReason,
2151 pub strategy: String,
2153 pub messages_before: usize,
2155}
2156
2157#[derive(Debug, Clone, Serialize, Deserialize)]
2159#[cfg_attr(feature = "openapi", derive(ToSchema))]
2160pub struct CompactionStepData {
2161 pub strategy: String,
2163 pub messages_after: usize,
2165 pub duration_ms: u64,
2167}
2168
2169#[derive(Debug, Clone, Serialize, Deserialize)]
2171#[cfg_attr(feature = "openapi", derive(ToSchema))]
2172pub struct ContextCompactedData {
2173 pub strategy_used: String,
2175 pub messages_before: usize,
2177 pub messages_after: usize,
2179 pub duration_ms: u64,
2181 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2183 pub steps: Vec<CompactionStepData>,
2184}
2185
2186#[derive(Debug, Clone, Serialize, Deserialize)]
2192#[cfg_attr(feature = "openapi", derive(ToSchema))]
2193pub struct FileWrittenData {
2194 pub path: String,
2196 pub operation: String,
2198 pub size_bytes: i64,
2200 pub created: bool,
2202}
2203
2204pub const FILE_OP_CREATE: &str = "create";
2206pub const FILE_OP_UPDATE: &str = "update";
2207
2208#[derive(Debug, Clone, Serialize, Deserialize)]
2214#[cfg_attr(feature = "openapi", derive(ToSchema))]
2215pub struct BudgetEventData {
2216 pub budget_id: String,
2218 pub balance: f64,
2220 pub limit: f64,
2222 pub currency: String,
2224 #[serde(skip_serializing_if = "Option::is_none")]
2226 pub message: Option<String>,
2227 #[serde(skip_serializing_if = "Option::is_none")]
2229 pub soft_limit: Option<f64>,
2230}
2231
2232#[derive(Debug, Clone, Serialize, Deserialize)]
2238#[cfg_attr(feature = "openapi", derive(ToSchema))]
2239pub struct VoiceSessionStartedData {
2240 #[cfg_attr(
2242 feature = "openapi",
2243 schema(example = "voice_01933b5a00007000800000000000001")
2244 )]
2245 pub voice_connection_id: String,
2246 #[cfg_attr(feature = "openapi", schema(example = "gpt-realtime"))]
2248 pub model: String,
2249 #[cfg_attr(feature = "openapi", schema(example = "alloy"))]
2251 pub voice: String,
2252 #[cfg_attr(feature = "openapi", schema(example = "medium"))]
2254 pub reasoning_effort: String,
2255 #[cfg_attr(feature = "openapi", schema(example = "webrtc"))]
2257 pub transport: String,
2258}
2259
2260#[derive(Debug, Clone, Serialize, Deserialize)]
2262#[cfg_attr(feature = "openapi", derive(ToSchema))]
2263pub struct VoiceTranscriptData {
2264 pub voice_connection_id: String,
2266 #[serde(default, skip_serializing_if = "Option::is_none")]
2268 pub item_id: Option<String>,
2269 #[serde(default, skip_serializing_if = "Option::is_none")]
2271 pub response_id: Option<String>,
2272 #[serde(default, skip_serializing_if = "Option::is_none")]
2274 pub phase: Option<String>,
2275 #[serde(default, skip_serializing_if = "String::is_empty")]
2277 pub delta: String,
2278 pub accumulated: String,
2280}
2281
2282#[derive(Debug, Clone, Serialize, Deserialize)]
2284#[cfg_attr(feature = "openapi", derive(ToSchema))]
2285pub struct VoiceSessionEndedData {
2286 #[cfg_attr(
2288 feature = "openapi",
2289 schema(example = "voice_01933b5a00007000800000000000001")
2290 )]
2291 pub voice_connection_id: String,
2292 #[serde(default, skip_serializing_if = "Option::is_none")]
2294 #[cfg_attr(
2295 feature = "openapi",
2296 schema(example = "User hung up after refund confirmed.")
2297 )]
2298 pub reason: Option<String>,
2299 #[serde(default, skip_serializing_if = "Option::is_none")]
2302 #[cfg_attr(feature = "openapi", schema(example = 184_500_u64))]
2303 pub duration_ms: Option<u64>,
2304}
2305
2306#[derive(Debug, Clone, Serialize, Deserialize)]
2308#[cfg_attr(feature = "openapi", derive(ToSchema))]
2309pub struct VoiceSessionFailedData {
2310 #[cfg_attr(
2312 feature = "openapi",
2313 schema(example = "voice_01933b5a00007000800000000000001")
2314 )]
2315 pub voice_connection_id: String,
2316 #[cfg_attr(
2318 feature = "openapi",
2319 schema(example = "realtime provider closed stream: 1011 internal_error")
2320 )]
2321 pub error: String,
2322}
2323
2324#[derive(Debug, Clone, Serialize, Deserialize)]
2363#[serde(untagged)]
2364#[cfg_attr(feature = "openapi", derive(ToSchema))]
2365#[cfg_attr(feature = "openapi", schema(
2366 title = "EventData",
2367 description = "Event-specific payload. The schema depends on the event type field.",
2368 example = json!({"message": {"id": "...", "role": "user", "content": []}})
2369))]
2370pub enum EventData {
2371 InputMessage(InputMessageData),
2373
2374 OutputMessageDelta(OutputMessageDeltaData),
2380 OutputMessageStarted(OutputMessageStartedData),
2381 OutputMessageReplaced(OutputMessageReplacedData),
2385 OutputMessageCompleted(OutputMessageCompletedData),
2386
2387 TurnStarted(TurnStartedData),
2389 TurnCompleted(TurnCompletedData),
2390 TurnFailed(TurnFailedData),
2391
2392 ReasonStarted(ReasonStartedData),
2394 ReasonCompleted(ReasonCompletedData),
2395 ReasonRecovered(ReasonRecoveredData),
2396 CapabilityUsage(CapabilityUsageData),
2397 ActStarted(ActStartedData),
2398 ActCompleted(ActCompletedData),
2399 ToolStarted(ToolStartedData),
2400 ToolCompleted(ToolCompletedData),
2401 ToolProgress(ToolProgressData),
2402 ToolOutputDelta(ToolOutputDeltaData),
2403 ToolCallRequested(ToolCallRequestedData),
2404
2405 TranscriptRepaired(TranscriptRepairedData),
2407 ToolCallRepaired(ToolCallRepairedData),
2408
2409 LlmGeneration(LlmGenerationData),
2411
2412 ReasonThinkingDelta(ReasonThinkingDeltaData),
2424 ReasonItem(ReasonItemData),
2425 ReasonThinkingStarted(ReasonThinkingStartedData),
2426 ReasonThinkingCompleted(ReasonThinkingCompletedData),
2427
2428 TurnSealed(TurnSealedData),
2433
2434 TurnCancelled(TurnCancelledData),
2438
2439 SessionStarted(SessionStartedData),
2441 SessionActivated(SessionActivatedData),
2442 SessionIdled(SessionIdledData),
2443
2444 TaskCreated(SessionTaskEventData),
2446 TaskUpdated(SessionTaskEventData),
2447 TaskMessageSent(TaskMessageEventData),
2448 TaskMessageReceived(TaskMessageEventData),
2449
2450 ContextCompacting(ContextCompactingData),
2452 ContextCompacted(ContextCompactedData),
2453
2454 FileWritten(FileWrittenData),
2456
2457 BudgetWarning(BudgetEventData),
2459 BudgetPaused(BudgetEventData),
2460 BudgetExhausted(BudgetEventData),
2461 BudgetResumed(BudgetEventData),
2462
2463 VoiceSessionStarted(VoiceSessionStartedData),
2465 VoiceInputTranscriptDelta(VoiceTranscriptData),
2466 VoiceInputTranscriptCompleted(VoiceTranscriptData),
2467 VoiceOutputTranscriptDelta(VoiceTranscriptData),
2468 VoiceOutputTranscriptCompleted(VoiceTranscriptData),
2469 VoiceSessionEnded(VoiceSessionEndedData),
2470 VoiceSessionFailed(VoiceSessionFailedData),
2471
2472 #[serde(skip)]
2476 Unsupported {
2477 event_type: String,
2479 data: serde_json::Value,
2481 },
2482}
2483
2484impl EventData {
2485 pub fn event_type(&self) -> &'static str {
2488 match self {
2489 EventData::InputMessage(_) => INPUT_MESSAGE,
2490 EventData::OutputMessageStarted(_) => OUTPUT_MESSAGE_STARTED,
2491 EventData::OutputMessageDelta(_) => OUTPUT_MESSAGE_DELTA,
2492 EventData::OutputMessageReplaced(_) => OUTPUT_MESSAGE_REPLACED,
2493 EventData::OutputMessageCompleted(_) => OUTPUT_MESSAGE_COMPLETED,
2494 EventData::TurnStarted(_) => TURN_STARTED,
2495 EventData::TurnCompleted(_) => TURN_COMPLETED,
2496 EventData::TurnFailed(_) => TURN_FAILED,
2497 EventData::TurnSealed(_) => TURN_SEALED,
2498 EventData::TurnCancelled(_) => TURN_CANCELLED,
2499 EventData::ReasonStarted(_) => REASON_STARTED,
2500 EventData::ReasonCompleted(_) => REASON_COMPLETED,
2501 EventData::ReasonRecovered(_) => REASON_RECOVERED,
2502 EventData::CapabilityUsage(_) => CAPABILITY_USAGE,
2503 EventData::ActStarted(_) => ACT_STARTED,
2504 EventData::ActCompleted(_) => ACT_COMPLETED,
2505 EventData::ToolStarted(_) => TOOL_STARTED,
2506 EventData::ToolCompleted(_) => TOOL_COMPLETED,
2507 EventData::ToolProgress(_) => TOOL_PROGRESS,
2508 EventData::ToolOutputDelta(_) => TOOL_OUTPUT_DELTA,
2509 EventData::ToolCallRequested(_) => TOOL_CALL_REQUESTED,
2510 EventData::TranscriptRepaired(_) => TRANSCRIPT_REPAIRED,
2511 EventData::ToolCallRepaired(_) => TOOL_CALL_REPAIRED,
2512 EventData::LlmGeneration(_) => LLM_GENERATION,
2513 EventData::ReasonThinkingDelta(_) => REASON_THINKING_DELTA,
2514 EventData::ReasonThinkingStarted(_) => REASON_THINKING_STARTED,
2515 EventData::ReasonThinkingCompleted(_) => REASON_THINKING_COMPLETED,
2516 EventData::ReasonItem(_) => REASON_ITEM,
2517 EventData::SessionStarted(_) => SESSION_STARTED,
2518 EventData::SessionActivated(_) => SESSION_ACTIVATED,
2519 EventData::SessionIdled(_) => SESSION_IDLED,
2520 EventData::TaskCreated(_) => TASK_CREATED,
2521 EventData::TaskUpdated(_) => TASK_UPDATED,
2522 EventData::TaskMessageSent(_) => TASK_MESSAGE_SENT,
2523 EventData::TaskMessageReceived(_) => TASK_MESSAGE_RECEIVED,
2524 EventData::ContextCompacting(_) => CONTEXT_COMPACTING,
2525 EventData::ContextCompacted(_) => CONTEXT_COMPACTED,
2526 EventData::FileWritten(_) => FILE_WRITTEN,
2527 EventData::BudgetWarning(_) => BUDGET_WARNING,
2528 EventData::BudgetPaused(_) => BUDGET_PAUSED,
2529 EventData::BudgetExhausted(_) => BUDGET_EXHAUSTED,
2530 EventData::BudgetResumed(_) => BUDGET_RESUMED,
2531 EventData::VoiceSessionStarted(_) => VOICE_SESSION_STARTED,
2532 EventData::VoiceInputTranscriptDelta(_) => VOICE_INPUT_TRANSCRIPT_DELTA,
2533 EventData::VoiceInputTranscriptCompleted(_) => VOICE_INPUT_TRANSCRIPT_COMPLETED,
2534 EventData::VoiceOutputTranscriptDelta(_) => VOICE_OUTPUT_TRANSCRIPT_DELTA,
2535 EventData::VoiceOutputTranscriptCompleted(_) => VOICE_OUTPUT_TRANSCRIPT_COMPLETED,
2536 EventData::VoiceSessionEnded(_) => VOICE_SESSION_ENDED,
2537 EventData::VoiceSessionFailed(_) => VOICE_SESSION_FAILED,
2538 EventData::Unsupported { .. } => "unsupported",
2539 }
2540 }
2541
2542 pub fn is_unsupported(&self) -> bool {
2545 matches!(self, EventData::Unsupported { .. })
2546 }
2547
2548 pub fn unsupported(event_type: String, data: serde_json::Value) -> Self {
2551 tracing::warn!(
2552 event_type = %event_type,
2553 "Encountered unsupported event type - will be filtered from API responses"
2554 );
2555 EventData::Unsupported { event_type, data }
2556 }
2557}
2558
2559pub fn deserialize_event_data(event_type: &str, data: serde_json::Value) -> EventData {
2573 let result =
2574 match event_type {
2575 INPUT_MESSAGE => serde_json::from_value::<InputMessageData>(data.clone())
2576 .map(EventData::InputMessage),
2577 OUTPUT_MESSAGE_STARTED => {
2578 serde_json::from_value::<OutputMessageStartedData>(data.clone())
2579 .map(EventData::OutputMessageStarted)
2580 }
2581 OUTPUT_MESSAGE_DELTA => serde_json::from_value::<OutputMessageDeltaData>(data.clone())
2582 .map(EventData::OutputMessageDelta),
2583 OUTPUT_MESSAGE_REPLACED => {
2584 serde_json::from_value::<OutputMessageReplacedData>(data.clone())
2585 .map(EventData::OutputMessageReplaced)
2586 }
2587 OUTPUT_MESSAGE_COMPLETED => {
2588 serde_json::from_value::<OutputMessageCompletedData>(data.clone())
2589 .map(EventData::OutputMessageCompleted)
2590 }
2591 TURN_STARTED => {
2592 serde_json::from_value::<TurnStartedData>(data.clone()).map(EventData::TurnStarted)
2593 }
2594 TURN_COMPLETED => serde_json::from_value::<TurnCompletedData>(data.clone())
2595 .map(EventData::TurnCompleted),
2596 TURN_FAILED => {
2597 serde_json::from_value::<TurnFailedData>(data.clone()).map(EventData::TurnFailed)
2598 }
2599 TURN_SEALED => {
2600 serde_json::from_value::<TurnSealedData>(data.clone()).map(EventData::TurnSealed)
2601 }
2602 TURN_CANCELLED => serde_json::from_value::<TurnCancelledData>(data.clone())
2603 .map(EventData::TurnCancelled),
2604 REASON_STARTED => serde_json::from_value::<ReasonStartedData>(data.clone())
2605 .map(EventData::ReasonStarted),
2606 REASON_COMPLETED => serde_json::from_value::<ReasonCompletedData>(data.clone())
2607 .map(EventData::ReasonCompleted),
2608 REASON_RECOVERED => serde_json::from_value::<ReasonRecoveredData>(data.clone())
2609 .map(EventData::ReasonRecovered),
2610 CAPABILITY_USAGE => serde_json::from_value::<CapabilityUsageData>(data.clone())
2611 .map(EventData::CapabilityUsage),
2612 ACT_STARTED => {
2613 serde_json::from_value::<ActStartedData>(data.clone()).map(EventData::ActStarted)
2614 }
2615 ACT_COMPLETED => serde_json::from_value::<ActCompletedData>(data.clone())
2616 .map(EventData::ActCompleted),
2617 TOOL_STARTED => {
2618 serde_json::from_value::<ToolStartedData>(data.clone()).map(EventData::ToolStarted)
2619 }
2620 TOOL_COMPLETED => serde_json::from_value::<ToolCompletedData>(data.clone())
2621 .map(EventData::ToolCompleted),
2622 TOOL_PROGRESS => serde_json::from_value::<ToolProgressData>(data.clone())
2623 .map(EventData::ToolProgress),
2624 TOOL_OUTPUT_DELTA => serde_json::from_value::<ToolOutputDeltaData>(data.clone())
2625 .map(EventData::ToolOutputDelta),
2626 TOOL_CALL_REQUESTED => serde_json::from_value::<ToolCallRequestedData>(data.clone())
2627 .map(EventData::ToolCallRequested),
2628 TRANSCRIPT_REPAIRED => serde_json::from_value::<TranscriptRepairedData>(data.clone())
2629 .map(EventData::TranscriptRepaired),
2630 TOOL_CALL_REPAIRED => serde_json::from_value::<ToolCallRepairedData>(data.clone())
2631 .map(EventData::ToolCallRepaired),
2632 LLM_GENERATION => serde_json::from_value::<LlmGenerationData>(data.clone())
2633 .map(EventData::LlmGeneration),
2634 REASON_THINKING_STARTED => {
2635 serde_json::from_value::<ReasonThinkingStartedData>(data.clone())
2636 .map(EventData::ReasonThinkingStarted)
2637 }
2638 REASON_THINKING_DELTA => {
2639 serde_json::from_value::<ReasonThinkingDeltaData>(data.clone())
2640 .map(EventData::ReasonThinkingDelta)
2641 }
2642 REASON_THINKING_COMPLETED => {
2643 serde_json::from_value::<ReasonThinkingCompletedData>(data.clone())
2644 .map(EventData::ReasonThinkingCompleted)
2645 }
2646 REASON_ITEM => {
2647 serde_json::from_value::<ReasonItemData>(data.clone()).map(EventData::ReasonItem)
2648 }
2649 SESSION_STARTED => serde_json::from_value::<SessionStartedData>(data.clone())
2650 .map(EventData::SessionStarted),
2651 SESSION_ACTIVATED => serde_json::from_value::<SessionActivatedData>(data.clone())
2652 .map(EventData::SessionActivated),
2653 SESSION_IDLED => serde_json::from_value::<SessionIdledData>(data.clone())
2654 .map(EventData::SessionIdled),
2655 CONTEXT_COMPACTING => serde_json::from_value::<ContextCompactingData>(data.clone())
2656 .map(EventData::ContextCompacting),
2657 CONTEXT_COMPACTED => serde_json::from_value::<ContextCompactedData>(data.clone())
2658 .map(EventData::ContextCompacted),
2659 FILE_WRITTEN => {
2660 serde_json::from_value::<FileWrittenData>(data.clone()).map(EventData::FileWritten)
2661 }
2662 BUDGET_WARNING => serde_json::from_value::<BudgetEventData>(data.clone())
2663 .map(EventData::BudgetWarning),
2664 BUDGET_PAUSED => {
2665 serde_json::from_value::<BudgetEventData>(data.clone()).map(EventData::BudgetPaused)
2666 }
2667 BUDGET_EXHAUSTED => serde_json::from_value::<BudgetEventData>(data.clone())
2668 .map(EventData::BudgetExhausted),
2669 BUDGET_RESUMED => serde_json::from_value::<BudgetEventData>(data.clone())
2670 .map(EventData::BudgetResumed),
2671 VOICE_SESSION_STARTED => {
2672 serde_json::from_value::<VoiceSessionStartedData>(data.clone())
2673 .map(EventData::VoiceSessionStarted)
2674 }
2675 VOICE_INPUT_TRANSCRIPT_DELTA => {
2676 serde_json::from_value::<VoiceTranscriptData>(data.clone())
2677 .map(EventData::VoiceInputTranscriptDelta)
2678 }
2679 VOICE_INPUT_TRANSCRIPT_COMPLETED => {
2680 serde_json::from_value::<VoiceTranscriptData>(data.clone())
2681 .map(EventData::VoiceInputTranscriptCompleted)
2682 }
2683 VOICE_OUTPUT_TRANSCRIPT_DELTA => {
2684 serde_json::from_value::<VoiceTranscriptData>(data.clone())
2685 .map(EventData::VoiceOutputTranscriptDelta)
2686 }
2687 VOICE_OUTPUT_TRANSCRIPT_COMPLETED => {
2688 serde_json::from_value::<VoiceTranscriptData>(data.clone())
2689 .map(EventData::VoiceOutputTranscriptCompleted)
2690 }
2691 VOICE_SESSION_ENDED => serde_json::from_value::<VoiceSessionEndedData>(data.clone())
2692 .map(EventData::VoiceSessionEnded),
2693 VOICE_SESSION_FAILED => serde_json::from_value::<VoiceSessionFailedData>(data.clone())
2694 .map(EventData::VoiceSessionFailed),
2695 TASK_CREATED => serde_json::from_value::<SessionTaskEventData>(data.clone())
2696 .map(EventData::TaskCreated),
2697 TASK_UPDATED => serde_json::from_value::<SessionTaskEventData>(data.clone())
2698 .map(EventData::TaskUpdated),
2699 TASK_MESSAGE_SENT => serde_json::from_value::<TaskMessageEventData>(data.clone())
2700 .map(EventData::TaskMessageSent),
2701 TASK_MESSAGE_RECEIVED => serde_json::from_value::<TaskMessageEventData>(data.clone())
2702 .map(EventData::TaskMessageReceived),
2703 _ => {
2704 return EventData::unsupported(event_type.to_string(), data);
2706 }
2707 };
2708
2709 result.unwrap_or_else(|e| {
2711 tracing::warn!(
2712 event_type = %event_type,
2713 error = %e,
2714 "Failed to deserialize known event type - treating as unsupported"
2715 );
2716 EventData::Unsupported {
2717 event_type: event_type.to_string(),
2718 data,
2719 }
2720 })
2721}
2722
2723macro_rules! impl_from_event_data {
2727 ($($data_type:ty => $variant:ident),* $(,)?) => {
2728 $(
2729 impl From<$data_type> for EventData {
2730 fn from(data: $data_type) -> Self {
2731 EventData::$variant(data)
2732 }
2733 }
2734 )*
2735 };
2736}
2737
2738impl_from_event_data! {
2740 InputMessageData => InputMessage,
2741 OutputMessageStartedData => OutputMessageStarted,
2742 OutputMessageDeltaData => OutputMessageDelta,
2743 OutputMessageReplacedData => OutputMessageReplaced,
2744 OutputMessageCompletedData => OutputMessageCompleted,
2745 TurnStartedData => TurnStarted,
2746 TurnCompletedData => TurnCompleted,
2747 TurnFailedData => TurnFailed,
2748 TurnSealedData => TurnSealed,
2749 TurnCancelledData => TurnCancelled,
2750 ReasonStartedData => ReasonStarted,
2751 ReasonCompletedData => ReasonCompleted,
2752 ReasonRecoveredData => ReasonRecovered,
2753 CapabilityUsageData => CapabilityUsage,
2754 ActStartedData => ActStarted,
2755 ActCompletedData => ActCompleted,
2756 ToolStartedData => ToolStarted,
2757 ToolCompletedData => ToolCompleted,
2758 ToolProgressData => ToolProgress,
2759 ToolOutputDeltaData => ToolOutputDelta,
2760 ToolCallRequestedData => ToolCallRequested,
2761 TranscriptRepairedData => TranscriptRepaired,
2762 ToolCallRepairedData => ToolCallRepaired,
2763 LlmGenerationData => LlmGeneration,
2764 ReasonThinkingStartedData => ReasonThinkingStarted,
2765 ReasonThinkingDeltaData => ReasonThinkingDelta,
2766 ReasonThinkingCompletedData => ReasonThinkingCompleted,
2767 ReasonItemData => ReasonItem,
2768 SessionStartedData => SessionStarted,
2769 SessionActivatedData => SessionActivated,
2770 SessionIdledData => SessionIdled,
2771 ContextCompactingData => ContextCompacting,
2772 ContextCompactedData => ContextCompacted,
2773 FileWrittenData => FileWritten,
2774 VoiceSessionStartedData => VoiceSessionStarted,
2775 VoiceSessionEndedData => VoiceSessionEnded,
2776 VoiceSessionFailedData => VoiceSessionFailed,
2777}
2778
2779impl EventData {
2780 pub fn voice_transcript_event(data: VoiceTranscriptData, event_type: &str) -> Self {
2781 match event_type {
2782 VOICE_INPUT_TRANSCRIPT_DELTA => EventData::VoiceInputTranscriptDelta(data),
2783 VOICE_INPUT_TRANSCRIPT_COMPLETED => EventData::VoiceInputTranscriptCompleted(data),
2784 VOICE_OUTPUT_TRANSCRIPT_DELTA => EventData::VoiceOutputTranscriptDelta(data),
2785 VOICE_OUTPUT_TRANSCRIPT_COMPLETED => EventData::VoiceOutputTranscriptCompleted(data),
2786 _ => panic!("Unknown voice transcript event type: {event_type}"),
2787 }
2788 }
2789}
2790
2791impl EventData {
2794 pub fn budget_event(data: BudgetEventData, event_type: &str) -> Self {
2795 match event_type {
2796 BUDGET_WARNING => EventData::BudgetWarning(data),
2797 BUDGET_PAUSED => EventData::BudgetPaused(data),
2798 BUDGET_EXHAUSTED => EventData::BudgetExhausted(data),
2799 BUDGET_RESUMED => EventData::BudgetResumed(data),
2800 _ => panic!("Unknown budget event type: {event_type}"),
2801 }
2802 }
2803}
2804
2805#[derive(Debug, Clone, Serialize)]
2815#[cfg_attr(feature = "openapi", derive(ToSchema))]
2816pub struct EventRequest {
2817 #[serde(rename = "type")]
2819 pub event_type: String,
2820
2821 pub ts: DateTime<Utc>,
2823
2824 pub session_id: SessionId,
2826
2827 pub context: EventContext,
2829
2830 pub data: EventData,
2832
2833 #[serde(skip_serializing_if = "Option::is_none")]
2835 pub metadata: Option<serde_json::Value>,
2836
2837 #[serde(skip_serializing_if = "Option::is_none")]
2839 pub tags: Option<Vec<String>>,
2840}
2841
2842#[derive(Debug, Deserialize)]
2843struct RawEventRequest {
2844 #[serde(rename = "type")]
2845 event_type: String,
2846 ts: DateTime<Utc>,
2847 session_id: SessionId,
2848 context: EventContext,
2849 data: serde_json::Value,
2850 metadata: Option<serde_json::Value>,
2851 tags: Option<Vec<String>>,
2852}
2853
2854impl<'de> Deserialize<'de> for EventRequest {
2855 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2856 where
2857 D: Deserializer<'de>,
2858 {
2859 let raw = RawEventRequest::deserialize(deserializer)?;
2860 let data = deserialize_event_data(&raw.event_type, raw.data);
2861 Ok(Self {
2862 event_type: raw.event_type,
2863 ts: raw.ts,
2864 session_id: raw.session_id,
2865 context: raw.context,
2866 data,
2867 metadata: raw.metadata,
2868 tags: raw.tags,
2869 })
2870 }
2871}
2872
2873impl EventRequest {
2874 pub fn new(session_id: SessionId, context: EventContext, data: impl Into<EventData>) -> Self {
2878 let data = data.into();
2879 let event_type = data.event_type().to_string();
2880 Self {
2881 event_type,
2882 ts: Utc::now(),
2883 session_id,
2884 context,
2885 data,
2886 metadata: None,
2887 tags: None,
2888 }
2889 }
2890
2891 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
2893 self.metadata = Some(metadata);
2894 self
2895 }
2896
2897 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
2899 self.tags = Some(tags);
2900 self
2901 }
2902
2903 pub fn is_ephemeral(&self) -> bool {
2911 is_ephemeral_event_type(&self.event_type)
2912 }
2913
2914 pub fn into_event(self, id: EventId, sequence: i32) -> Event {
2916 Event {
2917 id,
2918 event_type: self.event_type,
2919 ts: self.ts,
2920 session_id: self.session_id,
2921 context: self.context,
2922 data: self.data,
2923 metadata: self.metadata,
2924 tags: self.tags,
2925 sequence: Some(sequence),
2926 }
2927 }
2928}
2929
2930pub struct EventBuilder {
2936 session_id: SessionId,
2937 context: EventContext,
2938}
2939
2940impl EventBuilder {
2941 pub fn new(session_id: SessionId) -> Self {
2942 Self {
2943 session_id,
2944 context: EventContext::empty(),
2945 }
2946 }
2947
2948 pub fn with_turn(mut self, turn_id: TurnId, input_message_id: MessageId) -> Self {
2949 self.context.turn_id = Some(turn_id);
2950 self.context.input_message_id = Some(input_message_id);
2951 self
2952 }
2953
2954 pub fn with_exec(mut self, exec_id: ExecId) -> Self {
2955 self.context.exec_id = Some(exec_id);
2956 self
2957 }
2958
2959 pub fn build(self, data: impl Into<EventData>) -> Event {
2960 Event::new(self.session_id, self.context, data)
2961 }
2962}
2963
2964#[cfg(test)]
2969mod tests {
2970 use super::*;
2971 use crate::driver_registry::PromptCacheStrategy;
2972 use serde_json::json;
2973 use std::collections::HashMap;
2974
2975 #[test]
2976 fn test_event_creation() {
2977 let session_id = SessionId::new();
2978 let context = EventContext::empty();
2979 let data = InputMessageData::new(Message::user("test"));
2980
2981 let event = Event::new(session_id, context, data);
2982
2983 assert_eq!(event.event_type, "input.message");
2984 assert_eq!(event.session_uuid(), session_id.uuid());
2985 assert!(event.is_input_event());
2986 assert!(event.is_message_event());
2987 }
2988
2989 #[test]
2990 fn test_event_context_from_atom_context() {
2991 let session_id = SessionId::new();
2992 let turn_id = TurnId::new();
2993 let input_message_id = MessageId::new();
2994
2995 let atom_ctx = AtomContext::new(session_id, turn_id, input_message_id);
2996 let context = EventContext::from_atom_context(&atom_ctx);
2997
2998 assert_eq!(context.turn_id, Some(turn_id));
2999 assert_eq!(context.input_message_id, Some(input_message_id));
3000 assert_eq!(context.exec_id, Some(atom_ctx.exec_id));
3001 }
3002
3003 #[test]
3004 fn test_event_serialization() {
3005 let session_id = SessionId::new();
3006 let context = EventContext::empty();
3007 let event = Event::new(
3008 session_id,
3009 context,
3010 InputMessageData::new(Message::user("test")),
3011 );
3012
3013 let json = serde_json::to_string(&event).unwrap();
3014
3015 assert!(json.contains("\"type\":\"input.message\""));
3016 assert!(json.contains("\"session_id\""));
3017 assert!(json.contains("\"context\""));
3018 assert!(json.contains("\"data\""));
3019 }
3020
3021 #[test]
3022 fn transcript_repaired_is_valid_filter_event_type() {
3023 assert!(VALID_EVENT_TYPES.contains(&TRANSCRIPT_REPAIRED));
3024 }
3025
3026 #[test]
3030 fn capability_usage_is_valid_filter_event_type() {
3031 assert!(VALID_EVENT_TYPES.contains(&CAPABILITY_USAGE));
3032 }
3033
3034 #[test]
3035 fn test_event_builder() {
3036 let session_id = SessionId::new();
3037 let turn_id = TurnId::new();
3038 let input_message_id = MessageId::new();
3039 let exec_id = ExecId::new();
3040
3041 let event = EventBuilder::new(session_id)
3042 .with_turn(turn_id, input_message_id)
3043 .with_exec(exec_id)
3044 .build(ReasonStartedData {
3045 harness_id: HarnessId::from_seed(1),
3046 agent_id: Some(AgentId::new()),
3047 metadata: Some(ModelMetadata {
3048 model: "gpt-4o".to_string(),
3049 model_id: None,
3050 provider_id: None,
3051 }),
3052 });
3053
3054 assert_eq!(event.event_type, "reason.started");
3055 assert_eq!(event.session_id, session_id);
3056 assert_eq!(event.context.turn_id, Some(turn_id));
3057 assert_eq!(event.context.exec_id, Some(exec_id));
3058 }
3059
3060 #[test]
3061 fn test_reason_completed_data() {
3062 let data = ReasonCompletedData::success("Hello world", true, 2, Some(1000), None);
3063 assert!(data.success);
3064 assert_eq!(data.text_preview, Some("Hello world".to_string()));
3065 assert!(data.has_tool_calls);
3066 assert_eq!(data.tool_call_count, 2);
3067 assert_eq!(data.duration_ms, Some(1000));
3068 assert!(data.usage.is_none());
3069
3070 let data = ReasonCompletedData::failure("Network error".to_string(), Some(500));
3071 assert!(!data.success);
3072 assert_eq!(data.error, Some("Network error".to_string()));
3073 assert_eq!(data.duration_ms, Some(500));
3074 }
3075
3076 #[test]
3077 fn test_input_output_event_types() {
3078 assert_eq!(INPUT_MESSAGE, "input.message");
3079 assert_eq!(OUTPUT_MESSAGE_STARTED, "output.message.started");
3080 assert_eq!(OUTPUT_MESSAGE_DELTA, "output.message.delta");
3081 assert_eq!(OUTPUT_MESSAGE_COMPLETED, "output.message.completed");
3082 }
3083
3084 #[test]
3085 fn test_turn_event_types() {
3086 assert_eq!(TURN_STARTED, "turn.started");
3087 assert_eq!(TURN_COMPLETED, "turn.completed");
3088 assert_eq!(TURN_FAILED, "turn.failed");
3089 assert_eq!(TURN_CANCELLED, "turn.cancelled");
3090 }
3091
3092 #[test]
3093 fn test_turn_cancelled_data() {
3094 let data = TurnCancelledData {
3095 turn_id: TurnId::from_uuid(Uuid::now_v7()),
3096 reason: Some("User requested cancellation".to_string()),
3097 usage: Some(TokenUsage::new(100, 50)),
3098 };
3099
3100 let event_data: EventData = data.into();
3101 assert_eq!(event_data.event_type(), TURN_CANCELLED);
3102 }
3103
3104 #[test]
3105 fn test_tool_event_types() {
3106 assert_eq!(TOOL_STARTED, "tool.started");
3107 assert_eq!(TOOL_COMPLETED, "tool.completed");
3108 }
3109
3110 #[test]
3111 fn test_llm_generation_event_type() {
3112 assert_eq!(LLM_GENERATION, "llm.generation");
3113 }
3114
3115 #[test]
3116 fn test_llm_generation_data_success() {
3117 let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")];
3118 let tools = vec![ToolDefinitionSummary {
3119 name: "get_weather".to_string(),
3120 display_name: None,
3121 category: None,
3122 capability_id: None,
3123 capability_name: None,
3124 description: "Get weather for a city".to_string(),
3125 }];
3126 let tool_calls = vec![];
3127 let data = LlmGenerationData::success(
3128 messages.clone(),
3129 tools,
3130 Some("Hi there!".to_string()),
3131 tool_calls,
3132 "gpt-4o".to_string(),
3133 Some("openai".to_string()),
3134 Some(TokenUsage {
3135 input_tokens: 10,
3136 output_tokens: 5,
3137 cache_read_tokens: None,
3138 cache_creation_tokens: None,
3139 actual_cost_usd: None,
3140 estimated_cost_usd: None,
3141 }),
3142 Some(100),
3143 Some(25), );
3145
3146 assert_eq!(data.messages.len(), 2);
3147 assert_eq!(data.tools.len(), 1);
3148 assert_eq!(data.tools[0].name, "get_weather");
3149 assert_eq!(data.output.text, Some("Hi there!".to_string()));
3150 assert!(data.output.tool_calls.is_empty());
3151 assert!(data.metadata.success);
3152 assert_eq!(data.metadata.model, "gpt-4o");
3153 assert_eq!(data.metadata.provider, Some("openai".to_string()));
3154 assert!(data.metadata.error.is_none());
3155 assert_eq!(data.metadata.finish_reasons, Some(vec!["stop".to_string()]));
3157 assert!(data.metadata.response_id.is_none());
3158 }
3159
3160 #[test]
3161 fn test_llm_generation_data_with_full_metadata() {
3162 let messages = vec![Message::user("Hello")];
3163 let data = LlmGenerationData::success_with_metadata(
3164 messages,
3165 vec![],
3166 Some("Hi!".to_string()),
3167 vec![],
3168 "claude-3-opus".to_string(),
3169 Some("anthropic".to_string()),
3170 Some(TokenUsage {
3171 input_tokens: 5,
3172 output_tokens: 3,
3173 cache_read_tokens: None,
3174 cache_creation_tokens: None,
3175 actual_cost_usd: None,
3176 estimated_cost_usd: None,
3177 }),
3178 Some(50),
3179 Some(25), Some(vec!["end_turn".to_string()]),
3181 Some("msg_12345".to_string()),
3182 );
3183
3184 assert!(data.metadata.success);
3185 assert_eq!(data.metadata.model, "claude-3-opus");
3186 assert_eq!(data.metadata.provider, Some("anthropic".to_string()));
3187 assert_eq!(data.metadata.time_to_first_token_ms, Some(25));
3188 assert_eq!(
3189 data.metadata.finish_reasons,
3190 Some(vec!["end_turn".to_string()])
3191 );
3192 assert_eq!(data.metadata.response_id, Some("msg_12345".to_string()));
3193 }
3194
3195 #[test]
3196 fn test_llm_generation_data_failure() {
3197 let messages = vec![Message::user("Hello")];
3198 let data = LlmGenerationData::failure(
3199 messages,
3200 vec![],
3201 "gpt-4o".to_string(),
3202 Some("openai".to_string()),
3203 "Rate limit exceeded".to_string(),
3204 Some(50),
3205 None, );
3207
3208 assert!(!data.metadata.success);
3209 assert_eq!(data.metadata.error, Some("Rate limit exceeded".to_string()));
3210 assert!(data.output.text.is_none());
3211 assert!(data.output.tool_calls.is_empty());
3212 }
3213
3214 #[test]
3215 fn test_llm_generation_event_data() {
3216 let data = LlmGenerationData::success(
3217 vec![Message::user("test")],
3218 vec![],
3219 Some("response".to_string()),
3220 vec![],
3221 "model".to_string(),
3222 None,
3223 None,
3224 None,
3225 None, );
3227
3228 let event_data: EventData = data.into();
3229 assert_eq!(event_data.event_type(), LLM_GENERATION);
3230 }
3231
3232 #[test]
3233 fn test_llm_generation_is_durable_not_ephemeral() {
3234 let session_id = SessionId::new();
3235 let data = LlmGenerationData::success(
3236 vec![Message::user("test")],
3237 vec![],
3238 Some("response".to_string()),
3239 vec![],
3240 "model".to_string(),
3241 None,
3242 None,
3243 None,
3244 None,
3245 );
3246
3247 let request = EventRequest::new(session_id, EventContext::empty(), data);
3248 assert!(!request.is_ephemeral());
3249 }
3250
3251 #[test]
3252 fn test_delta_events_are_ephemeral() {
3253 let session_id = SessionId::new();
3254 let turn_id = TurnId::new();
3255
3256 let output_delta = EventRequest::new(
3257 session_id,
3258 EventContext::empty(),
3259 OutputMessageDeltaData {
3260 turn_id,
3261 delta: "hel".to_string(),
3262 accumulated: "hel".to_string(),
3263 },
3264 );
3265 assert!(output_delta.is_ephemeral());
3266
3267 let thinking_delta = EventRequest::new(
3268 session_id,
3269 EventContext::empty(),
3270 ReasonThinkingDeltaData {
3271 turn_id,
3272 delta: "step".to_string(),
3273 accumulated: "step".to_string(),
3274 },
3275 );
3276 assert!(thinking_delta.is_ephemeral());
3277
3278 let tool_delta = EventRequest::new(
3279 session_id,
3280 EventContext::empty(),
3281 ToolOutputDeltaData {
3282 tool_call_id: "call_123".to_string(),
3283 tool_name: "bash".to_string(),
3284 delta: "line".to_string(),
3285 stream: "stdout".to_string(),
3286 },
3287 );
3288 assert!(tool_delta.is_ephemeral());
3289 }
3290
3291 #[test]
3292 fn test_llm_generation_data_with_request_options() {
3293 let mut provider_options = HashMap::new();
3294 provider_options.insert(
3295 "openai".to_string(),
3296 json!({ "previous_response_id": true }),
3297 );
3298
3299 let data = LlmGenerationData::success(
3300 vec![Message::user("Hello")],
3301 vec![],
3302 Some("Hi".to_string()),
3303 vec![],
3304 "gpt-5.4".to_string(),
3305 Some("openai".to_string()),
3306 None,
3307 Some(42),
3308 Some(12),
3309 )
3310 .with_request_options(LlmRequestOptions {
3311 prompt_cache: Some(LlmPromptCacheInfo {
3312 enabled: true,
3313 strategy: PromptCacheStrategy::Auto,
3314 provider_mode: Some("prompt_cache_key".to_string()),
3315 }),
3316 tool_search: Some(LlmToolSearchInfo {
3317 enabled: true,
3318 threshold: 8,
3319 }),
3320 provider_options,
3321 metadata: Default::default(),
3322 });
3323
3324 let json = serde_json::to_value(&data).unwrap();
3325 assert_eq!(
3326 json["metadata"]["request_options"]["prompt_cache"]["provider_mode"],
3327 "prompt_cache_key"
3328 );
3329 assert_eq!(
3330 json["metadata"]["request_options"]["tool_search"]["threshold"],
3331 8
3332 );
3333 assert_eq!(
3334 json["metadata"]["request_options"]["provider_options"]["openai"]["previous_response_id"],
3335 true
3336 );
3337 }
3338
3339 #[test]
3340 fn test_extended_thinking_event_types() {
3341 assert_eq!(REASON_THINKING_STARTED, "reason.thinking.started");
3342 assert_eq!(REASON_THINKING_DELTA, "reason.thinking.delta");
3343 assert_eq!(REASON_THINKING_COMPLETED, "reason.thinking.completed");
3344 }
3345
3346 #[test]
3347 fn test_output_message_started_data() {
3348 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3349 let data = OutputMessageStartedData {
3350 turn_id,
3351 model: Some("claude-4-opus".to_string()),
3352 iteration: None,
3353 };
3354
3355 let event_data: EventData = data.into();
3356 assert_eq!(event_data.event_type(), OUTPUT_MESSAGE_STARTED);
3357
3358 let json = serde_json::to_string(&event_data).unwrap();
3360 assert!(json.contains("turn_id"));
3361 assert!(json.contains("claude-4-opus"));
3362 }
3363
3364 #[test]
3365 fn test_output_message_started_data_without_model() {
3366 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3367 let data = OutputMessageStartedData {
3368 turn_id,
3369 model: None,
3370 iteration: None,
3371 };
3372
3373 let json = serde_json::to_string(&data).unwrap();
3375 assert!(!json.contains("model"));
3376 }
3377
3378 #[test]
3379 fn test_reason_thinking_started_data() {
3380 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3381 let data = ReasonThinkingStartedData {
3382 turn_id,
3383 model: Some("claude-4-opus".to_string()),
3384 };
3385
3386 let event_data: EventData = data.into();
3387 assert_eq!(event_data.event_type(), REASON_THINKING_STARTED);
3388
3389 let json = serde_json::to_string(&event_data).unwrap();
3391 assert!(json.contains("turn_id"));
3392 assert!(json.contains("claude-4-opus"));
3393 }
3394
3395 #[test]
3396 fn test_reason_thinking_delta_data() {
3397 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3398 let data = ReasonThinkingDeltaData {
3399 turn_id,
3400 delta: "thinking step 1".to_string(),
3401 accumulated: "thinking step 1".to_string(),
3402 };
3403
3404 let event_data: EventData = data.into();
3405 assert_eq!(event_data.event_type(), REASON_THINKING_DELTA);
3406
3407 let json = serde_json::to_string(&event_data).unwrap();
3409 assert!(json.contains("turn_id"));
3410 assert!(json.contains("delta"));
3411 assert!(json.contains("accumulated"));
3412 }
3413
3414 #[test]
3415 fn test_reason_thinking_completed_data() {
3416 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3417 let data = ReasonThinkingCompletedData {
3418 turn_id,
3419 thinking: "Full thinking content here".to_string(),
3420 };
3421
3422 let event_data: EventData = data.into();
3423 assert_eq!(event_data.event_type(), REASON_THINKING_COMPLETED);
3424
3425 let json = serde_json::to_string(&event_data).unwrap();
3427 assert!(json.contains("turn_id"));
3428 assert!(json.contains("thinking"));
3429 }
3430
3431 #[test]
3432 fn test_output_message_delta_data() {
3433 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3434 let data = OutputMessageDeltaData {
3435 turn_id,
3436 delta: "Hello".to_string(),
3437 accumulated: "Hello".to_string(),
3438 };
3439
3440 let event_data: EventData = data.into();
3441 assert_eq!(event_data.event_type(), OUTPUT_MESSAGE_DELTA);
3442
3443 let json = serde_json::to_string(&event_data).unwrap();
3445 assert!(json.contains("turn_id"));
3446 assert!(json.contains("delta"));
3447 assert!(json.contains("accumulated"));
3448 }
3449
3450 #[test]
3451 fn test_output_message_delta_deserialization_preserves_fields() {
3452 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3455 let data = OutputMessageDeltaData {
3456 turn_id,
3457 delta: "Hello world".to_string(),
3458 accumulated: "Hello world".to_string(),
3459 };
3460
3461 let json = serde_json::to_value(EventData::OutputMessageDelta(data.clone())).unwrap();
3463
3464 let deserialized: EventData = serde_json::from_value(json).unwrap();
3466
3467 match deserialized {
3469 EventData::OutputMessageDelta(td) => {
3470 assert_eq!(td.turn_id, turn_id);
3471 assert_eq!(td.delta, "Hello world");
3472 assert_eq!(td.accumulated, "Hello world");
3473 }
3474 _ => panic!("Expected OutputMessageDelta, got different variant"),
3475 }
3476 }
3477
3478 #[test]
3479 fn test_output_message_started_deserialization() {
3480 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3481 let data = OutputMessageStartedData {
3482 turn_id,
3483 model: Some("claude-3".to_string()),
3484 iteration: None,
3485 };
3486
3487 let json = serde_json::to_value(EventData::OutputMessageStarted(data.clone())).unwrap();
3489
3490 let deserialized: EventData = serde_json::from_value(json).unwrap();
3492
3493 match deserialized {
3495 EventData::OutputMessageStarted(at) => {
3496 assert_eq!(at.turn_id, turn_id);
3497 assert_eq!(at.model, Some("claude-3".to_string()));
3498 }
3499 _ => panic!("Expected OutputMessageStarted, got different variant"),
3500 }
3501 }
3502
3503 #[test]
3504 fn test_reason_thinking_started_deserialization() {
3505 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3509 let data = ReasonThinkingStartedData {
3510 turn_id,
3511 model: Some("claude-3".to_string()),
3512 };
3513
3514 let json = serde_json::to_value(&data).unwrap();
3516
3517 let deserialized = deserialize_event_data(REASON_THINKING_STARTED, json);
3519
3520 match deserialized {
3522 EventData::ReasonThinkingStarted(at) => {
3523 assert_eq!(at.turn_id, turn_id);
3524 assert_eq!(at.model, Some("claude-3".to_string()));
3525 }
3526 other => panic!("Expected ReasonThinkingStarted, got {}", other.event_type()),
3527 }
3528 }
3529
3530 #[test]
3531 fn test_llm_generation_with_ttft() {
3532 let messages = vec![Message::user("Hello")];
3533 let data = LlmGenerationData::success_with_metadata(
3534 messages,
3535 vec![],
3536 Some("Hi!".to_string()),
3537 vec![],
3538 "gpt-4o".to_string(),
3539 Some("openai".to_string()),
3540 Some(TokenUsage {
3541 input_tokens: 10,
3542 output_tokens: 5,
3543 cache_read_tokens: None,
3544 cache_creation_tokens: None,
3545 actual_cost_usd: None,
3546 estimated_cost_usd: None,
3547 }),
3548 Some(500), Some(120), Some(vec!["stop".to_string()]),
3551 None,
3552 );
3553
3554 assert!(data.metadata.success);
3555 assert_eq!(data.metadata.duration_ms, Some(500));
3556 assert_eq!(data.metadata.time_to_first_token_ms, Some(120));
3557 }
3558
3559 #[test]
3560 fn test_llm_generation_ttft_serialization() {
3561 let messages = vec![Message::user("test")];
3562 let data = LlmGenerationData::success_with_metadata(
3563 messages,
3564 vec![],
3565 Some("response".to_string()),
3566 vec![],
3567 "model".to_string(),
3568 None,
3569 None,
3570 Some(1000),
3571 Some(150), None,
3573 None,
3574 );
3575
3576 let json = serde_json::to_string(&data).unwrap();
3577 assert!(json.contains("time_to_first_token_ms"));
3578 assert!(json.contains("150"));
3579 }
3580
3581 #[test]
3582 fn test_reason_item_data_event_type_and_serialization() {
3583 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3584 let data = ReasonItemData {
3585 turn_id,
3586 provider: "openai".to_string(),
3587 model: Some("gpt-5.5".to_string()),
3588 item_id: "rs_abc".to_string(),
3589 encrypted_content: Some("OPAQUE_BLOB".to_string()),
3590 summary: vec!["safe summary".to_string()],
3591 token_count: Some(123),
3592 };
3593
3594 let event_data: EventData = data.into();
3595 assert_eq!(event_data.event_type(), REASON_ITEM);
3596
3597 let json = serde_json::to_string(&event_data).unwrap();
3598 assert!(json.contains("turn_id"));
3599 assert!(json.contains("openai"));
3600 assert!(json.contains("rs_abc"));
3601 assert!(json.contains("OPAQUE_BLOB"));
3602 assert!(json.contains("safe summary"));
3603 }
3604
3605 #[test]
3606 fn test_event_deserialize_reason_item_uses_event_type_dispatch() {
3607 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3608 let payload = serde_json::json!({
3609 "id": EventId::new().to_string(),
3610 "type": REASON_ITEM,
3611 "ts": Utc::now().to_rfc3339(),
3612 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
3613 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
3614 "data": {
3615 "turn_id": turn_id.to_string(),
3616 "provider": "openai",
3617 "model": "gpt-5",
3618 "item_id": "rs_event",
3619 "encrypted_content": "ENC",
3620 "summary": ["safe"],
3621 "token_count": 9
3622 }
3623 });
3624
3625 let event: Event = serde_json::from_value(payload).expect("event deserializes");
3626 match event.data {
3627 EventData::ReasonItem(data) => {
3628 assert_eq!(data.turn_id, turn_id);
3629 assert_eq!(data.provider, "openai");
3630 assert_eq!(data.item_id, "rs_event");
3631 assert_eq!(data.token_count, Some(9));
3632 }
3633 other => panic!("expected reason.item data, got {}", other.event_type()),
3634 }
3635 }
3636
3637 #[test]
3638 fn test_event_request_deserialize_reason_item_uses_event_type_dispatch() {
3639 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3640 let payload = serde_json::json!({
3641 "type": REASON_ITEM,
3642 "ts": Utc::now().to_rfc3339(),
3643 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
3644 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
3645 "data": {
3646 "turn_id": turn_id.to_string(),
3647 "provider": "openai",
3648 "item_id": "rs_request",
3649 "encrypted_content": "ENC",
3650 "summary": ["safe"]
3651 }
3652 });
3653
3654 let req: EventRequest = serde_json::from_value(payload).expect("request deserializes");
3655 match req.data {
3656 EventData::ReasonItem(data) => {
3657 assert_eq!(data.turn_id, turn_id);
3658 assert_eq!(data.provider, "openai");
3659 assert_eq!(data.item_id, "rs_request");
3660 }
3661 other => panic!("expected reason.item data, got {}", other.event_type()),
3662 }
3663 }
3664
3665 #[test]
3666 fn test_reason_item_data_round_trip_uses_typed_dispatch() {
3667 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3671 let data = ReasonItemData {
3672 turn_id,
3673 provider: "openai".to_string(),
3674 model: Some("gpt-5".to_string()),
3675 item_id: "rs_xyz".to_string(),
3676 encrypted_content: Some("ENC".to_string()),
3677 summary: vec![],
3678 token_count: None,
3679 };
3680
3681 let json = serde_json::to_value(&data).unwrap();
3682 let deserialized = deserialize_event_data(REASON_ITEM, json);
3683
3684 match deserialized {
3685 EventData::ReasonItem(out) => {
3686 assert_eq!(out.turn_id, turn_id);
3687 assert_eq!(out.provider, "openai");
3688 assert_eq!(out.item_id, "rs_xyz");
3689 assert_eq!(out.encrypted_content.as_deref(), Some("ENC"));
3690 }
3691 other => panic!("Expected ReasonItem, got {}", other.event_type()),
3692 }
3693 }
3694
3695 #[test]
3704 fn test_reason_item_variant_precedes_reason_thinking_started() {
3705 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3712 let json = serde_json::json!({
3713 "turn_id": turn_id.to_string(),
3714 "provider": "openai",
3715 "model": "gpt-5",
3716 "item_id": "rs_keep",
3717 "encrypted_content": "ENC",
3718 "summary": ["s"],
3719 "token_count": 7,
3720 });
3721
3722 let as_thinking: ReasonThinkingStartedData =
3728 serde_json::from_value(json.clone()).expect("thinking ignores extra fields");
3729 assert_eq!(as_thinking.turn_id, turn_id);
3730 assert_eq!(as_thinking.model.as_deref(), Some("gpt-5"));
3731
3732 let as_item: ReasonItemData =
3733 serde_json::from_value(json.clone()).expect("ReasonItem accepts payload");
3734 assert_eq!(as_item.item_id, "rs_keep");
3735 assert_eq!(as_item.provider, "openai");
3736
3737 let event_data = deserialize_event_data(REASON_ITEM, json);
3740 match event_data {
3741 EventData::ReasonItem(out) => {
3742 assert_eq!(out.item_id, "rs_keep");
3743 assert_eq!(out.provider, "openai");
3744 }
3745 other => panic!(
3746 "Typed dispatcher must select ReasonItem for {REASON_ITEM}, got {}",
3747 other.event_type()
3748 ),
3749 }
3750 }
3751
3752 #[test]
3759 fn test_reason_item_data_excludes_plaintext_reasoning() {
3760 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3761 let data = ReasonItemData {
3762 turn_id,
3763 provider: "openai".to_string(),
3764 model: Some("gpt-5".to_string()),
3765 item_id: "rs_secret".to_string(),
3766 encrypted_content: Some("opaque_blob_thinking_content_reasoning_text".to_string()),
3770 summary: vec!["safe summary mentioning content and thinking".to_string()],
3771 token_count: Some(1),
3772 };
3773
3774 let value = serde_json::to_value(&data).expect("serializable");
3775 let object = value.as_object().expect("data serializes to JSON object");
3776 for forbidden in [
3777 "content",
3778 "reasoning_text",
3779 "thinking",
3780 "reasoning_content",
3781 "raw_reasoning",
3782 ] {
3783 assert!(
3784 !object.contains_key(forbidden),
3785 "ReasonItemData JSON must not expose `{forbidden}` key, got: {object:?}",
3786 );
3787 }
3788 assert!(object.contains_key("encrypted_content"));
3790 assert!(object.contains_key("summary"));
3791 }
3792
3793 #[test]
3794 fn test_llm_generation_ttft_omitted_when_none() {
3795 let messages = vec![Message::user("test")];
3796 let data = LlmGenerationData::success(
3797 messages,
3798 vec![],
3799 Some("response".to_string()),
3800 vec![],
3801 "model".to_string(),
3802 None,
3803 None,
3804 None,
3805 None, );
3807
3808 assert!(data.metadata.time_to_first_token_ms.is_none());
3810
3811 let json = serde_json::to_string(&data).unwrap();
3813 assert!(!json.contains("time_to_first_token_ms"));
3814 }
3815}
3816
3817#[cfg(test)]
3826mod contract_tests {
3827 use super::*;
3828 use insta::{assert_json_snapshot, with_settings};
3829
3830 fn test_session_id() -> SessionId {
3832 SessionId::from_uuid(uuid::Uuid::from_u128(
3833 0x0000_0000_0000_0000_0000_0000_0000_0001,
3834 ))
3835 }
3836
3837 fn test_turn_id() -> TurnId {
3838 TurnId::from_uuid(uuid::Uuid::from_u128(
3839 0x0000_0000_0000_0000_0000_0000_0000_0002,
3840 ))
3841 }
3842
3843 fn test_message_id() -> MessageId {
3844 MessageId::from_uuid(uuid::Uuid::from_u128(
3845 0x0000_0000_0000_0000_0000_0000_0000_0003,
3846 ))
3847 }
3848
3849 fn test_agent_id() -> AgentId {
3850 AgentId::from_uuid(uuid::Uuid::from_u128(
3851 0x0000_0000_0000_0000_0000_0000_0000_0004,
3852 ))
3853 }
3854
3855 fn test_harness_id() -> HarnessId {
3856 HarnessId::from_uuid(uuid::Uuid::from_u128(
3857 0x0000_0000_0000_0000_0000_0000_0000_0005,
3858 ))
3859 }
3860
3861 #[test]
3868 fn snapshot_input_message() {
3869 let data = InputMessageData::new(Message::user("Hello, world!"));
3870 with_settings!({
3871 sort_maps => true,
3872 }, {
3873 assert_json_snapshot!("event_data_input_message", data, {
3875 ".message.id" => "[MESSAGE_ID]",
3876 ".message.created_at" => "[TIMESTAMP]"
3877 });
3878 });
3879 }
3880
3881 #[test]
3882 fn snapshot_output_message_started() {
3883 let data = OutputMessageStartedData {
3884 turn_id: test_turn_id(),
3885 model: Some("gpt-4o".to_string()),
3886 iteration: None,
3887 };
3888 with_settings!({
3889 sort_maps => true,
3890 }, {
3891 assert_json_snapshot!("event_data_output_message_started", data);
3892 });
3893 }
3894
3895 #[test]
3896 fn snapshot_output_message_delta() {
3897 let data = OutputMessageDeltaData {
3898 turn_id: test_turn_id(),
3899 delta: "Hello".to_string(),
3900 accumulated: "Hello".to_string(),
3901 };
3902 with_settings!({
3903 sort_maps => true,
3904 }, {
3905 assert_json_snapshot!("event_data_output_message_delta", data);
3906 });
3907 }
3908
3909 #[test]
3910 fn snapshot_output_message_completed() {
3911 let data = OutputMessageCompletedData::new(Message::assistant("Hello!"));
3912 with_settings!({
3913 sort_maps => true,
3914 }, {
3915 assert_json_snapshot!("event_data_output_message_completed", data, {
3917 ".message.id" => "[MESSAGE_ID]",
3918 ".message.created_at" => "[TIMESTAMP]"
3919 });
3920 });
3921 }
3922
3923 #[test]
3924 fn snapshot_turn_started() {
3925 let data = TurnStartedData {
3926 turn_id: test_turn_id(),
3927 input_message_id: test_message_id(),
3928 input_content: Some("Hello".to_string()),
3929 };
3930 with_settings!({
3931 sort_maps => true,
3932 }, {
3933 assert_json_snapshot!("event_data_turn_started", data);
3934 });
3935 }
3936
3937 #[test]
3938 fn snapshot_turn_completed() {
3939 let data = TurnCompletedData {
3940 turn_id: test_turn_id(),
3941 iterations: 3,
3942 duration_ms: Some(1500),
3943 usage: Some(TokenUsage::new(100, 50)),
3944 input_content: None,
3945 final_message_id: Some(test_message_id()),
3946 final_answer_preview: Some("Done.".to_string()),
3947 time_to_first_token_ms: Some(120),
3948 tool_call_count: Some(2),
3949 llm_call_count: Some(3),
3950 status: Some("completed".to_string()),
3951 };
3952 with_settings!({
3953 sort_maps => true,
3954 }, {
3955 assert_json_snapshot!("event_data_turn_completed", data);
3956 });
3957 }
3958
3959 #[test]
3960 fn snapshot_turn_failed() {
3961 let data = TurnFailedData {
3962 turn_id: test_turn_id(),
3963 error: "Rate limit exceeded".to_string(),
3964 error_code: Some("RATE_LIMIT".to_string()),
3965 error_fields: None,
3966 error_disclosure: None,
3967 };
3968 with_settings!({
3969 sort_maps => true,
3970 }, {
3971 assert_json_snapshot!("event_data_turn_failed", data);
3972 });
3973 }
3974
3975 #[test]
3976 fn snapshot_turn_cancelled() {
3977 let data = TurnCancelledData {
3978 turn_id: test_turn_id(),
3979 reason: Some("User requested".to_string()),
3980 usage: Some(TokenUsage::new(50, 25)),
3981 };
3982 with_settings!({
3983 sort_maps => true,
3984 }, {
3985 assert_json_snapshot!("event_data_turn_cancelled", data);
3986 });
3987 }
3988
3989 #[test]
3990 fn snapshot_reason_started() {
3991 let data = ReasonStartedData {
3992 harness_id: test_harness_id(),
3993 agent_id: Some(test_agent_id()),
3994 metadata: Some(ModelMetadata {
3995 model: "gpt-4o".to_string(),
3996 model_id: None,
3997 provider_id: None,
3998 }),
3999 };
4000 with_settings!({
4001 sort_maps => true,
4002 }, {
4003 assert_json_snapshot!("event_data_reason_started", data);
4004 });
4005 }
4006
4007 #[test]
4008 fn snapshot_reason_completed() {
4009 let data = ReasonCompletedData::success(
4010 "Hello world",
4011 true,
4012 2,
4013 Some(1000),
4014 Some(TokenUsage::new(100, 50)),
4015 );
4016 with_settings!({
4017 sort_maps => true,
4018 }, {
4019 assert_json_snapshot!("event_data_reason_completed", data);
4020 });
4021 }
4022
4023 #[test]
4024 fn snapshot_act_started() {
4025 let data = ActStartedData {
4026 tool_calls: vec![ToolCallSummary {
4027 id: "tc_1".to_string(),
4028 name: "get_weather".to_string(),
4029 display_name: None,
4030 narration: None,
4031 }],
4032 headline: None,
4033 };
4034 with_settings!({
4035 sort_maps => true,
4036 }, {
4037 assert_json_snapshot!("event_data_act_started", data);
4038 });
4039 }
4040
4041 #[test]
4042 fn snapshot_act_completed() {
4043 let data = ActCompletedData {
4044 completed: true,
4045 success_count: 2,
4046 error_count: 0,
4047 duration_ms: Some(500),
4048 headline: None,
4049 };
4050 with_settings!({
4051 sort_maps => true,
4052 }, {
4053 assert_json_snapshot!("event_data_act_completed", data);
4054 });
4055 }
4056
4057 #[test]
4058 fn snapshot_tool_started() {
4059 let data = ToolStartedData {
4060 tool_call: ToolCall {
4061 id: "tc_1".to_string(),
4062 name: "get_weather".to_string(),
4063 arguments: serde_json::json!({"city": "London"}),
4064 },
4065 tool_call_fingerprint: None,
4066 display_name: None,
4067 narration: None,
4068 };
4069 with_settings!({
4070 sort_maps => true,
4071 }, {
4072 assert_json_snapshot!("event_data_tool_started", data);
4073 });
4074 }
4075
4076 #[test]
4077 fn snapshot_tool_completed() {
4078 let data = ToolCompletedData::success(
4079 "tc_1".to_string(),
4080 "get_weather".to_string(),
4081 vec![crate::message::ContentPart::text("Sunny, 22°C")],
4082 Some(250),
4083 );
4084 with_settings!({
4085 sort_maps => true,
4086 }, {
4087 assert_json_snapshot!("event_data_tool_completed", data);
4088 });
4089 }
4090
4091 #[test]
4092 fn snapshot_llm_generation() {
4093 let data = LlmGenerationData::success(
4094 vec![Message::user("Hello")],
4095 vec![ToolDefinitionSummary {
4096 name: "tool1".to_string(),
4097 display_name: None,
4098 category: None,
4099 capability_id: None,
4100 capability_name: None,
4101 description: "A tool".to_string(),
4102 }],
4103 Some("Hi there!".to_string()),
4104 vec![],
4105 "gpt-4o".to_string(),
4106 Some("openai".to_string()),
4107 Some(TokenUsage::new(10, 5)),
4108 Some(100),
4109 Some(25),
4110 );
4111 with_settings!({
4112 sort_maps => true,
4113 }, {
4114 assert_json_snapshot!("event_data_llm_generation", data, {
4116 ".messages[].id" => "[MESSAGE_ID]",
4117 ".messages[].created_at" => "[TIMESTAMP]"
4118 });
4119 });
4120 }
4121
4122 #[test]
4123 fn snapshot_reason_thinking_started() {
4124 let data = ReasonThinkingStartedData {
4125 turn_id: test_turn_id(),
4126 model: Some("claude-4-opus".to_string()),
4127 };
4128 with_settings!({
4129 sort_maps => true,
4130 }, {
4131 assert_json_snapshot!("event_data_reason_thinking_started", data);
4132 });
4133 }
4134
4135 #[test]
4136 fn snapshot_reason_thinking_delta() {
4137 let data = ReasonThinkingDeltaData {
4138 turn_id: test_turn_id(),
4139 delta: "Let me think...".to_string(),
4140 accumulated: "Let me think...".to_string(),
4141 };
4142 with_settings!({
4143 sort_maps => true,
4144 }, {
4145 assert_json_snapshot!("event_data_reason_thinking_delta", data);
4146 });
4147 }
4148
4149 #[test]
4150 fn snapshot_reason_thinking_completed() {
4151 let data = ReasonThinkingCompletedData {
4152 turn_id: test_turn_id(),
4153 thinking: "I need to consider...".to_string(),
4154 };
4155 with_settings!({
4156 sort_maps => true,
4157 }, {
4158 assert_json_snapshot!("event_data_reason_thinking_completed", data);
4159 });
4160 }
4161
4162 #[test]
4163 fn snapshot_reason_item() {
4164 let data = ReasonItemData {
4165 turn_id: test_turn_id(),
4166 provider: "openai".to_string(),
4167 model: Some("gpt-5.5".to_string()),
4168 item_id: "rs_test".to_string(),
4169 encrypted_content: Some("OPAQUE".to_string()),
4170 summary: vec!["safe summary".to_string()],
4171 token_count: Some(42),
4172 };
4173 with_settings!({
4174 sort_maps => true,
4175 }, {
4176 assert_json_snapshot!("event_data_reason_item", data);
4177 });
4178 }
4179
4180 #[test]
4181 fn snapshot_session_started() {
4182 let data = SessionStartedData {
4183 harness_id: test_harness_id(),
4184 agent_id: Some(test_agent_id()),
4185 model_id: None,
4186 };
4187 with_settings!({
4188 sort_maps => true,
4189 }, {
4190 assert_json_snapshot!("event_data_session_started", data);
4191 });
4192 }
4193
4194 #[test]
4195 fn snapshot_session_activated() {
4196 let data = SessionActivatedData {
4197 turn_id: test_turn_id(),
4198 input_message_id: test_message_id(),
4199 };
4200 with_settings!({
4201 sort_maps => true,
4202 }, {
4203 assert_json_snapshot!("event_data_session_activated", data);
4204 });
4205 }
4206
4207 #[test]
4208 fn snapshot_session_idled() {
4209 let data = SessionIdledData {
4210 turn_id: test_turn_id(),
4211 iterations: Some(3),
4212 usage: Some(TokenUsage::new(500, 200)),
4213 };
4214 with_settings!({
4215 sort_maps => true,
4216 }, {
4217 assert_json_snapshot!("event_data_session_idled", data);
4218 });
4219 }
4220
4221 #[test]
4227 fn tool_call_summary_with_display_name() {
4228 let summary = ToolCallSummary {
4229 id: "tc_1".to_string(),
4230 name: "get_weather".to_string(),
4231 display_name: Some("Get Weather".to_string()),
4232 narration: None,
4233 };
4234 let json = serde_json::to_value(&summary).unwrap();
4235 assert_eq!(json["display_name"], "Get Weather");
4236
4237 let deserialized: ToolCallSummary = serde_json::from_value(json).unwrap();
4239 assert_eq!(deserialized.display_name.as_deref(), Some("Get Weather"));
4240 }
4241
4242 #[test]
4243 fn tool_call_summary_without_display_name_omits_field() {
4244 let summary = ToolCallSummary {
4245 id: "tc_1".to_string(),
4246 name: "get_weather".to_string(),
4247 display_name: None,
4248 narration: None,
4249 };
4250 let json = serde_json::to_string(&summary).unwrap();
4251 assert!(!json.contains("display_name"));
4252
4253 let json_without = r#"{"id":"tc_1","name":"get_weather"}"#;
4255 let deserialized: ToolCallSummary = serde_json::from_str(json_without).unwrap();
4256 assert_eq!(deserialized.display_name, None);
4257 }
4258
4259 #[test]
4260 fn act_started_with_definitions_populates_display_names() {
4261 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4262
4263 let tool_calls = vec![
4264 ToolCall {
4265 id: "tc_1".to_string(),
4266 name: "get_weather".to_string(),
4267 arguments: serde_json::json!({}),
4268 },
4269 ToolCall {
4270 id: "tc_2".to_string(),
4271 name: "unknown_tool".to_string(),
4272 arguments: serde_json::json!({}),
4273 },
4274 ];
4275 let tool_defs = vec![crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4276 name: "get_weather".to_string(),
4277 display_name: Some("Get Weather".to_string()),
4278 description: "Gets weather".to_string(),
4279 parameters: serde_json::json!({}),
4280 policy: ToolPolicy::Auto,
4281 category: None,
4282 deferrable: DeferrablePolicy::default(),
4283 hints: crate::tool_types::ToolHints::default(),
4284 full_parameters: None,
4285 })];
4286
4287 let data = ActStartedData::with_definitions(&tool_calls, &tool_defs);
4288 assert_eq!(data.tool_calls.len(), 2);
4289 assert_eq!(
4290 data.tool_calls[0].display_name.as_deref(),
4291 Some("Get Weather")
4292 );
4293 assert_eq!(data.tool_calls[1].display_name, None);
4294 }
4295
4296 #[test]
4297 fn tool_completed_with_display_name_roundtrip() {
4298 let data = ToolCompletedData::success(
4299 "tc_1".to_string(),
4300 "get_weather".to_string(),
4301 vec![crate::message::ContentPart::text("Sunny")],
4302 Some(100),
4303 )
4304 .with_display_name(Some("Get Weather".to_string()));
4305
4306 assert_eq!(data.display_name.as_deref(), Some("Get Weather"));
4307
4308 let json = serde_json::to_value(&data).unwrap();
4309 assert_eq!(json["display_name"], "Get Weather");
4310
4311 let deserialized: ToolCompletedData = serde_json::from_value(json).unwrap();
4312 assert_eq!(deserialized.display_name.as_deref(), Some("Get Weather"));
4313 }
4314
4315 #[test]
4316 fn tool_started_display_name_serialization() {
4317 let data = ToolStartedData {
4318 tool_call: ToolCall {
4319 id: "tc_1".to_string(),
4320 name: "bash".to_string(),
4321 arguments: serde_json::json!({"command": "ls"}),
4322 },
4323 tool_call_fingerprint: None,
4324 display_name: Some("Bash".to_string()),
4325 narration: None,
4326 };
4327
4328 let json = serde_json::to_value(&data).unwrap();
4329 assert_eq!(json["display_name"], "Bash");
4330 }
4331
4332 #[test]
4333 fn tool_definition_summary_display_name() {
4334 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4335
4336 let def = crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4337 name: "read_file".to_string(),
4338 display_name: Some("Read File".to_string()),
4339 description: "Reads a file".to_string(),
4340 parameters: serde_json::json!({}),
4341 policy: ToolPolicy::Auto,
4342 category: None,
4343 deferrable: DeferrablePolicy::default(),
4344 hints: crate::tool_types::ToolHints::default(),
4345 full_parameters: None,
4346 });
4347
4348 let summary = ToolDefinitionSummary::from(&def);
4349 assert_eq!(summary.display_name.as_deref(), Some("Read File"));
4350
4351 let json = serde_json::to_value(&summary).unwrap();
4352 assert_eq!(json["display_name"], "Read File");
4353 }
4354
4355 #[test]
4362 fn forward_compat_unknown_fields_ignored() {
4363 let json = r#"{
4365 "turn_id": "turn_00000000000000000000000000000002",
4366 "iterations": 3,
4367 "duration_ms": 1500,
4368 "usage": {"input_tokens": 100, "output_tokens": 50},
4369 "future_field": "should be ignored",
4370 "another_new_field": 42
4371 }"#;
4372
4373 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4374 assert_eq!(data.iterations, 3);
4375 assert_eq!(data.duration_ms, Some(1500));
4376 }
4377
4378 #[test]
4379 fn forward_compat_unknown_event_type_becomes_unsupported() {
4380 let json = serde_json::json!({"some_field": "value"});
4382 let data = deserialize_event_data("future.event.type", json);
4383
4384 assert!(data.is_unsupported());
4385 assert_eq!(data.event_type(), "unsupported");
4386 }
4387
4388 #[test]
4389 fn forward_compat_unsupported_preserves_data() {
4390 let original = serde_json::json!({"key": "value", "nested": {"a": 1}});
4392 let data = deserialize_event_data("unknown.event", original.clone());
4393
4394 match data {
4395 EventData::Unsupported { event_type, data } => {
4396 assert_eq!(event_type, "unknown.event");
4397 assert_eq!(data, original);
4398 }
4399 _ => panic!("Expected Unsupported variant"),
4400 }
4401 }
4402
4403 #[test]
4404 fn forward_compat_optional_fields_absent() {
4405 let json = r#"{
4407 "turn_id": "turn_00000000000000000000000000000002",
4408 "iterations": 3
4409 }"#;
4410
4411 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4412 assert_eq!(data.iterations, 3);
4413 assert!(data.duration_ms.is_none());
4414 assert!(data.usage.is_none());
4415 assert!(data.input_content.is_none());
4416 assert!(data.final_message_id.is_none());
4417 assert!(data.final_answer_preview.is_none());
4418 assert!(data.time_to_first_token_ms.is_none());
4419 assert!(data.tool_call_count.is_none());
4420 assert!(data.llm_call_count.is_none());
4421 assert!(data.status.is_none());
4422 }
4423
4424 #[test]
4430 fn round_trip_all_event_data_types() {
4431 let test_cases: Vec<(&str, EventData)> = vec![
4433 (
4434 INPUT_MESSAGE,
4435 InputMessageData::new(Message::user("test")).into(),
4436 ),
4437 (
4438 OUTPUT_MESSAGE_STARTED,
4439 OutputMessageStartedData {
4440 turn_id: test_turn_id(),
4441 model: None,
4442 iteration: None,
4443 }
4444 .into(),
4445 ),
4446 (
4447 OUTPUT_MESSAGE_DELTA,
4448 OutputMessageDeltaData {
4449 turn_id: test_turn_id(),
4450 delta: "x".to_string(),
4451 accumulated: "x".to_string(),
4452 }
4453 .into(),
4454 ),
4455 (
4456 OUTPUT_MESSAGE_COMPLETED,
4457 OutputMessageCompletedData::new(Message::assistant("hi")).into(),
4458 ),
4459 (
4460 TURN_STARTED,
4461 TurnStartedData {
4462 turn_id: test_turn_id(),
4463 input_message_id: test_message_id(),
4464 input_content: None,
4465 }
4466 .into(),
4467 ),
4468 (
4469 TURN_COMPLETED,
4470 TurnCompletedData {
4471 turn_id: test_turn_id(),
4472 iterations: 1,
4473 duration_ms: None,
4474 usage: None,
4475 input_content: None,
4476 final_message_id: None,
4477 final_answer_preview: None,
4478 time_to_first_token_ms: None,
4479 tool_call_count: None,
4480 llm_call_count: None,
4481 status: None,
4482 }
4483 .into(),
4484 ),
4485 (
4486 TURN_FAILED,
4487 TurnFailedData {
4488 turn_id: test_turn_id(),
4489 error: "err".to_string(),
4490 error_code: None,
4491 error_fields: None,
4492 error_disclosure: None,
4493 }
4494 .into(),
4495 ),
4496 (
4497 TURN_CANCELLED,
4498 TurnCancelledData {
4499 turn_id: test_turn_id(),
4500 reason: None,
4501 usage: None,
4502 }
4503 .into(),
4504 ),
4505 (
4506 TURN_SEALED,
4507 TurnSealedData {
4508 turn_id: test_turn_id(),
4509 reason: "no_progress".to_string(),
4510 detail: Some("sealed".to_string()),
4511 iterations: Some(3),
4512 usage: None,
4513 }
4514 .into(),
4515 ),
4516 (
4517 REASON_STARTED,
4518 ReasonStartedData {
4519 harness_id: test_harness_id(),
4520 agent_id: Some(test_agent_id()),
4521 metadata: None,
4522 }
4523 .into(),
4524 ),
4525 (
4526 REASON_COMPLETED,
4527 ReasonCompletedData::success("", false, 0, None, None).into(),
4528 ),
4529 (
4530 ACT_STARTED,
4531 ActStartedData {
4532 tool_calls: vec![],
4533 headline: None,
4534 }
4535 .into(),
4536 ),
4537 (
4538 ACT_COMPLETED,
4539 ActCompletedData {
4540 completed: true,
4541 success_count: 0,
4542 error_count: 0,
4543 duration_ms: None,
4544 headline: None,
4545 }
4546 .into(),
4547 ),
4548 (
4549 SESSION_STARTED,
4550 SessionStartedData {
4551 harness_id: test_harness_id(),
4552 agent_id: Some(test_agent_id()),
4553 model_id: None,
4554 }
4555 .into(),
4556 ),
4557 (
4558 SESSION_ACTIVATED,
4559 SessionActivatedData {
4560 turn_id: test_turn_id(),
4561 input_message_id: test_message_id(),
4562 }
4563 .into(),
4564 ),
4565 (
4566 SESSION_IDLED,
4567 SessionIdledData {
4568 turn_id: test_turn_id(),
4569 iterations: None,
4570 usage: None,
4571 }
4572 .into(),
4573 ),
4574 ];
4575
4576 for (event_type, original) in test_cases {
4577 let json = serde_json::to_value(&original).unwrap();
4579 let deserialized = deserialize_event_data(event_type, json);
4581 assert_eq!(
4583 original.event_type(),
4584 deserialized.event_type(),
4585 "Event type mismatch for {}",
4586 event_type
4587 );
4588 }
4589 }
4590
4591 #[test]
4597 fn event_structure_has_required_fields() {
4598 let session_id = test_session_id();
4599 let context = EventContext::turn(test_turn_id(), test_message_id());
4600 let event = Event::new(
4601 session_id,
4602 context,
4603 InputMessageData::new(Message::user("test")),
4604 );
4605
4606 let json = serde_json::to_value(&event).unwrap();
4608 assert!(json.get("id").is_some(), "Missing id field");
4609 assert!(json.get("type").is_some(), "Missing type field");
4610 assert!(json.get("ts").is_some(), "Missing ts field");
4611 assert!(json.get("session_id").is_some(), "Missing session_id field");
4612 assert!(json.get("context").is_some(), "Missing context field");
4613 assert!(json.get("data").is_some(), "Missing data field");
4614 }
4615
4616 #[test]
4617 fn event_context_span_fields() {
4618 let context = EventContext::empty().with_span(
4619 "trace123".to_string(),
4620 "span456".to_string(),
4621 Some("parent789".to_string()),
4622 );
4623
4624 let json = serde_json::to_value(&context).unwrap();
4625 assert_eq!(
4626 json.get("trace_id").and_then(|v| v.as_str()),
4627 Some("trace123")
4628 );
4629 assert_eq!(
4630 json.get("span_id").and_then(|v| v.as_str()),
4631 Some("span456")
4632 );
4633 assert_eq!(
4634 json.get("parent_span_id").and_then(|v| v.as_str()),
4635 Some("parent789")
4636 );
4637 }
4638
4639 #[test]
4640 fn is_unsupported_returns_false_for_known_types() {
4641 let data = InputMessageData::new(Message::user("test"));
4642 let event_data: EventData = data.into();
4643 assert!(!event_data.is_unsupported());
4644 }
4645
4646 #[test]
4647 fn is_unsupported_returns_true_for_unsupported() {
4648 let data = deserialize_event_data("unknown.type", serde_json::json!({}));
4649 assert!(data.is_unsupported());
4650 }
4651}