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_CANCELLED: &str = "turn.cancelled";
56
57pub const REASON_STARTED: &str = "reason.started";
59pub const REASON_COMPLETED: &str = "reason.completed";
60pub const CAPABILITY_USAGE: &str = "capability.usage";
61pub const ACT_STARTED: &str = "act.started";
62pub const ACT_COMPLETED: &str = "act.completed";
63pub const TOOL_STARTED: &str = "tool.started";
64pub const TOOL_COMPLETED: &str = "tool.completed";
65pub const TOOL_PROGRESS: &str = "tool.progress";
66pub const TOOL_OUTPUT_DELTA: &str = "tool.output.delta";
67pub const TOOL_CALL_REQUESTED: &str = "tool.call_requested";
68
69pub const LLM_GENERATION: &str = "llm.generation";
71
72fn is_ephemeral_event_type(event_type: &str) -> bool {
76 matches!(
77 event_type,
78 OUTPUT_MESSAGE_DELTA
79 | REASON_THINKING_DELTA
80 | TOOL_OUTPUT_DELTA
81 | VOICE_INPUT_TRANSCRIPT_DELTA
82 | VOICE_OUTPUT_TRANSCRIPT_DELTA
83 )
84}
85
86pub const REASON_THINKING_STARTED: &str = "reason.thinking.started";
88pub const REASON_THINKING_DELTA: &str = "reason.thinking.delta";
89pub const REASON_THINKING_COMPLETED: &str = "reason.thinking.completed";
90
91pub const REASON_ITEM: &str = "reason.item";
98
99pub const SESSION_STARTED: &str = "session.started";
101pub const SESSION_ACTIVATED: &str = "session.activated";
102pub const SESSION_IDLED: &str = "session.idled";
103
104pub const SCHEDULE_TRIGGERED: &str = "schedule.triggered";
106
107pub const SUBAGENT_SPAWNED: &str = "subagent.spawned";
109pub const SUBAGENT_COMPLETED: &str = "subagent.completed";
110pub const SUBAGENT_FAILED: &str = "subagent.failed";
111pub const SUBAGENT_CANCELLED: &str = "subagent.cancelled";
112
113pub const CONTEXT_COMPACTING: &str = "context.compacting";
115pub const CONTEXT_COMPACTED: &str = "context.compacted";
116
117pub const FILE_WRITTEN: &str = "file.written";
119
120pub const BUDGET_WARNING: &str = "budget.warning";
122pub const BUDGET_PAUSED: &str = "budget.paused";
123pub const BUDGET_EXHAUSTED: &str = "budget.exhausted";
124pub const BUDGET_RESUMED: &str = "budget.resumed";
125
126pub const VOICE_SESSION_STARTED: &str = "voice.session.started";
128pub const VOICE_INPUT_TRANSCRIPT_DELTA: &str = "voice.input_transcript.delta";
129pub const VOICE_INPUT_TRANSCRIPT_COMPLETED: &str = "voice.input_transcript.completed";
130pub const VOICE_OUTPUT_TRANSCRIPT_DELTA: &str = "voice.output_transcript.delta";
131pub const VOICE_OUTPUT_TRANSCRIPT_COMPLETED: &str = "voice.output_transcript.completed";
132pub const VOICE_SESSION_ENDED: &str = "voice.session.ended";
133pub const VOICE_SESSION_FAILED: &str = "voice.session.failed";
134
135pub const VALID_EVENT_TYPES: &[&str] = &[
139 INPUT_MESSAGE,
140 OUTPUT_MESSAGE_STARTED,
141 OUTPUT_MESSAGE_DELTA,
142 OUTPUT_MESSAGE_COMPLETED,
143 OUTPUT_MESSAGE_REPLACED,
144 TURN_STARTED,
145 TURN_COMPLETED,
146 TURN_FAILED,
147 TURN_CANCELLED,
148 REASON_STARTED,
149 REASON_COMPLETED,
150 ACT_STARTED,
151 ACT_COMPLETED,
152 TOOL_STARTED,
153 TOOL_COMPLETED,
154 TOOL_PROGRESS,
155 TOOL_OUTPUT_DELTA,
156 TOOL_CALL_REQUESTED,
157 LLM_GENERATION,
158 REASON_THINKING_STARTED,
159 REASON_THINKING_DELTA,
160 REASON_THINKING_COMPLETED,
161 REASON_ITEM,
162 SESSION_STARTED,
163 SESSION_ACTIVATED,
164 SESSION_IDLED,
165 SCHEDULE_TRIGGERED,
166 SUBAGENT_SPAWNED,
167 SUBAGENT_COMPLETED,
168 SUBAGENT_FAILED,
169 SUBAGENT_CANCELLED,
170 CONTEXT_COMPACTING,
171 CONTEXT_COMPACTED,
172 BUDGET_WARNING,
173 BUDGET_PAUSED,
174 BUDGET_EXHAUSTED,
175 BUDGET_RESUMED,
176 VOICE_SESSION_STARTED,
177 VOICE_INPUT_TRANSCRIPT_DELTA,
178 VOICE_INPUT_TRANSCRIPT_COMPLETED,
179 VOICE_OUTPUT_TRANSCRIPT_DELTA,
180 VOICE_OUTPUT_TRANSCRIPT_COMPLETED,
181 VOICE_SESSION_ENDED,
182 VOICE_SESSION_FAILED,
183 FILE_WRITTEN,
184];
185
186use crate::atoms::AtomContext;
191
192#[derive(Debug, Clone, Serialize, Deserialize, Default)]
199#[cfg_attr(feature = "openapi", derive(ToSchema))]
200pub struct EventContext {
201 #[serde(skip_serializing_if = "Option::is_none")]
203 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "turn_01933b5a00007000800000000000001"))]
204 pub turn_id: Option<TurnId>,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
208 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "message_01933b5a00007000800000000000001"))]
209 pub input_message_id: Option<MessageId>,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "exec_01933b5a00007000800000000000001"))]
214 pub exec_id: Option<ExecId>,
215
216 #[serde(skip_serializing_if = "Option::is_none")]
219 pub trace_id: Option<String>,
220
221 #[serde(skip_serializing_if = "Option::is_none")]
224 pub span_id: Option<String>,
225
226 #[serde(skip_serializing_if = "Option::is_none")]
229 pub parent_span_id: Option<String>,
230}
231
232impl EventContext {
233 pub fn empty() -> Self {
235 Self::default()
236 }
237
238 pub fn from_atom_context(ctx: &AtomContext) -> Self {
240 Self {
241 turn_id: Some(ctx.turn_id),
242 input_message_id: Some(ctx.input_message_id),
243 exec_id: Some(ctx.exec_id),
244 trace_id: None,
245 span_id: None,
246 parent_span_id: None,
247 }
248 }
249
250 pub fn turn(turn_id: TurnId, input_message_id: MessageId) -> Self {
252 Self {
253 turn_id: Some(turn_id),
254 input_message_id: Some(input_message_id),
255 exec_id: None,
256 trace_id: None,
257 span_id: None,
258 parent_span_id: None,
259 }
260 }
261
262 pub fn with_span(
264 mut self,
265 trace_id: String,
266 span_id: String,
267 parent_span_id: Option<String>,
268 ) -> Self {
269 self.trace_id = Some(trace_id);
270 self.span_id = Some(span_id);
271 self.parent_span_id = parent_span_id;
272 self
273 }
274}
275
276#[derive(Debug, Clone, Serialize)]
292#[cfg_attr(feature = "openapi", derive(ToSchema))]
293pub struct Event {
294 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "event_01933b5a00007000800000000000001"))]
296 pub id: EventId,
297
298 #[serde(rename = "type")]
300 pub event_type: String,
301
302 pub ts: DateTime<Utc>,
304
305 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "session_01933b5a00007000800000000000001"))]
307 pub session_id: SessionId,
308
309 pub context: EventContext,
311
312 pub data: EventData,
315
316 #[serde(skip_serializing_if = "Option::is_none")]
318 pub metadata: Option<serde_json::Value>,
319
320 #[serde(skip_serializing_if = "Option::is_none")]
322 pub tags: Option<Vec<String>>,
323
324 #[serde(skip_serializing_if = "Option::is_none")]
326 pub sequence: Option<i32>,
327}
328
329#[derive(Debug, Deserialize)]
330struct RawEvent {
331 id: EventId,
332 #[serde(rename = "type")]
333 event_type: String,
334 ts: DateTime<Utc>,
335 session_id: SessionId,
336 context: EventContext,
337 data: serde_json::Value,
338 metadata: Option<serde_json::Value>,
339 tags: Option<Vec<String>>,
340 sequence: Option<i32>,
341}
342
343impl<'de> Deserialize<'de> for Event {
344 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
345 where
346 D: Deserializer<'de>,
347 {
348 let raw = RawEvent::deserialize(deserializer)?;
349 let data = deserialize_event_data(&raw.event_type, raw.data);
350 Ok(Self {
351 id: raw.id,
352 event_type: raw.event_type,
353 ts: raw.ts,
354 session_id: raw.session_id,
355 context: raw.context,
356 data,
357 metadata: raw.metadata,
358 tags: raw.tags,
359 sequence: raw.sequence,
360 })
361 }
362}
363
364impl Event {
365 pub fn new(session_id: SessionId, context: EventContext, data: impl Into<EventData>) -> Self {
369 let data = data.into();
370 let event_type = data.event_type().to_string();
371 Self {
372 id: EventId::new(),
373 event_type,
374 ts: Utc::now(),
375 session_id,
376 context,
377 data,
378 metadata: None,
379 tags: None,
380 sequence: None,
381 }
382 }
383
384 pub fn with_id(
386 id: EventId,
387 session_id: SessionId,
388 context: EventContext,
389 data: impl Into<EventData>,
390 ) -> Self {
391 let data = data.into();
392 let event_type = data.event_type().to_string();
393 Self {
394 id,
395 event_type,
396 ts: Utc::now(),
397 session_id,
398 context,
399 data,
400 metadata: None,
401 tags: None,
402 sequence: None,
403 }
404 }
405
406 pub fn with_sequence(mut self, sequence: i32) -> Self {
408 self.sequence = Some(sequence);
409 self
410 }
411
412 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
414 self.metadata = Some(metadata);
415 self
416 }
417
418 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
420 self.tags = Some(tags);
421 self
422 }
423
424 pub fn session_uuid(&self) -> Uuid {
426 self.session_id.uuid()
427 }
428
429 pub fn is_message_event(&self) -> bool {
431 self.event_type == INPUT_MESSAGE || self.event_type == OUTPUT_MESSAGE_COMPLETED
432 }
433
434 pub fn is_ephemeral(&self) -> bool {
442 is_ephemeral_event_type(&self.event_type)
443 }
444
445 pub fn is_input_event(&self) -> bool {
447 self.event_type.starts_with("input.")
448 }
449
450 pub fn is_output_event(&self) -> bool {
452 self.event_type.starts_with("output.")
453 }
454
455 pub fn is_atom_event(&self) -> bool {
457 matches!(
458 self.event_type.as_str(),
459 REASON_STARTED
460 | REASON_COMPLETED
461 | ACT_STARTED
462 | ACT_COMPLETED
463 | TOOL_STARTED
464 | TOOL_COMPLETED
465 | TOOL_PROGRESS
466 | TOOL_CALL_REQUESTED
467 )
468 }
469
470 pub fn is_turn_event(&self) -> bool {
472 self.event_type.starts_with("turn.")
473 }
474
475 pub fn is_session_event(&self) -> bool {
477 self.event_type.starts_with("session.")
478 }
479
480 pub fn is_unsupported(&self) -> bool {
483 self.data.is_unsupported()
484 }
485}
486
487use crate::message::{ContentPart, Message};
492use crate::tool_narration::{
493 ToolNarrationPhase, render_group_headline_with_locale, render_tool_narration_with_locale,
494};
495use crate::tool_types::ToolCall;
496
497#[derive(Debug, Clone, Serialize, Deserialize)]
499#[cfg_attr(feature = "openapi", derive(ToSchema))]
500pub struct ModelMetadata {
501 pub model: String,
503
504 #[serde(skip_serializing_if = "Option::is_none")]
506 pub model_id: Option<Uuid>,
507
508 #[serde(skip_serializing_if = "Option::is_none")]
510 pub provider_id: Option<Uuid>,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize, Default)]
521#[cfg_attr(feature = "openapi", derive(ToSchema))]
522pub struct TokenUsage {
523 pub input_tokens: u32,
525 pub output_tokens: u32,
527 #[serde(skip_serializing_if = "Option::is_none")]
529 pub cache_read_tokens: Option<u32>,
530 #[serde(skip_serializing_if = "Option::is_none")]
532 pub cache_creation_tokens: Option<u32>,
533
534 #[serde(skip_serializing_if = "Option::is_none")]
538 pub actual_cost_usd: Option<f64>,
539
540 #[serde(skip_serializing_if = "Option::is_none")]
545 pub estimated_cost_usd: Option<f64>,
546}
547
548impl TokenUsage {
549 pub fn new(input_tokens: u32, output_tokens: u32) -> Self {
551 Self {
552 input_tokens,
553 output_tokens,
554 cache_read_tokens: None,
555 cache_creation_tokens: None,
556 actual_cost_usd: None,
557 estimated_cost_usd: None,
558 }
559 }
560
561 pub fn with_cache(
563 input_tokens: u32,
564 output_tokens: u32,
565 cache_read_tokens: Option<u32>,
566 cache_creation_tokens: Option<u32>,
567 ) -> Self {
568 Self {
569 input_tokens,
570 output_tokens,
571 cache_read_tokens,
572 cache_creation_tokens,
573 actual_cost_usd: None,
574 estimated_cost_usd: None,
575 }
576 }
577
578 pub fn with_cost(
582 mut self,
583 actual_cost_usd: Option<f64>,
584 estimated_cost_usd: Option<f64>,
585 ) -> Self {
586 self.actual_cost_usd = actual_cost_usd;
587 self.estimated_cost_usd = estimated_cost_usd;
588 self
589 }
590
591 pub fn effective_cost_usd(&self) -> Option<f64> {
595 self.actual_cost_usd.or(self.estimated_cost_usd)
596 }
597
598 pub fn total_tokens(&self) -> u32 {
600 self.input_tokens + self.output_tokens
601 }
602
603 pub fn add(&mut self, other: &TokenUsage) {
605 self.input_tokens += other.input_tokens;
606 self.output_tokens += other.output_tokens;
607 if let Some(cache) = other.cache_read_tokens {
608 *self.cache_read_tokens.get_or_insert(0) += cache;
609 }
610 if let Some(cache) = other.cache_creation_tokens {
611 *self.cache_creation_tokens.get_or_insert(0) += cache;
612 }
613 if let Some(cost) = other.actual_cost_usd {
614 *self.actual_cost_usd.get_or_insert(0.0) += cost;
615 }
616 if let Some(cost) = other.estimated_cost_usd {
617 *self.estimated_cost_usd.get_or_insert(0.0) += cost;
618 }
619 }
620}
621
622#[derive(Debug, Clone, Serialize, Deserialize)]
624#[cfg_attr(feature = "openapi", derive(ToSchema))]
625pub struct InputMessageData {
626 pub message: Message,
628}
629
630impl InputMessageData {
631 pub fn new(message: Message) -> Self {
632 Self { message }
633 }
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize)]
645#[cfg_attr(feature = "openapi", derive(ToSchema))]
646pub struct OutputMessageStartedData {
647 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
649 pub turn_id: TurnId,
650
651 #[serde(skip_serializing_if = "Option::is_none")]
653 pub model: Option<String>,
654
655 #[serde(skip_serializing_if = "Option::is_none")]
658 pub iteration: Option<u32>,
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize)]
666#[cfg_attr(feature = "openapi", derive(ToSchema))]
667pub struct OutputMessageDeltaData {
668 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
670 pub turn_id: TurnId,
671
672 pub delta: String,
674
675 pub accumulated: String,
677}
678
679#[derive(Debug, Clone, Serialize, Deserialize)]
681#[cfg_attr(feature = "openapi", derive(ToSchema))]
682pub struct OutputMessageCompletedData {
683 pub message: Message,
685
686 #[serde(skip_serializing_if = "Option::is_none")]
688 pub metadata: Option<ModelMetadata>,
689
690 #[serde(skip_serializing_if = "Option::is_none")]
692 pub usage: Option<TokenUsage>,
693
694 #[serde(default, skip_serializing_if = "Option::is_none")]
696 pub error_code: Option<String>,
697
698 #[serde(default, skip_serializing_if = "Option::is_none")]
700 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
701 pub error_fields: Option<UserFacingErrorFields>,
702}
703
704impl OutputMessageCompletedData {
705 pub fn new(message: Message) -> Self {
706 Self {
707 message,
708 metadata: None,
709 usage: None,
710 error_code: None,
711 error_fields: None,
712 }
713 }
714
715 pub fn with_metadata(mut self, metadata: ModelMetadata) -> Self {
716 self.metadata = Some(metadata);
717 self
718 }
719
720 pub fn with_usage(mut self, usage: TokenUsage) -> Self {
721 self.usage = Some(usage);
722 self
723 }
724
725 pub fn with_user_facing_error(mut self, error: &UserFacingError) -> Self {
726 error.apply_to_event_fields(&mut self.error_code, &mut self.error_fields);
727 self
728 }
729}
730
731#[derive(Debug, Clone, Serialize, Deserialize)]
738#[cfg_attr(feature = "openapi", derive(ToSchema))]
739pub struct OutputMessageReplacedData {
740 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
742 pub turn_id: TurnId,
743
744 pub guardrail_capability_id: String,
747
748 pub guardrail_id: String,
750
751 pub reason_code: String,
754
755 pub replacement: String,
757}
758
759#[derive(Debug, Clone, Serialize, Deserialize)]
765#[cfg_attr(feature = "openapi", derive(ToSchema))]
766pub struct ReasonStartedData {
767 pub harness_id: HarnessId,
769
770 #[serde(skip_serializing_if = "Option::is_none")]
772 pub agent_id: Option<AgentId>,
773
774 #[serde(skip_serializing_if = "Option::is_none")]
776 pub metadata: Option<ModelMetadata>,
777}
778
779#[derive(Debug, Clone, Serialize, Deserialize)]
781#[cfg_attr(feature = "openapi", derive(ToSchema))]
782pub struct ReasonCompletedData {
783 pub success: bool,
785
786 #[serde(skip_serializing_if = "Option::is_none")]
788 pub text_preview: Option<String>,
789
790 pub has_tool_calls: bool,
792
793 pub tool_call_count: u32,
795
796 #[serde(skip_serializing_if = "Option::is_none")]
798 pub error: Option<String>,
799
800 #[serde(skip_serializing_if = "Option::is_none")]
802 pub duration_ms: Option<u64>,
803
804 #[serde(skip_serializing_if = "Option::is_none")]
806 pub usage: Option<TokenUsage>,
807}
808
809impl ReasonCompletedData {
810 pub fn success(
811 text: &str,
812 has_tool_calls: bool,
813 tool_call_count: u32,
814 duration_ms: Option<u64>,
815 usage: Option<TokenUsage>,
816 ) -> Self {
817 let text_preview = if text.is_empty() {
818 None
819 } else {
820 Some(text.chars().take(200).collect())
821 };
822
823 Self {
824 success: true,
825 text_preview,
826 has_tool_calls,
827 tool_call_count,
828 error: None,
829 duration_ms,
830 usage,
831 }
832 }
833
834 pub fn failure(error: String, duration_ms: Option<u64>) -> Self {
835 Self {
836 success: false,
837 text_preview: None,
838 has_tool_calls: false,
839 tool_call_count: 0,
840 error: Some(error),
841 duration_ms,
842 usage: None,
843 }
844 }
845}
846
847#[derive(Debug, Clone, Serialize, Deserialize)]
849#[serde(rename_all = "snake_case")]
850#[cfg_attr(feature = "openapi", derive(ToSchema))]
851pub enum CapabilityUsageKind {
852 Configured,
853 Resolved,
854 Exposed,
855 Invoked,
856 EffectRan,
857}
858
859#[derive(Debug, Clone, Serialize, Deserialize)]
863#[cfg_attr(feature = "openapi", derive(ToSchema))]
864pub struct CapabilityUsageRecord {
865 pub capability_id: String,
867 #[serde(default, skip_serializing_if = "Option::is_none")]
869 pub capability_name: Option<String>,
870 pub usage_kind: CapabilityUsageKind,
872 #[serde(default, skip_serializing_if = "Option::is_none")]
874 pub tool_name: Option<String>,
875 #[serde(default, skip_serializing_if = "Option::is_none")]
877 pub usage_count: Option<u64>,
878 #[serde(default, skip_serializing_if = "Option::is_none")]
880 pub duration_ms: Option<u64>,
881}
882
883#[derive(Debug, Clone, Serialize, Deserialize)]
885#[cfg_attr(feature = "openapi", derive(ToSchema))]
886pub struct CapabilityUsageData {
887 pub records: Vec<CapabilityUsageRecord>,
888}
889
890#[derive(Debug, Clone, Serialize, Deserialize)]
892#[cfg_attr(feature = "openapi", derive(ToSchema))]
893pub struct ToolCallSummary {
894 pub id: String,
895 pub name: String,
896 #[serde(default, skip_serializing_if = "Option::is_none")]
898 pub display_name: Option<String>,
899 #[serde(default, skip_serializing_if = "Option::is_none")]
901 pub narration: Option<String>,
902}
903
904impl From<&ToolCall> for ToolCallSummary {
905 fn from(tc: &ToolCall) -> Self {
906 Self {
907 id: tc.id.clone(),
908 name: tc.name.clone(),
909 display_name: None,
910 narration: None,
911 }
912 }
913}
914
915#[derive(Debug, Clone, Serialize, Deserialize)]
917#[cfg_attr(feature = "openapi", derive(ToSchema))]
918pub struct ToolDefinitionSummary {
919 pub name: String,
921 #[serde(default, skip_serializing_if = "Option::is_none")]
923 pub display_name: Option<String>,
924 #[serde(default, skip_serializing_if = "Option::is_none")]
926 pub category: Option<String>,
927 #[serde(default, skip_serializing_if = "Option::is_none")]
929 pub capability_id: Option<String>,
930 #[serde(default, skip_serializing_if = "Option::is_none")]
932 pub capability_name: Option<String>,
933 pub description: String,
935}
936
937impl From<&crate::tool_types::ToolDefinition> for ToolDefinitionSummary {
938 fn from(tool: &crate::tool_types::ToolDefinition) -> Self {
939 let capability_attribution = tool.capability_attribution();
940 Self {
941 name: tool.name().to_string(),
942 display_name: tool.display_name().map(|s| s.to_string()),
943 category: tool.category().map(|s| s.to_string()),
944 capability_id: capability_attribution.map(|(id, _)| id.to_string()),
945 capability_name: capability_attribution.and_then(|(_, name)| name.map(str::to_string)),
946 description: tool.description().to_string(),
947 }
948 }
949}
950
951#[derive(Debug, Clone, Serialize, Deserialize)]
953#[cfg_attr(feature = "openapi", derive(ToSchema))]
954pub struct ActStartedData {
955 pub tool_calls: Vec<ToolCallSummary>,
957 #[serde(default, skip_serializing_if = "Option::is_none")]
959 pub headline: Option<String>,
960}
961
962impl ActStartedData {
963 pub fn new(tool_calls: &[ToolCall]) -> Self {
964 Self::new_with_locale(tool_calls, None)
965 }
966
967 pub fn new_with_locale(tool_calls: &[ToolCall], locale: Option<&str>) -> Self {
968 Self {
969 tool_calls: tool_calls.iter().map(ToolCallSummary::from).collect(),
970 headline: render_group_headline_with_locale(
971 tool_calls,
972 &[],
973 ToolNarrationPhase::Started,
974 locale,
975 ),
976 }
977 }
978
979 pub fn with_definitions(
981 tool_calls: &[ToolCall],
982 tool_defs: &[crate::tool_types::ToolDefinition],
983 ) -> Self {
984 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
985 }
986
987 pub fn with_definitions_and_locale(
988 tool_calls: &[ToolCall],
989 tool_defs: &[crate::tool_types::ToolDefinition],
990 locale: Option<&str>,
991 ) -> Self {
992 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
993 tool_defs.iter().map(|d| (d.name(), d)).collect();
994 Self {
995 tool_calls: tool_calls
996 .iter()
997 .map(|tc| {
998 let tool_def = def_map.get(tc.name.as_str()).copied();
999 let display_name = localized_tool_display_name(
1000 &tc.name,
1001 tool_def.and_then(|d| d.display_name()),
1002 locale,
1003 );
1004 ToolCallSummary {
1005 id: tc.id.clone(),
1006 name: tc.name.clone(),
1007 display_name,
1008 narration: Some(render_tool_narration_with_locale(
1009 tool_def,
1010 tc,
1011 ToolNarrationPhase::Started,
1012 locale,
1013 )),
1014 }
1015 })
1016 .collect(),
1017 headline: render_group_headline_with_locale(
1018 tool_calls,
1019 tool_defs,
1020 ToolNarrationPhase::Started,
1021 locale,
1022 ),
1023 }
1024 }
1025}
1026
1027#[derive(Debug, Clone, Serialize, Deserialize)]
1029#[cfg_attr(feature = "openapi", derive(ToSchema))]
1030pub struct ActCompletedData {
1031 pub completed: bool,
1033
1034 pub success_count: u32,
1036
1037 pub error_count: u32,
1039
1040 #[serde(skip_serializing_if = "Option::is_none")]
1042 pub duration_ms: Option<u64>,
1043 #[serde(default, skip_serializing_if = "Option::is_none")]
1045 pub headline: Option<String>,
1046}
1047
1048#[derive(Debug, Clone, Serialize, Deserialize)]
1050#[cfg_attr(feature = "openapi", derive(ToSchema))]
1051pub struct ToolStartedData {
1052 pub tool_call: ToolCall,
1054 #[serde(default, skip_serializing_if = "Option::is_none")]
1056 pub tool_call_fingerprint: Option<String>,
1057 #[serde(default, skip_serializing_if = "Option::is_none")]
1059 pub display_name: Option<String>,
1060 #[serde(default, skip_serializing_if = "Option::is_none")]
1062 pub narration: Option<String>,
1063}
1064
1065#[derive(Debug, Clone, Serialize, Deserialize)]
1067#[cfg_attr(feature = "openapi", derive(ToSchema))]
1068pub struct ToolCompletedData {
1069 pub tool_call_id: String,
1071
1072 pub tool_name: String,
1074
1075 #[serde(default, skip_serializing_if = "Option::is_none")]
1077 pub tool_call_fingerprint: Option<String>,
1078
1079 #[serde(default, skip_serializing_if = "Option::is_none")]
1081 pub tool_result_fingerprint: Option<String>,
1082
1083 #[serde(default, skip_serializing_if = "Option::is_none")]
1085 pub display_name: Option<String>,
1086
1087 pub success: bool,
1089
1090 pub status: String,
1092
1093 #[serde(skip_serializing_if = "Option::is_none")]
1095 pub result: Option<Vec<ContentPart>>,
1096
1097 #[serde(skip_serializing_if = "Option::is_none")]
1099 pub error: Option<String>,
1100
1101 #[serde(skip_serializing_if = "Option::is_none")]
1103 pub duration_ms: Option<u64>,
1104
1105 #[serde(default, skip_serializing_if = "Option::is_none")]
1107 pub capability_id: Option<String>,
1108
1109 #[serde(default, skip_serializing_if = "Option::is_none")]
1111 pub capability_name: Option<String>,
1112
1113 #[serde(default, skip_serializing_if = "Option::is_none")]
1115 pub narration: Option<String>,
1116}
1117
1118impl ToolCompletedData {
1119 pub fn success(
1120 tool_call_id: String,
1121 tool_name: String,
1122 result: Vec<ContentPart>,
1123 duration_ms: Option<u64>,
1124 ) -> Self {
1125 Self {
1126 tool_call_id,
1127 tool_name,
1128 tool_call_fingerprint: None,
1129 tool_result_fingerprint: None,
1130 display_name: None,
1131 success: true,
1132 status: "success".to_string(),
1133 result: Some(result),
1134 error: None,
1135 duration_ms,
1136 capability_id: None,
1137 capability_name: None,
1138 narration: None,
1139 }
1140 }
1141
1142 pub fn failure(
1143 tool_call_id: String,
1144 tool_name: String,
1145 status: String,
1146 error: String,
1147 duration_ms: Option<u64>,
1148 ) -> Self {
1149 Self {
1150 tool_call_id,
1151 tool_name,
1152 tool_call_fingerprint: None,
1153 tool_result_fingerprint: None,
1154 display_name: None,
1155 success: false,
1156 status,
1157 result: None,
1158 error: Some(error),
1159 duration_ms,
1160 capability_id: None,
1161 capability_name: None,
1162 narration: None,
1163 }
1164 }
1165
1166 pub fn with_display_name(mut self, display_name: Option<String>) -> Self {
1168 self.display_name = display_name;
1169 self
1170 }
1171
1172 pub fn with_fingerprints(
1173 mut self,
1174 tool_call_fingerprint: String,
1175 tool_result_fingerprint: String,
1176 ) -> Self {
1177 self.tool_call_fingerprint = Some(tool_call_fingerprint);
1178 self.tool_result_fingerprint = Some(tool_result_fingerprint);
1179 self
1180 }
1181
1182 pub fn with_narration(mut self, narration: Option<String>) -> Self {
1184 self.narration = narration;
1185 self
1186 }
1187
1188 pub fn with_capability_attribution(
1190 mut self,
1191 capability_id: Option<String>,
1192 capability_name: Option<String>,
1193 ) -> Self {
1194 self.capability_id = capability_id;
1195 self.capability_name = capability_name;
1196 self
1197 }
1198}
1199
1200#[derive(Debug, Clone, Serialize, Deserialize)]
1206#[cfg_attr(feature = "openapi", derive(ToSchema))]
1207pub struct ToolProgressData {
1208 pub tool_call_id: String,
1210
1211 pub tool_name: String,
1213
1214 pub message: String,
1216
1217 #[serde(default, skip_serializing_if = "Option::is_none")]
1219 pub display_name: Option<String>,
1220}
1221
1222#[derive(Debug, Clone, Serialize, Deserialize)]
1232#[cfg_attr(feature = "openapi", derive(ToSchema))]
1233pub struct ToolOutputDeltaData {
1234 pub tool_call_id: String,
1236
1237 pub tool_name: String,
1239
1240 pub delta: String,
1242
1243 pub stream: String,
1245}
1246
1247#[derive(Debug, Clone, Serialize, Deserialize)]
1252#[cfg_attr(feature = "openapi", derive(ToSchema))]
1253pub struct ToolCallRequestedData {
1254 pub tool_calls: Vec<ToolCall>,
1256 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1258 pub tool_summaries: Vec<ToolCallSummary>,
1259 #[serde(default, skip_serializing_if = "Option::is_none")]
1261 pub headline: Option<String>,
1262}
1263
1264impl ToolCallRequestedData {
1265 pub fn with_definitions(
1266 tool_calls: &[ToolCall],
1267 tool_defs: &[crate::tool_types::ToolDefinition],
1268 ) -> Self {
1269 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
1270 }
1271
1272 pub fn with_definitions_and_locale(
1273 tool_calls: &[ToolCall],
1274 tool_defs: &[crate::tool_types::ToolDefinition],
1275 locale: Option<&str>,
1276 ) -> Self {
1277 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
1278 tool_defs.iter().map(|d| (d.name(), d)).collect();
1279
1280 let tool_summaries = tool_calls
1281 .iter()
1282 .map(|tool_call| {
1283 let tool_def = def_map.get(tool_call.name.as_str()).copied();
1284 ToolCallSummary {
1285 id: tool_call.id.clone(),
1286 name: tool_call.name.clone(),
1287 display_name: localized_tool_display_name(
1288 &tool_call.name,
1289 tool_def.and_then(|def| def.display_name()),
1290 locale,
1291 ),
1292 narration: Some(render_tool_narration_with_locale(
1293 tool_def,
1294 tool_call,
1295 ToolNarrationPhase::Waiting,
1296 locale,
1297 )),
1298 }
1299 })
1300 .collect();
1301
1302 Self {
1303 tool_calls: tool_calls.to_vec(),
1304 tool_summaries,
1305 headline: render_group_headline_with_locale(
1306 tool_calls,
1307 tool_defs,
1308 ToolNarrationPhase::Waiting,
1309 locale,
1310 ),
1311 }
1312 }
1313}
1314
1315#[derive(Debug, Clone, Serialize, Deserialize)]
1321#[cfg_attr(feature = "openapi", derive(ToSchema))]
1322pub struct LlmGenerationOutput {
1323 #[serde(skip_serializing_if = "Option::is_none")]
1325 pub text: Option<String>,
1326
1327 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1329 pub tool_calls: Vec<ToolCall>,
1330}
1331
1332#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1337#[cfg_attr(feature = "openapi", derive(ToSchema))]
1338pub struct LlmRequestOptions {
1339 #[serde(skip_serializing_if = "Option::is_none")]
1341 pub prompt_cache: Option<LlmPromptCacheInfo>,
1342 #[serde(skip_serializing_if = "Option::is_none")]
1344 pub tool_search: Option<LlmToolSearchInfo>,
1345 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1347 pub provider_options: HashMap<String, Value>,
1348}
1349
1350impl LlmRequestOptions {
1351 pub fn is_empty(&self) -> bool {
1352 self.prompt_cache.is_none()
1353 && self.tool_search.is_none()
1354 && self.provider_options.is_empty()
1355 }
1356}
1357
1358#[derive(Debug, Clone, Serialize, Deserialize)]
1360#[cfg_attr(feature = "openapi", derive(ToSchema))]
1361pub struct LlmPromptCacheInfo {
1362 pub enabled: bool,
1364 pub strategy: crate::llm_driver_registry::PromptCacheStrategy,
1366 #[serde(skip_serializing_if = "Option::is_none")]
1368 pub provider_mode: Option<String>,
1369}
1370
1371#[derive(Debug, Clone, Serialize, Deserialize)]
1373#[cfg_attr(feature = "openapi", derive(ToSchema))]
1374pub struct LlmToolSearchInfo {
1375 pub enabled: bool,
1377 pub threshold: usize,
1379}
1380
1381#[derive(Debug, Clone, Serialize, Deserialize)]
1383#[cfg_attr(feature = "openapi", derive(ToSchema))]
1384pub struct LlmGenerationMetadata {
1385 #[cfg_attr(feature = "openapi", schema(example = "claude-sonnet-4-5"))]
1387 pub model: String,
1388
1389 #[serde(skip_serializing_if = "Option::is_none")]
1391 #[cfg_attr(feature = "openapi", schema(example = "anthropic"))]
1392 pub provider: Option<String>,
1393
1394 #[serde(skip_serializing_if = "Option::is_none")]
1396 pub usage: Option<TokenUsage>,
1397
1398 #[serde(skip_serializing_if = "Option::is_none")]
1400 #[cfg_attr(feature = "openapi", schema(example = 1_842u64))]
1401 pub duration_ms: Option<u64>,
1402
1403 #[serde(skip_serializing_if = "Option::is_none")]
1405 #[cfg_attr(feature = "openapi", schema(example = 312u64))]
1406 pub time_to_first_token_ms: Option<u64>,
1407
1408 #[cfg_attr(feature = "openapi", schema(example = true))]
1410 pub success: bool,
1411
1412 #[serde(skip_serializing_if = "Option::is_none")]
1414 #[cfg_attr(feature = "openapi", schema(example = "provider returned 503"))]
1415 pub error: Option<String>,
1416
1417 #[serde(skip_serializing_if = "Option::is_none")]
1420 #[cfg_attr(feature = "openapi", schema(example = json!(["tool_calls"])))]
1421 pub finish_reasons: Option<Vec<String>>,
1422
1423 #[serde(skip_serializing_if = "Option::is_none")]
1426 #[cfg_attr(feature = "openapi", schema(example = "msg_01ABCDef0123456789"))]
1427 pub response_id: Option<String>,
1428
1429 #[serde(skip_serializing_if = "Option::is_none")]
1432 pub retry: Option<LlmRetryInfo>,
1433
1434 #[serde(skip_serializing_if = "Option::is_none")]
1437 pub compaction: Option<LlmCompactionInfo>,
1438
1439 #[serde(skip_serializing_if = "Option::is_none")]
1441 pub request_options: Option<LlmRequestOptions>,
1442}
1443
1444#[derive(Debug, Clone, Serialize, Deserialize)]
1446#[cfg_attr(feature = "openapi", derive(ToSchema))]
1447pub struct LlmRetryInfo {
1448 pub attempts: u32,
1450
1451 pub total_wait_ms: u64,
1453}
1454
1455#[derive(Debug, Clone, Serialize, Deserialize)]
1460#[cfg_attr(feature = "openapi", derive(ToSchema))]
1461pub struct LlmCompactionInfo {
1462 pub compacted: bool,
1464
1465 #[serde(skip_serializing_if = "Option::is_none")]
1467 pub input_tokens_before: Option<u32>,
1468
1469 #[serde(skip_serializing_if = "Option::is_none")]
1471 pub input_tokens_after: Option<u32>,
1472
1473 #[serde(skip_serializing_if = "Option::is_none")]
1475 pub duration_ms: Option<u64>,
1476}
1477
1478impl LlmCompactionInfo {
1479 pub fn new(
1481 input_tokens_before: Option<u32>,
1482 input_tokens_after: Option<u32>,
1483 duration_ms: Option<u64>,
1484 ) -> Self {
1485 Self {
1486 compacted: true,
1487 input_tokens_before,
1488 input_tokens_after,
1489 duration_ms,
1490 }
1491 }
1492}
1493
1494#[derive(Debug, Clone, Serialize, Deserialize)]
1499#[cfg_attr(feature = "openapi", derive(ToSchema))]
1500pub struct LlmGenerationData {
1501 pub messages: Vec<Message>,
1503
1504 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1506 pub tools: Vec<ToolDefinitionSummary>,
1507
1508 pub output: LlmGenerationOutput,
1510
1511 pub metadata: LlmGenerationMetadata,
1513}
1514
1515impl LlmGenerationData {
1516 #[allow(clippy::too_many_arguments)]
1518 pub fn success(
1519 messages: Vec<Message>,
1520 tools: Vec<ToolDefinitionSummary>,
1521 text: Option<String>,
1522 tool_calls: Vec<ToolCall>,
1523 model: String,
1524 provider: Option<String>,
1525 usage: Option<TokenUsage>,
1526 duration_ms: Option<u64>,
1527 time_to_first_token_ms: Option<u64>,
1528 ) -> Self {
1529 let finish_reasons = if !tool_calls.is_empty() {
1531 Some(vec!["tool_calls".to_string()])
1532 } else {
1533 Some(vec!["stop".to_string()])
1534 };
1535
1536 Self {
1537 messages,
1538 tools,
1539 output: LlmGenerationOutput { text, tool_calls },
1540 metadata: LlmGenerationMetadata {
1541 model,
1542 provider,
1543 usage,
1544 duration_ms,
1545 time_to_first_token_ms,
1546 success: true,
1547 error: None,
1548 finish_reasons,
1549 response_id: None,
1550 retry: None,
1551 compaction: None,
1552 request_options: None,
1553 },
1554 }
1555 }
1556
1557 #[allow(clippy::too_many_arguments)]
1559 pub fn success_with_metadata(
1560 messages: Vec<Message>,
1561 tools: Vec<ToolDefinitionSummary>,
1562 text: Option<String>,
1563 tool_calls: Vec<ToolCall>,
1564 model: String,
1565 provider: Option<String>,
1566 usage: Option<TokenUsage>,
1567 duration_ms: Option<u64>,
1568 time_to_first_token_ms: Option<u64>,
1569 finish_reasons: Option<Vec<String>>,
1570 response_id: Option<String>,
1571 ) -> Self {
1572 Self {
1573 messages,
1574 tools,
1575 output: LlmGenerationOutput { text, tool_calls },
1576 metadata: LlmGenerationMetadata {
1577 model,
1578 provider,
1579 usage,
1580 duration_ms,
1581 time_to_first_token_ms,
1582 success: true,
1583 error: None,
1584 finish_reasons,
1585 response_id,
1586 retry: None,
1587 compaction: None,
1588 request_options: None,
1589 },
1590 }
1591 }
1592
1593 #[allow(clippy::too_many_arguments)]
1595 pub fn success_with_retry(
1596 messages: Vec<Message>,
1597 tools: Vec<ToolDefinitionSummary>,
1598 text: Option<String>,
1599 tool_calls: Vec<ToolCall>,
1600 model: String,
1601 provider: Option<String>,
1602 usage: Option<TokenUsage>,
1603 duration_ms: Option<u64>,
1604 time_to_first_token_ms: Option<u64>,
1605 finish_reasons: Option<Vec<String>>,
1606 response_id: Option<String>,
1607 retry: Option<LlmRetryInfo>,
1608 ) -> Self {
1609 Self {
1610 messages,
1611 tools,
1612 output: LlmGenerationOutput { text, tool_calls },
1613 metadata: LlmGenerationMetadata {
1614 model,
1615 provider,
1616 usage,
1617 duration_ms,
1618 time_to_first_token_ms,
1619 success: true,
1620 error: None,
1621 finish_reasons,
1622 response_id,
1623 retry,
1624 compaction: None,
1625 request_options: None,
1626 },
1627 }
1628 }
1629
1630 pub fn failure(
1632 messages: Vec<Message>,
1633 tools: Vec<ToolDefinitionSummary>,
1634 model: String,
1635 provider: Option<String>,
1636 error: String,
1637 duration_ms: Option<u64>,
1638 time_to_first_token_ms: Option<u64>,
1639 ) -> Self {
1640 Self {
1641 messages,
1642 tools,
1643 output: LlmGenerationOutput {
1644 text: None,
1645 tool_calls: vec![],
1646 },
1647 metadata: LlmGenerationMetadata {
1648 model,
1649 provider,
1650 usage: None,
1651 duration_ms,
1652 time_to_first_token_ms,
1653 success: false,
1654 error: Some(error),
1655 finish_reasons: Some(vec!["error".to_string()]),
1656 response_id: None,
1657 retry: None,
1658 compaction: None,
1659 request_options: None,
1660 },
1661 }
1662 }
1663
1664 pub fn with_compaction(mut self, compaction: LlmCompactionInfo) -> Self {
1668 self.metadata.compaction = Some(compaction);
1669 self
1670 }
1671
1672 pub fn with_retry(mut self, retry: LlmRetryInfo) -> Self {
1674 self.metadata.retry = Some(retry);
1675 self
1676 }
1677
1678 pub fn with_request_options(mut self, request_options: LlmRequestOptions) -> Self {
1680 if !request_options.is_empty() {
1681 self.metadata.request_options = Some(request_options);
1682 }
1683 self
1684 }
1685}
1686
1687#[derive(Debug, Clone, Serialize, Deserialize)]
1697#[cfg_attr(feature = "openapi", derive(ToSchema))]
1698pub struct ReasonThinkingStartedData {
1699 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1701 pub turn_id: TurnId,
1702
1703 #[serde(skip_serializing_if = "Option::is_none")]
1705 pub model: Option<String>,
1706}
1707
1708#[derive(Debug, Clone, Serialize, Deserialize)]
1714#[cfg_attr(feature = "openapi", derive(ToSchema))]
1715pub struct ReasonThinkingDeltaData {
1716 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1718 pub turn_id: TurnId,
1719
1720 pub delta: String,
1722
1723 pub accumulated: String,
1725}
1726
1727#[derive(Debug, Clone, Serialize, Deserialize)]
1732#[cfg_attr(feature = "openapi", derive(ToSchema))]
1733pub struct ReasonThinkingCompletedData {
1734 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1736 pub turn_id: TurnId,
1737
1738 pub thinking: String,
1740}
1741
1742#[derive(Debug, Clone, Serialize, Deserialize)]
1750#[cfg_attr(feature = "openapi", derive(ToSchema))]
1751pub struct ReasonItemData {
1752 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1754 pub turn_id: TurnId,
1755
1756 pub provider: String,
1758
1759 #[serde(skip_serializing_if = "Option::is_none")]
1761 pub model: Option<String>,
1762
1763 pub item_id: String,
1765
1766 #[serde(skip_serializing_if = "Option::is_none")]
1768 pub encrypted_content: Option<String>,
1769
1770 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1773 pub summary: Vec<String>,
1774
1775 #[serde(skip_serializing_if = "Option::is_none")]
1777 pub token_count: Option<u32>,
1778}
1779
1780#[derive(Debug, Clone, Serialize, Deserialize)]
1786#[cfg_attr(feature = "openapi", derive(ToSchema))]
1787pub struct TurnStartedData {
1788 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1790 pub turn_id: TurnId,
1791
1792 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
1794 pub input_message_id: MessageId,
1795
1796 #[serde(skip_serializing_if = "Option::is_none")]
1798 pub input_content: Option<String>,
1799}
1800
1801#[derive(Debug, Clone, Serialize, Deserialize)]
1803#[cfg_attr(feature = "openapi", derive(ToSchema))]
1804pub struct TurnCompletedData {
1805 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1807 pub turn_id: TurnId,
1808
1809 pub iterations: u32,
1811
1812 #[serde(skip_serializing_if = "Option::is_none")]
1814 pub duration_ms: Option<u64>,
1815
1816 #[serde(skip_serializing_if = "Option::is_none")]
1818 pub usage: Option<TokenUsage>,
1819
1820 #[serde(skip_serializing_if = "Option::is_none")]
1822 pub input_content: Option<String>,
1823
1824 #[serde(skip_serializing_if = "Option::is_none")]
1826 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "message_01933b5a00007000800000000000001"))]
1827 pub final_message_id: Option<MessageId>,
1828
1829 #[serde(skip_serializing_if = "Option::is_none")]
1831 pub final_answer_preview: Option<String>,
1832
1833 #[serde(skip_serializing_if = "Option::is_none")]
1835 pub time_to_first_token_ms: Option<u64>,
1836
1837 #[serde(skip_serializing_if = "Option::is_none")]
1839 pub tool_call_count: Option<u32>,
1840
1841 #[serde(skip_serializing_if = "Option::is_none")]
1843 pub llm_call_count: Option<u32>,
1844
1845 #[serde(skip_serializing_if = "Option::is_none")]
1847 pub status: Option<String>,
1848}
1849
1850#[derive(Debug, Clone, Serialize, Deserialize)]
1852#[cfg_attr(feature = "openapi", derive(ToSchema))]
1853pub struct TurnFailedData {
1854 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1856 pub turn_id: TurnId,
1857
1858 pub error: String,
1860
1861 #[serde(default, skip_serializing_if = "Option::is_none")]
1863 pub error_code: Option<String>,
1864
1865 #[serde(default, skip_serializing_if = "Option::is_none")]
1867 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
1868 pub error_fields: Option<UserFacingErrorFields>,
1869}
1870
1871#[derive(Debug, Clone, Serialize, Deserialize)]
1873#[cfg_attr(feature = "openapi", derive(ToSchema))]
1874pub struct TurnCancelledData {
1875 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1877 pub turn_id: TurnId,
1878
1879 #[serde(skip_serializing_if = "Option::is_none")]
1881 pub reason: Option<String>,
1882
1883 #[serde(skip_serializing_if = "Option::is_none")]
1885 pub usage: Option<TokenUsage>,
1886}
1887
1888#[derive(Debug, Clone, Serialize, Deserialize)]
1894#[cfg_attr(feature = "openapi", derive(ToSchema))]
1895pub struct SessionStartedData {
1896 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "harness_01933b5a00007000800000000000001"))]
1898 pub harness_id: HarnessId,
1899
1900 #[serde(skip_serializing_if = "Option::is_none")]
1902 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agent_01933b5a00007000800000000000001"))]
1903 pub agent_id: Option<AgentId>,
1904
1905 #[serde(skip_serializing_if = "Option::is_none")]
1907 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
1908 pub model_id: Option<ModelId>,
1909}
1910
1911#[derive(Debug, Clone, Serialize, Deserialize)]
1913#[cfg_attr(feature = "openapi", derive(ToSchema))]
1914pub struct SessionActivatedData {
1915 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1917 pub turn_id: TurnId,
1918
1919 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
1921 pub input_message_id: MessageId,
1922}
1923
1924#[derive(Debug, Clone, Serialize, Deserialize)]
1926#[cfg_attr(feature = "openapi", derive(ToSchema))]
1927pub struct SessionIdledData {
1928 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1930 pub turn_id: TurnId,
1931
1932 #[serde(skip_serializing_if = "Option::is_none")]
1934 pub iterations: Option<u32>,
1935
1936 #[serde(skip_serializing_if = "Option::is_none")]
1938 pub usage: Option<TokenUsage>,
1939}
1940
1941#[derive(Debug, Clone, Serialize, Deserialize)]
1947#[cfg_attr(feature = "openapi", derive(ToSchema))]
1948pub struct SubagentEventData {
1949 #[cfg_attr(feature = "openapi", schema(value_type = String))]
1951 pub subagent_session_id: SessionId,
1952 pub subagent_name: String,
1954 pub task: String,
1956 pub status: String,
1958 #[serde(skip_serializing_if = "Option::is_none")]
1960 pub result: Option<String>,
1961 #[serde(skip_serializing_if = "Option::is_none")]
1963 pub error: Option<String>,
1964}
1965
1966impl From<SubagentEventData> for EventData {
1967 fn from(data: SubagentEventData) -> Self {
1968 match data.status.as_str() {
1969 "completed" => EventData::SubagentCompleted(data),
1970 "failed" => EventData::SubagentFailed(data),
1971 "cancelled" => EventData::SubagentCancelled(data),
1972 _ => EventData::SubagentSpawned(data),
1973 }
1974 }
1975}
1976
1977#[derive(Debug, Clone, Serialize, Deserialize)]
1983#[cfg_attr(feature = "openapi", derive(ToSchema))]
1984#[serde(rename_all = "snake_case")]
1985pub enum CompactionReason {
1986 ProactiveBudget,
1988 RequestTooLarge,
1990 Manual,
1992}
1993
1994impl std::fmt::Display for CompactionReason {
1995 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1996 match self {
1997 Self::ProactiveBudget => write!(f, "proactive_budget"),
1998 Self::RequestTooLarge => write!(f, "request_too_large"),
1999 Self::Manual => write!(f, "manual"),
2000 }
2001 }
2002}
2003
2004#[derive(Debug, Clone, Serialize, Deserialize)]
2006#[cfg_attr(feature = "openapi", derive(ToSchema))]
2007pub struct ContextCompactingData {
2008 pub reason: CompactionReason,
2010 pub strategy: String,
2012 pub messages_before: usize,
2014}
2015
2016#[derive(Debug, Clone, Serialize, Deserialize)]
2018#[cfg_attr(feature = "openapi", derive(ToSchema))]
2019pub struct CompactionStepData {
2020 pub strategy: String,
2022 pub messages_after: usize,
2024 pub duration_ms: u64,
2026}
2027
2028#[derive(Debug, Clone, Serialize, Deserialize)]
2030#[cfg_attr(feature = "openapi", derive(ToSchema))]
2031pub struct ContextCompactedData {
2032 pub strategy_used: String,
2034 pub messages_before: usize,
2036 pub messages_after: usize,
2038 pub duration_ms: u64,
2040 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2042 pub steps: Vec<CompactionStepData>,
2043}
2044
2045#[derive(Debug, Clone, Serialize, Deserialize)]
2051#[cfg_attr(feature = "openapi", derive(ToSchema))]
2052pub struct FileWrittenData {
2053 pub path: String,
2055 pub operation: String,
2057 pub size_bytes: i64,
2059 pub created: bool,
2061}
2062
2063pub const FILE_OP_CREATE: &str = "create";
2065pub const FILE_OP_UPDATE: &str = "update";
2066
2067#[derive(Debug, Clone, Serialize, Deserialize)]
2073#[cfg_attr(feature = "openapi", derive(ToSchema))]
2074pub struct BudgetEventData {
2075 pub budget_id: String,
2077 pub balance: f64,
2079 pub limit: f64,
2081 pub currency: String,
2083 #[serde(skip_serializing_if = "Option::is_none")]
2085 pub message: Option<String>,
2086 #[serde(skip_serializing_if = "Option::is_none")]
2088 pub soft_limit: Option<f64>,
2089}
2090
2091#[derive(Debug, Clone, Serialize, Deserialize)]
2097#[cfg_attr(feature = "openapi", derive(ToSchema))]
2098pub struct VoiceSessionStartedData {
2099 #[cfg_attr(
2101 feature = "openapi",
2102 schema(example = "voice_01933b5a00007000800000000000001")
2103 )]
2104 pub voice_connection_id: String,
2105 #[cfg_attr(feature = "openapi", schema(example = "gpt-realtime"))]
2107 pub model: String,
2108 #[cfg_attr(feature = "openapi", schema(example = "alloy"))]
2110 pub voice: String,
2111 #[cfg_attr(feature = "openapi", schema(example = "medium"))]
2113 pub reasoning_effort: String,
2114 #[cfg_attr(feature = "openapi", schema(example = "webrtc"))]
2116 pub transport: String,
2117}
2118
2119#[derive(Debug, Clone, Serialize, Deserialize)]
2121#[cfg_attr(feature = "openapi", derive(ToSchema))]
2122pub struct VoiceTranscriptData {
2123 pub voice_connection_id: String,
2125 #[serde(default, skip_serializing_if = "Option::is_none")]
2127 pub item_id: Option<String>,
2128 #[serde(default, skip_serializing_if = "Option::is_none")]
2130 pub response_id: Option<String>,
2131 #[serde(default, skip_serializing_if = "Option::is_none")]
2133 pub phase: Option<String>,
2134 #[serde(default, skip_serializing_if = "String::is_empty")]
2136 pub delta: String,
2137 pub accumulated: String,
2139}
2140
2141#[derive(Debug, Clone, Serialize, Deserialize)]
2143#[cfg_attr(feature = "openapi", derive(ToSchema))]
2144pub struct VoiceSessionEndedData {
2145 #[cfg_attr(
2147 feature = "openapi",
2148 schema(example = "voice_01933b5a00007000800000000000001")
2149 )]
2150 pub voice_connection_id: String,
2151 #[serde(default, skip_serializing_if = "Option::is_none")]
2153 #[cfg_attr(
2154 feature = "openapi",
2155 schema(example = "User hung up after refund confirmed.")
2156 )]
2157 pub reason: Option<String>,
2158 #[serde(default, skip_serializing_if = "Option::is_none")]
2161 #[cfg_attr(feature = "openapi", schema(example = 184_500_u64))]
2162 pub duration_ms: Option<u64>,
2163}
2164
2165#[derive(Debug, Clone, Serialize, Deserialize)]
2167#[cfg_attr(feature = "openapi", derive(ToSchema))]
2168pub struct VoiceSessionFailedData {
2169 #[cfg_attr(
2171 feature = "openapi",
2172 schema(example = "voice_01933b5a00007000800000000000001")
2173 )]
2174 pub voice_connection_id: String,
2175 #[cfg_attr(
2177 feature = "openapi",
2178 schema(example = "realtime provider closed stream: 1011 internal_error")
2179 )]
2180 pub error: String,
2181}
2182
2183#[derive(Debug, Clone, Serialize, Deserialize)]
2226#[serde(untagged)]
2227#[cfg_attr(feature = "openapi", derive(ToSchema))]
2228#[cfg_attr(feature = "openapi", schema(
2229 title = "EventData",
2230 description = "Event-specific payload. The schema depends on the event type field.",
2231 example = json!({"message": {"id": "...", "role": "user", "content": []}})
2232))]
2233pub enum EventData {
2234 InputMessage(InputMessageData),
2236
2237 OutputMessageDelta(OutputMessageDeltaData),
2243 OutputMessageStarted(OutputMessageStartedData),
2244 OutputMessageReplaced(OutputMessageReplacedData),
2248 OutputMessageCompleted(OutputMessageCompletedData),
2249
2250 TurnStarted(TurnStartedData),
2252 TurnCompleted(TurnCompletedData),
2253 TurnFailed(TurnFailedData),
2254
2255 ReasonStarted(ReasonStartedData),
2257 ReasonCompleted(ReasonCompletedData),
2258 CapabilityUsage(CapabilityUsageData),
2259 ActStarted(ActStartedData),
2260 ActCompleted(ActCompletedData),
2261 ToolStarted(ToolStartedData),
2262 ToolCompleted(ToolCompletedData),
2263 ToolProgress(ToolProgressData),
2264 ToolOutputDelta(ToolOutputDeltaData),
2265 ToolCallRequested(ToolCallRequestedData),
2266
2267 LlmGeneration(LlmGenerationData),
2269
2270 ReasonThinkingDelta(ReasonThinkingDeltaData),
2282 ReasonItem(ReasonItemData),
2283 ReasonThinkingStarted(ReasonThinkingStartedData),
2284 ReasonThinkingCompleted(ReasonThinkingCompletedData),
2285
2286 TurnCancelled(TurnCancelledData),
2290
2291 SessionStarted(SessionStartedData),
2293 SessionActivated(SessionActivatedData),
2294 SessionIdled(SessionIdledData),
2295
2296 SubagentSpawned(SubagentEventData),
2298 SubagentCompleted(SubagentEventData),
2299 SubagentFailed(SubagentEventData),
2300 SubagentCancelled(SubagentEventData),
2301
2302 ContextCompacting(ContextCompactingData),
2304 ContextCompacted(ContextCompactedData),
2305
2306 FileWritten(FileWrittenData),
2308
2309 BudgetWarning(BudgetEventData),
2311 BudgetPaused(BudgetEventData),
2312 BudgetExhausted(BudgetEventData),
2313 BudgetResumed(BudgetEventData),
2314
2315 VoiceSessionStarted(VoiceSessionStartedData),
2317 VoiceInputTranscriptDelta(VoiceTranscriptData),
2318 VoiceInputTranscriptCompleted(VoiceTranscriptData),
2319 VoiceOutputTranscriptDelta(VoiceTranscriptData),
2320 VoiceOutputTranscriptCompleted(VoiceTranscriptData),
2321 VoiceSessionEnded(VoiceSessionEndedData),
2322 VoiceSessionFailed(VoiceSessionFailedData),
2323
2324 #[serde(skip)]
2328 Unsupported {
2329 event_type: String,
2331 data: serde_json::Value,
2333 },
2334}
2335
2336impl EventData {
2337 pub fn event_type(&self) -> &'static str {
2340 match self {
2341 EventData::InputMessage(_) => INPUT_MESSAGE,
2342 EventData::OutputMessageStarted(_) => OUTPUT_MESSAGE_STARTED,
2343 EventData::OutputMessageDelta(_) => OUTPUT_MESSAGE_DELTA,
2344 EventData::OutputMessageReplaced(_) => OUTPUT_MESSAGE_REPLACED,
2345 EventData::OutputMessageCompleted(_) => OUTPUT_MESSAGE_COMPLETED,
2346 EventData::TurnStarted(_) => TURN_STARTED,
2347 EventData::TurnCompleted(_) => TURN_COMPLETED,
2348 EventData::TurnFailed(_) => TURN_FAILED,
2349 EventData::TurnCancelled(_) => TURN_CANCELLED,
2350 EventData::ReasonStarted(_) => REASON_STARTED,
2351 EventData::ReasonCompleted(_) => REASON_COMPLETED,
2352 EventData::CapabilityUsage(_) => CAPABILITY_USAGE,
2353 EventData::ActStarted(_) => ACT_STARTED,
2354 EventData::ActCompleted(_) => ACT_COMPLETED,
2355 EventData::ToolStarted(_) => TOOL_STARTED,
2356 EventData::ToolCompleted(_) => TOOL_COMPLETED,
2357 EventData::ToolProgress(_) => TOOL_PROGRESS,
2358 EventData::ToolOutputDelta(_) => TOOL_OUTPUT_DELTA,
2359 EventData::ToolCallRequested(_) => TOOL_CALL_REQUESTED,
2360 EventData::LlmGeneration(_) => LLM_GENERATION,
2361 EventData::ReasonThinkingDelta(_) => REASON_THINKING_DELTA,
2362 EventData::ReasonThinkingStarted(_) => REASON_THINKING_STARTED,
2363 EventData::ReasonThinkingCompleted(_) => REASON_THINKING_COMPLETED,
2364 EventData::ReasonItem(_) => REASON_ITEM,
2365 EventData::SessionStarted(_) => SESSION_STARTED,
2366 EventData::SessionActivated(_) => SESSION_ACTIVATED,
2367 EventData::SessionIdled(_) => SESSION_IDLED,
2368 EventData::SubagentSpawned(_) => SUBAGENT_SPAWNED,
2369 EventData::SubagentCompleted(_) => SUBAGENT_COMPLETED,
2370 EventData::SubagentFailed(_) => SUBAGENT_FAILED,
2371 EventData::SubagentCancelled(_) => SUBAGENT_CANCELLED,
2372 EventData::ContextCompacting(_) => CONTEXT_COMPACTING,
2373 EventData::ContextCompacted(_) => CONTEXT_COMPACTED,
2374 EventData::FileWritten(_) => FILE_WRITTEN,
2375 EventData::BudgetWarning(_) => BUDGET_WARNING,
2376 EventData::BudgetPaused(_) => BUDGET_PAUSED,
2377 EventData::BudgetExhausted(_) => BUDGET_EXHAUSTED,
2378 EventData::BudgetResumed(_) => BUDGET_RESUMED,
2379 EventData::VoiceSessionStarted(_) => VOICE_SESSION_STARTED,
2380 EventData::VoiceInputTranscriptDelta(_) => VOICE_INPUT_TRANSCRIPT_DELTA,
2381 EventData::VoiceInputTranscriptCompleted(_) => VOICE_INPUT_TRANSCRIPT_COMPLETED,
2382 EventData::VoiceOutputTranscriptDelta(_) => VOICE_OUTPUT_TRANSCRIPT_DELTA,
2383 EventData::VoiceOutputTranscriptCompleted(_) => VOICE_OUTPUT_TRANSCRIPT_COMPLETED,
2384 EventData::VoiceSessionEnded(_) => VOICE_SESSION_ENDED,
2385 EventData::VoiceSessionFailed(_) => VOICE_SESSION_FAILED,
2386 EventData::Unsupported { .. } => "unsupported",
2387 }
2388 }
2389
2390 pub fn is_unsupported(&self) -> bool {
2393 matches!(self, EventData::Unsupported { .. })
2394 }
2395
2396 pub fn unsupported(event_type: String, data: serde_json::Value) -> Self {
2399 tracing::warn!(
2400 event_type = %event_type,
2401 "Encountered unsupported event type - will be filtered from API responses"
2402 );
2403 EventData::Unsupported { event_type, data }
2404 }
2405}
2406
2407pub fn deserialize_event_data(event_type: &str, data: serde_json::Value) -> EventData {
2421 let result =
2422 match event_type {
2423 INPUT_MESSAGE => serde_json::from_value::<InputMessageData>(data.clone())
2424 .map(EventData::InputMessage),
2425 OUTPUT_MESSAGE_STARTED => {
2426 serde_json::from_value::<OutputMessageStartedData>(data.clone())
2427 .map(EventData::OutputMessageStarted)
2428 }
2429 OUTPUT_MESSAGE_DELTA => serde_json::from_value::<OutputMessageDeltaData>(data.clone())
2430 .map(EventData::OutputMessageDelta),
2431 OUTPUT_MESSAGE_REPLACED => {
2432 serde_json::from_value::<OutputMessageReplacedData>(data.clone())
2433 .map(EventData::OutputMessageReplaced)
2434 }
2435 OUTPUT_MESSAGE_COMPLETED => {
2436 serde_json::from_value::<OutputMessageCompletedData>(data.clone())
2437 .map(EventData::OutputMessageCompleted)
2438 }
2439 TURN_STARTED => {
2440 serde_json::from_value::<TurnStartedData>(data.clone()).map(EventData::TurnStarted)
2441 }
2442 TURN_COMPLETED => serde_json::from_value::<TurnCompletedData>(data.clone())
2443 .map(EventData::TurnCompleted),
2444 TURN_FAILED => {
2445 serde_json::from_value::<TurnFailedData>(data.clone()).map(EventData::TurnFailed)
2446 }
2447 TURN_CANCELLED => serde_json::from_value::<TurnCancelledData>(data.clone())
2448 .map(EventData::TurnCancelled),
2449 REASON_STARTED => serde_json::from_value::<ReasonStartedData>(data.clone())
2450 .map(EventData::ReasonStarted),
2451 REASON_COMPLETED => serde_json::from_value::<ReasonCompletedData>(data.clone())
2452 .map(EventData::ReasonCompleted),
2453 CAPABILITY_USAGE => serde_json::from_value::<CapabilityUsageData>(data.clone())
2454 .map(EventData::CapabilityUsage),
2455 ACT_STARTED => {
2456 serde_json::from_value::<ActStartedData>(data.clone()).map(EventData::ActStarted)
2457 }
2458 ACT_COMPLETED => serde_json::from_value::<ActCompletedData>(data.clone())
2459 .map(EventData::ActCompleted),
2460 TOOL_STARTED => {
2461 serde_json::from_value::<ToolStartedData>(data.clone()).map(EventData::ToolStarted)
2462 }
2463 TOOL_COMPLETED => serde_json::from_value::<ToolCompletedData>(data.clone())
2464 .map(EventData::ToolCompleted),
2465 TOOL_PROGRESS => serde_json::from_value::<ToolProgressData>(data.clone())
2466 .map(EventData::ToolProgress),
2467 TOOL_OUTPUT_DELTA => serde_json::from_value::<ToolOutputDeltaData>(data.clone())
2468 .map(EventData::ToolOutputDelta),
2469 TOOL_CALL_REQUESTED => serde_json::from_value::<ToolCallRequestedData>(data.clone())
2470 .map(EventData::ToolCallRequested),
2471 LLM_GENERATION => serde_json::from_value::<LlmGenerationData>(data.clone())
2472 .map(EventData::LlmGeneration),
2473 REASON_THINKING_STARTED => {
2474 serde_json::from_value::<ReasonThinkingStartedData>(data.clone())
2475 .map(EventData::ReasonThinkingStarted)
2476 }
2477 REASON_THINKING_DELTA => {
2478 serde_json::from_value::<ReasonThinkingDeltaData>(data.clone())
2479 .map(EventData::ReasonThinkingDelta)
2480 }
2481 REASON_THINKING_COMPLETED => {
2482 serde_json::from_value::<ReasonThinkingCompletedData>(data.clone())
2483 .map(EventData::ReasonThinkingCompleted)
2484 }
2485 REASON_ITEM => {
2486 serde_json::from_value::<ReasonItemData>(data.clone()).map(EventData::ReasonItem)
2487 }
2488 SESSION_STARTED => serde_json::from_value::<SessionStartedData>(data.clone())
2489 .map(EventData::SessionStarted),
2490 SESSION_ACTIVATED => serde_json::from_value::<SessionActivatedData>(data.clone())
2491 .map(EventData::SessionActivated),
2492 SESSION_IDLED => serde_json::from_value::<SessionIdledData>(data.clone())
2493 .map(EventData::SessionIdled),
2494 CONTEXT_COMPACTING => serde_json::from_value::<ContextCompactingData>(data.clone())
2495 .map(EventData::ContextCompacting),
2496 CONTEXT_COMPACTED => serde_json::from_value::<ContextCompactedData>(data.clone())
2497 .map(EventData::ContextCompacted),
2498 FILE_WRITTEN => {
2499 serde_json::from_value::<FileWrittenData>(data.clone()).map(EventData::FileWritten)
2500 }
2501 BUDGET_WARNING => serde_json::from_value::<BudgetEventData>(data.clone())
2502 .map(EventData::BudgetWarning),
2503 BUDGET_PAUSED => {
2504 serde_json::from_value::<BudgetEventData>(data.clone()).map(EventData::BudgetPaused)
2505 }
2506 BUDGET_EXHAUSTED => serde_json::from_value::<BudgetEventData>(data.clone())
2507 .map(EventData::BudgetExhausted),
2508 BUDGET_RESUMED => serde_json::from_value::<BudgetEventData>(data.clone())
2509 .map(EventData::BudgetResumed),
2510 VOICE_SESSION_STARTED => {
2511 serde_json::from_value::<VoiceSessionStartedData>(data.clone())
2512 .map(EventData::VoiceSessionStarted)
2513 }
2514 VOICE_INPUT_TRANSCRIPT_DELTA => {
2515 serde_json::from_value::<VoiceTranscriptData>(data.clone())
2516 .map(EventData::VoiceInputTranscriptDelta)
2517 }
2518 VOICE_INPUT_TRANSCRIPT_COMPLETED => {
2519 serde_json::from_value::<VoiceTranscriptData>(data.clone())
2520 .map(EventData::VoiceInputTranscriptCompleted)
2521 }
2522 VOICE_OUTPUT_TRANSCRIPT_DELTA => {
2523 serde_json::from_value::<VoiceTranscriptData>(data.clone())
2524 .map(EventData::VoiceOutputTranscriptDelta)
2525 }
2526 VOICE_OUTPUT_TRANSCRIPT_COMPLETED => {
2527 serde_json::from_value::<VoiceTranscriptData>(data.clone())
2528 .map(EventData::VoiceOutputTranscriptCompleted)
2529 }
2530 VOICE_SESSION_ENDED => serde_json::from_value::<VoiceSessionEndedData>(data.clone())
2531 .map(EventData::VoiceSessionEnded),
2532 VOICE_SESSION_FAILED => serde_json::from_value::<VoiceSessionFailedData>(data.clone())
2533 .map(EventData::VoiceSessionFailed),
2534 SUBAGENT_SPAWNED => serde_json::from_value::<SubagentEventData>(data.clone())
2535 .map(EventData::SubagentSpawned),
2536 SUBAGENT_COMPLETED => serde_json::from_value::<SubagentEventData>(data.clone())
2537 .map(EventData::SubagentCompleted),
2538 SUBAGENT_FAILED => serde_json::from_value::<SubagentEventData>(data.clone())
2539 .map(EventData::SubagentFailed),
2540 SUBAGENT_CANCELLED => serde_json::from_value::<SubagentEventData>(data.clone())
2541 .map(EventData::SubagentCancelled),
2542 _ => {
2543 return EventData::unsupported(event_type.to_string(), data);
2545 }
2546 };
2547
2548 result.unwrap_or_else(|e| {
2550 tracing::warn!(
2551 event_type = %event_type,
2552 error = %e,
2553 "Failed to deserialize known event type - treating as unsupported"
2554 );
2555 EventData::Unsupported {
2556 event_type: event_type.to_string(),
2557 data,
2558 }
2559 })
2560}
2561
2562macro_rules! impl_from_event_data {
2566 ($($data_type:ty => $variant:ident),* $(,)?) => {
2567 $(
2568 impl From<$data_type> for EventData {
2569 fn from(data: $data_type) -> Self {
2570 EventData::$variant(data)
2571 }
2572 }
2573 )*
2574 };
2575}
2576
2577impl_from_event_data! {
2579 InputMessageData => InputMessage,
2580 OutputMessageStartedData => OutputMessageStarted,
2581 OutputMessageDeltaData => OutputMessageDelta,
2582 OutputMessageReplacedData => OutputMessageReplaced,
2583 OutputMessageCompletedData => OutputMessageCompleted,
2584 TurnStartedData => TurnStarted,
2585 TurnCompletedData => TurnCompleted,
2586 TurnFailedData => TurnFailed,
2587 TurnCancelledData => TurnCancelled,
2588 ReasonStartedData => ReasonStarted,
2589 ReasonCompletedData => ReasonCompleted,
2590 CapabilityUsageData => CapabilityUsage,
2591 ActStartedData => ActStarted,
2592 ActCompletedData => ActCompleted,
2593 ToolStartedData => ToolStarted,
2594 ToolCompletedData => ToolCompleted,
2595 ToolProgressData => ToolProgress,
2596 ToolOutputDeltaData => ToolOutputDelta,
2597 ToolCallRequestedData => ToolCallRequested,
2598 LlmGenerationData => LlmGeneration,
2599 ReasonThinkingStartedData => ReasonThinkingStarted,
2600 ReasonThinkingDeltaData => ReasonThinkingDelta,
2601 ReasonThinkingCompletedData => ReasonThinkingCompleted,
2602 ReasonItemData => ReasonItem,
2603 SessionStartedData => SessionStarted,
2604 SessionActivatedData => SessionActivated,
2605 SessionIdledData => SessionIdled,
2606 ContextCompactingData => ContextCompacting,
2607 ContextCompactedData => ContextCompacted,
2608 FileWrittenData => FileWritten,
2609 VoiceSessionStartedData => VoiceSessionStarted,
2610 VoiceSessionEndedData => VoiceSessionEnded,
2611 VoiceSessionFailedData => VoiceSessionFailed,
2612}
2613
2614impl EventData {
2615 pub fn voice_transcript_event(data: VoiceTranscriptData, event_type: &str) -> Self {
2616 match event_type {
2617 VOICE_INPUT_TRANSCRIPT_DELTA => EventData::VoiceInputTranscriptDelta(data),
2618 VOICE_INPUT_TRANSCRIPT_COMPLETED => EventData::VoiceInputTranscriptCompleted(data),
2619 VOICE_OUTPUT_TRANSCRIPT_DELTA => EventData::VoiceOutputTranscriptDelta(data),
2620 VOICE_OUTPUT_TRANSCRIPT_COMPLETED => EventData::VoiceOutputTranscriptCompleted(data),
2621 _ => panic!("Unknown voice transcript event type: {event_type}"),
2622 }
2623 }
2624}
2625
2626impl EventData {
2629 pub fn budget_event(data: BudgetEventData, event_type: &str) -> Self {
2630 match event_type {
2631 BUDGET_WARNING => EventData::BudgetWarning(data),
2632 BUDGET_PAUSED => EventData::BudgetPaused(data),
2633 BUDGET_EXHAUSTED => EventData::BudgetExhausted(data),
2634 BUDGET_RESUMED => EventData::BudgetResumed(data),
2635 _ => panic!("Unknown budget event type: {event_type}"),
2636 }
2637 }
2638}
2639
2640#[derive(Debug, Clone, Serialize)]
2650#[cfg_attr(feature = "openapi", derive(ToSchema))]
2651pub struct EventRequest {
2652 #[serde(rename = "type")]
2654 pub event_type: String,
2655
2656 pub ts: DateTime<Utc>,
2658
2659 pub session_id: SessionId,
2661
2662 pub context: EventContext,
2664
2665 pub data: EventData,
2667
2668 #[serde(skip_serializing_if = "Option::is_none")]
2670 pub metadata: Option<serde_json::Value>,
2671
2672 #[serde(skip_serializing_if = "Option::is_none")]
2674 pub tags: Option<Vec<String>>,
2675}
2676
2677#[derive(Debug, Deserialize)]
2678struct RawEventRequest {
2679 #[serde(rename = "type")]
2680 event_type: String,
2681 ts: DateTime<Utc>,
2682 session_id: SessionId,
2683 context: EventContext,
2684 data: serde_json::Value,
2685 metadata: Option<serde_json::Value>,
2686 tags: Option<Vec<String>>,
2687}
2688
2689impl<'de> Deserialize<'de> for EventRequest {
2690 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2691 where
2692 D: Deserializer<'de>,
2693 {
2694 let raw = RawEventRequest::deserialize(deserializer)?;
2695 let data = deserialize_event_data(&raw.event_type, raw.data);
2696 Ok(Self {
2697 event_type: raw.event_type,
2698 ts: raw.ts,
2699 session_id: raw.session_id,
2700 context: raw.context,
2701 data,
2702 metadata: raw.metadata,
2703 tags: raw.tags,
2704 })
2705 }
2706}
2707
2708impl EventRequest {
2709 pub fn new(session_id: SessionId, context: EventContext, data: impl Into<EventData>) -> Self {
2713 let data = data.into();
2714 let event_type = data.event_type().to_string();
2715 Self {
2716 event_type,
2717 ts: Utc::now(),
2718 session_id,
2719 context,
2720 data,
2721 metadata: None,
2722 tags: None,
2723 }
2724 }
2725
2726 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
2728 self.metadata = Some(metadata);
2729 self
2730 }
2731
2732 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
2734 self.tags = Some(tags);
2735 self
2736 }
2737
2738 pub fn is_ephemeral(&self) -> bool {
2746 is_ephemeral_event_type(&self.event_type)
2747 }
2748
2749 pub fn into_event(self, id: EventId, sequence: i32) -> Event {
2751 Event {
2752 id,
2753 event_type: self.event_type,
2754 ts: self.ts,
2755 session_id: self.session_id,
2756 context: self.context,
2757 data: self.data,
2758 metadata: self.metadata,
2759 tags: self.tags,
2760 sequence: Some(sequence),
2761 }
2762 }
2763}
2764
2765pub struct EventBuilder {
2771 session_id: SessionId,
2772 context: EventContext,
2773}
2774
2775impl EventBuilder {
2776 pub fn new(session_id: SessionId) -> Self {
2777 Self {
2778 session_id,
2779 context: EventContext::empty(),
2780 }
2781 }
2782
2783 pub fn with_turn(mut self, turn_id: TurnId, input_message_id: MessageId) -> Self {
2784 self.context.turn_id = Some(turn_id);
2785 self.context.input_message_id = Some(input_message_id);
2786 self
2787 }
2788
2789 pub fn with_exec(mut self, exec_id: ExecId) -> Self {
2790 self.context.exec_id = Some(exec_id);
2791 self
2792 }
2793
2794 pub fn build(self, data: impl Into<EventData>) -> Event {
2795 Event::new(self.session_id, self.context, data)
2796 }
2797}
2798
2799#[cfg(test)]
2804mod tests {
2805 use super::*;
2806 use crate::llm_driver_registry::PromptCacheStrategy;
2807 use serde_json::json;
2808 use std::collections::HashMap;
2809
2810 #[test]
2811 fn test_event_creation() {
2812 let session_id = SessionId::new();
2813 let context = EventContext::empty();
2814 let data = InputMessageData::new(Message::user("test"));
2815
2816 let event = Event::new(session_id, context, data);
2817
2818 assert_eq!(event.event_type, "input.message");
2819 assert_eq!(event.session_uuid(), session_id.uuid());
2820 assert!(event.is_input_event());
2821 assert!(event.is_message_event());
2822 }
2823
2824 #[test]
2825 fn test_event_context_from_atom_context() {
2826 let session_id = SessionId::new();
2827 let turn_id = TurnId::new();
2828 let input_message_id = MessageId::new();
2829
2830 let atom_ctx = AtomContext::new(session_id, turn_id, input_message_id);
2831 let context = EventContext::from_atom_context(&atom_ctx);
2832
2833 assert_eq!(context.turn_id, Some(turn_id));
2834 assert_eq!(context.input_message_id, Some(input_message_id));
2835 assert_eq!(context.exec_id, Some(atom_ctx.exec_id));
2836 }
2837
2838 #[test]
2839 fn test_event_serialization() {
2840 let session_id = SessionId::new();
2841 let context = EventContext::empty();
2842 let event = Event::new(
2843 session_id,
2844 context,
2845 InputMessageData::new(Message::user("test")),
2846 );
2847
2848 let json = serde_json::to_string(&event).unwrap();
2849
2850 assert!(json.contains("\"type\":\"input.message\""));
2851 assert!(json.contains("\"session_id\""));
2852 assert!(json.contains("\"context\""));
2853 assert!(json.contains("\"data\""));
2854 }
2855
2856 #[test]
2857 fn test_event_builder() {
2858 let session_id = SessionId::new();
2859 let turn_id = TurnId::new();
2860 let input_message_id = MessageId::new();
2861 let exec_id = ExecId::new();
2862
2863 let event = EventBuilder::new(session_id)
2864 .with_turn(turn_id, input_message_id)
2865 .with_exec(exec_id)
2866 .build(ReasonStartedData {
2867 harness_id: HarnessId::from_seed(1),
2868 agent_id: Some(AgentId::new()),
2869 metadata: Some(ModelMetadata {
2870 model: "gpt-4o".to_string(),
2871 model_id: None,
2872 provider_id: None,
2873 }),
2874 });
2875
2876 assert_eq!(event.event_type, "reason.started");
2877 assert_eq!(event.session_id, session_id);
2878 assert_eq!(event.context.turn_id, Some(turn_id));
2879 assert_eq!(event.context.exec_id, Some(exec_id));
2880 }
2881
2882 #[test]
2883 fn test_reason_completed_data() {
2884 let data = ReasonCompletedData::success("Hello world", true, 2, Some(1000), None);
2885 assert!(data.success);
2886 assert_eq!(data.text_preview, Some("Hello world".to_string()));
2887 assert!(data.has_tool_calls);
2888 assert_eq!(data.tool_call_count, 2);
2889 assert_eq!(data.duration_ms, Some(1000));
2890 assert!(data.usage.is_none());
2891
2892 let data = ReasonCompletedData::failure("Network error".to_string(), Some(500));
2893 assert!(!data.success);
2894 assert_eq!(data.error, Some("Network error".to_string()));
2895 assert_eq!(data.duration_ms, Some(500));
2896 }
2897
2898 #[test]
2899 fn test_input_output_event_types() {
2900 assert_eq!(INPUT_MESSAGE, "input.message");
2901 assert_eq!(OUTPUT_MESSAGE_STARTED, "output.message.started");
2902 assert_eq!(OUTPUT_MESSAGE_DELTA, "output.message.delta");
2903 assert_eq!(OUTPUT_MESSAGE_COMPLETED, "output.message.completed");
2904 }
2905
2906 #[test]
2907 fn test_turn_event_types() {
2908 assert_eq!(TURN_STARTED, "turn.started");
2909 assert_eq!(TURN_COMPLETED, "turn.completed");
2910 assert_eq!(TURN_FAILED, "turn.failed");
2911 assert_eq!(TURN_CANCELLED, "turn.cancelled");
2912 }
2913
2914 #[test]
2915 fn test_turn_cancelled_data() {
2916 let data = TurnCancelledData {
2917 turn_id: TurnId::from_uuid(Uuid::now_v7()),
2918 reason: Some("User requested cancellation".to_string()),
2919 usage: Some(TokenUsage::new(100, 50)),
2920 };
2921
2922 let event_data: EventData = data.into();
2923 assert_eq!(event_data.event_type(), TURN_CANCELLED);
2924 }
2925
2926 #[test]
2927 fn test_tool_event_types() {
2928 assert_eq!(TOOL_STARTED, "tool.started");
2929 assert_eq!(TOOL_COMPLETED, "tool.completed");
2930 }
2931
2932 #[test]
2933 fn test_llm_generation_event_type() {
2934 assert_eq!(LLM_GENERATION, "llm.generation");
2935 }
2936
2937 #[test]
2938 fn test_llm_generation_data_success() {
2939 let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")];
2940 let tools = vec![ToolDefinitionSummary {
2941 name: "get_weather".to_string(),
2942 display_name: None,
2943 category: None,
2944 capability_id: None,
2945 capability_name: None,
2946 description: "Get weather for a city".to_string(),
2947 }];
2948 let tool_calls = vec![];
2949 let data = LlmGenerationData::success(
2950 messages.clone(),
2951 tools,
2952 Some("Hi there!".to_string()),
2953 tool_calls,
2954 "gpt-4o".to_string(),
2955 Some("openai".to_string()),
2956 Some(TokenUsage {
2957 input_tokens: 10,
2958 output_tokens: 5,
2959 cache_read_tokens: None,
2960 cache_creation_tokens: None,
2961 actual_cost_usd: None,
2962 estimated_cost_usd: None,
2963 }),
2964 Some(100),
2965 Some(25), );
2967
2968 assert_eq!(data.messages.len(), 2);
2969 assert_eq!(data.tools.len(), 1);
2970 assert_eq!(data.tools[0].name, "get_weather");
2971 assert_eq!(data.output.text, Some("Hi there!".to_string()));
2972 assert!(data.output.tool_calls.is_empty());
2973 assert!(data.metadata.success);
2974 assert_eq!(data.metadata.model, "gpt-4o");
2975 assert_eq!(data.metadata.provider, Some("openai".to_string()));
2976 assert!(data.metadata.error.is_none());
2977 assert_eq!(data.metadata.finish_reasons, Some(vec!["stop".to_string()]));
2979 assert!(data.metadata.response_id.is_none());
2980 }
2981
2982 #[test]
2983 fn test_llm_generation_data_with_full_metadata() {
2984 let messages = vec![Message::user("Hello")];
2985 let data = LlmGenerationData::success_with_metadata(
2986 messages,
2987 vec![],
2988 Some("Hi!".to_string()),
2989 vec![],
2990 "claude-3-opus".to_string(),
2991 Some("anthropic".to_string()),
2992 Some(TokenUsage {
2993 input_tokens: 5,
2994 output_tokens: 3,
2995 cache_read_tokens: None,
2996 cache_creation_tokens: None,
2997 actual_cost_usd: None,
2998 estimated_cost_usd: None,
2999 }),
3000 Some(50),
3001 Some(25), Some(vec!["end_turn".to_string()]),
3003 Some("msg_12345".to_string()),
3004 );
3005
3006 assert!(data.metadata.success);
3007 assert_eq!(data.metadata.model, "claude-3-opus");
3008 assert_eq!(data.metadata.provider, Some("anthropic".to_string()));
3009 assert_eq!(data.metadata.time_to_first_token_ms, Some(25));
3010 assert_eq!(
3011 data.metadata.finish_reasons,
3012 Some(vec!["end_turn".to_string()])
3013 );
3014 assert_eq!(data.metadata.response_id, Some("msg_12345".to_string()));
3015 }
3016
3017 #[test]
3018 fn test_llm_generation_data_failure() {
3019 let messages = vec![Message::user("Hello")];
3020 let data = LlmGenerationData::failure(
3021 messages,
3022 vec![],
3023 "gpt-4o".to_string(),
3024 Some("openai".to_string()),
3025 "Rate limit exceeded".to_string(),
3026 Some(50),
3027 None, );
3029
3030 assert!(!data.metadata.success);
3031 assert_eq!(data.metadata.error, Some("Rate limit exceeded".to_string()));
3032 assert!(data.output.text.is_none());
3033 assert!(data.output.tool_calls.is_empty());
3034 }
3035
3036 #[test]
3037 fn test_llm_generation_event_data() {
3038 let data = LlmGenerationData::success(
3039 vec![Message::user("test")],
3040 vec![],
3041 Some("response".to_string()),
3042 vec![],
3043 "model".to_string(),
3044 None,
3045 None,
3046 None,
3047 None, );
3049
3050 let event_data: EventData = data.into();
3051 assert_eq!(event_data.event_type(), LLM_GENERATION);
3052 }
3053
3054 #[test]
3055 fn test_llm_generation_is_durable_not_ephemeral() {
3056 let session_id = SessionId::new();
3057 let data = LlmGenerationData::success(
3058 vec![Message::user("test")],
3059 vec![],
3060 Some("response".to_string()),
3061 vec![],
3062 "model".to_string(),
3063 None,
3064 None,
3065 None,
3066 None,
3067 );
3068
3069 let request = EventRequest::new(session_id, EventContext::empty(), data);
3070 assert!(!request.is_ephemeral());
3071 }
3072
3073 #[test]
3074 fn test_delta_events_are_ephemeral() {
3075 let session_id = SessionId::new();
3076 let turn_id = TurnId::new();
3077
3078 let output_delta = EventRequest::new(
3079 session_id,
3080 EventContext::empty(),
3081 OutputMessageDeltaData {
3082 turn_id,
3083 delta: "hel".to_string(),
3084 accumulated: "hel".to_string(),
3085 },
3086 );
3087 assert!(output_delta.is_ephemeral());
3088
3089 let thinking_delta = EventRequest::new(
3090 session_id,
3091 EventContext::empty(),
3092 ReasonThinkingDeltaData {
3093 turn_id,
3094 delta: "step".to_string(),
3095 accumulated: "step".to_string(),
3096 },
3097 );
3098 assert!(thinking_delta.is_ephemeral());
3099
3100 let tool_delta = EventRequest::new(
3101 session_id,
3102 EventContext::empty(),
3103 ToolOutputDeltaData {
3104 tool_call_id: "call_123".to_string(),
3105 tool_name: "bash".to_string(),
3106 delta: "line".to_string(),
3107 stream: "stdout".to_string(),
3108 },
3109 );
3110 assert!(tool_delta.is_ephemeral());
3111 }
3112
3113 #[test]
3114 fn test_llm_generation_data_with_request_options() {
3115 let mut provider_options = HashMap::new();
3116 provider_options.insert(
3117 "openai".to_string(),
3118 json!({ "previous_response_id": true }),
3119 );
3120
3121 let data = LlmGenerationData::success(
3122 vec![Message::user("Hello")],
3123 vec![],
3124 Some("Hi".to_string()),
3125 vec![],
3126 "gpt-5.4".to_string(),
3127 Some("openai".to_string()),
3128 None,
3129 Some(42),
3130 Some(12),
3131 )
3132 .with_request_options(LlmRequestOptions {
3133 prompt_cache: Some(LlmPromptCacheInfo {
3134 enabled: true,
3135 strategy: PromptCacheStrategy::Auto,
3136 provider_mode: Some("prompt_cache_key".to_string()),
3137 }),
3138 tool_search: Some(LlmToolSearchInfo {
3139 enabled: true,
3140 threshold: 8,
3141 }),
3142 provider_options,
3143 });
3144
3145 let json = serde_json::to_value(&data).unwrap();
3146 assert_eq!(
3147 json["metadata"]["request_options"]["prompt_cache"]["provider_mode"],
3148 "prompt_cache_key"
3149 );
3150 assert_eq!(
3151 json["metadata"]["request_options"]["tool_search"]["threshold"],
3152 8
3153 );
3154 assert_eq!(
3155 json["metadata"]["request_options"]["provider_options"]["openai"]["previous_response_id"],
3156 true
3157 );
3158 }
3159
3160 #[test]
3161 fn test_extended_thinking_event_types() {
3162 assert_eq!(REASON_THINKING_STARTED, "reason.thinking.started");
3163 assert_eq!(REASON_THINKING_DELTA, "reason.thinking.delta");
3164 assert_eq!(REASON_THINKING_COMPLETED, "reason.thinking.completed");
3165 }
3166
3167 #[test]
3168 fn test_output_message_started_data() {
3169 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3170 let data = OutputMessageStartedData {
3171 turn_id,
3172 model: Some("claude-4-opus".to_string()),
3173 iteration: None,
3174 };
3175
3176 let event_data: EventData = data.into();
3177 assert_eq!(event_data.event_type(), OUTPUT_MESSAGE_STARTED);
3178
3179 let json = serde_json::to_string(&event_data).unwrap();
3181 assert!(json.contains("turn_id"));
3182 assert!(json.contains("claude-4-opus"));
3183 }
3184
3185 #[test]
3186 fn test_output_message_started_data_without_model() {
3187 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3188 let data = OutputMessageStartedData {
3189 turn_id,
3190 model: None,
3191 iteration: None,
3192 };
3193
3194 let json = serde_json::to_string(&data).unwrap();
3196 assert!(!json.contains("model"));
3197 }
3198
3199 #[test]
3200 fn test_reason_thinking_started_data() {
3201 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3202 let data = ReasonThinkingStartedData {
3203 turn_id,
3204 model: Some("claude-4-opus".to_string()),
3205 };
3206
3207 let event_data: EventData = data.into();
3208 assert_eq!(event_data.event_type(), REASON_THINKING_STARTED);
3209
3210 let json = serde_json::to_string(&event_data).unwrap();
3212 assert!(json.contains("turn_id"));
3213 assert!(json.contains("claude-4-opus"));
3214 }
3215
3216 #[test]
3217 fn test_reason_thinking_delta_data() {
3218 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3219 let data = ReasonThinkingDeltaData {
3220 turn_id,
3221 delta: "thinking step 1".to_string(),
3222 accumulated: "thinking step 1".to_string(),
3223 };
3224
3225 let event_data: EventData = data.into();
3226 assert_eq!(event_data.event_type(), REASON_THINKING_DELTA);
3227
3228 let json = serde_json::to_string(&event_data).unwrap();
3230 assert!(json.contains("turn_id"));
3231 assert!(json.contains("delta"));
3232 assert!(json.contains("accumulated"));
3233 }
3234
3235 #[test]
3236 fn test_reason_thinking_completed_data() {
3237 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3238 let data = ReasonThinkingCompletedData {
3239 turn_id,
3240 thinking: "Full thinking content here".to_string(),
3241 };
3242
3243 let event_data: EventData = data.into();
3244 assert_eq!(event_data.event_type(), REASON_THINKING_COMPLETED);
3245
3246 let json = serde_json::to_string(&event_data).unwrap();
3248 assert!(json.contains("turn_id"));
3249 assert!(json.contains("thinking"));
3250 }
3251
3252 #[test]
3253 fn test_output_message_delta_data() {
3254 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3255 let data = OutputMessageDeltaData {
3256 turn_id,
3257 delta: "Hello".to_string(),
3258 accumulated: "Hello".to_string(),
3259 };
3260
3261 let event_data: EventData = data.into();
3262 assert_eq!(event_data.event_type(), OUTPUT_MESSAGE_DELTA);
3263
3264 let json = serde_json::to_string(&event_data).unwrap();
3266 assert!(json.contains("turn_id"));
3267 assert!(json.contains("delta"));
3268 assert!(json.contains("accumulated"));
3269 }
3270
3271 #[test]
3272 fn test_output_message_delta_deserialization_preserves_fields() {
3273 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3276 let data = OutputMessageDeltaData {
3277 turn_id,
3278 delta: "Hello world".to_string(),
3279 accumulated: "Hello world".to_string(),
3280 };
3281
3282 let json = serde_json::to_value(EventData::OutputMessageDelta(data.clone())).unwrap();
3284
3285 let deserialized: EventData = serde_json::from_value(json).unwrap();
3287
3288 match deserialized {
3290 EventData::OutputMessageDelta(td) => {
3291 assert_eq!(td.turn_id, turn_id);
3292 assert_eq!(td.delta, "Hello world");
3293 assert_eq!(td.accumulated, "Hello world");
3294 }
3295 _ => panic!("Expected OutputMessageDelta, got different variant"),
3296 }
3297 }
3298
3299 #[test]
3300 fn test_output_message_started_deserialization() {
3301 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3302 let data = OutputMessageStartedData {
3303 turn_id,
3304 model: Some("claude-3".to_string()),
3305 iteration: None,
3306 };
3307
3308 let json = serde_json::to_value(EventData::OutputMessageStarted(data.clone())).unwrap();
3310
3311 let deserialized: EventData = serde_json::from_value(json).unwrap();
3313
3314 match deserialized {
3316 EventData::OutputMessageStarted(at) => {
3317 assert_eq!(at.turn_id, turn_id);
3318 assert_eq!(at.model, Some("claude-3".to_string()));
3319 }
3320 _ => panic!("Expected OutputMessageStarted, got different variant"),
3321 }
3322 }
3323
3324 #[test]
3325 fn test_reason_thinking_started_deserialization() {
3326 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3330 let data = ReasonThinkingStartedData {
3331 turn_id,
3332 model: Some("claude-3".to_string()),
3333 };
3334
3335 let json = serde_json::to_value(&data).unwrap();
3337
3338 let deserialized = deserialize_event_data(REASON_THINKING_STARTED, json);
3340
3341 match deserialized {
3343 EventData::ReasonThinkingStarted(at) => {
3344 assert_eq!(at.turn_id, turn_id);
3345 assert_eq!(at.model, Some("claude-3".to_string()));
3346 }
3347 other => panic!("Expected ReasonThinkingStarted, got {}", other.event_type()),
3348 }
3349 }
3350
3351 #[test]
3352 fn test_llm_generation_with_ttft() {
3353 let messages = vec![Message::user("Hello")];
3354 let data = LlmGenerationData::success_with_metadata(
3355 messages,
3356 vec![],
3357 Some("Hi!".to_string()),
3358 vec![],
3359 "gpt-4o".to_string(),
3360 Some("openai".to_string()),
3361 Some(TokenUsage {
3362 input_tokens: 10,
3363 output_tokens: 5,
3364 cache_read_tokens: None,
3365 cache_creation_tokens: None,
3366 actual_cost_usd: None,
3367 estimated_cost_usd: None,
3368 }),
3369 Some(500), Some(120), Some(vec!["stop".to_string()]),
3372 None,
3373 );
3374
3375 assert!(data.metadata.success);
3376 assert_eq!(data.metadata.duration_ms, Some(500));
3377 assert_eq!(data.metadata.time_to_first_token_ms, Some(120));
3378 }
3379
3380 #[test]
3381 fn test_llm_generation_ttft_serialization() {
3382 let messages = vec![Message::user("test")];
3383 let data = LlmGenerationData::success_with_metadata(
3384 messages,
3385 vec![],
3386 Some("response".to_string()),
3387 vec![],
3388 "model".to_string(),
3389 None,
3390 None,
3391 Some(1000),
3392 Some(150), None,
3394 None,
3395 );
3396
3397 let json = serde_json::to_string(&data).unwrap();
3398 assert!(json.contains("time_to_first_token_ms"));
3399 assert!(json.contains("150"));
3400 }
3401
3402 #[test]
3403 fn test_reason_item_data_event_type_and_serialization() {
3404 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3405 let data = ReasonItemData {
3406 turn_id,
3407 provider: "openai".to_string(),
3408 model: Some("gpt-5.5".to_string()),
3409 item_id: "rs_abc".to_string(),
3410 encrypted_content: Some("OPAQUE_BLOB".to_string()),
3411 summary: vec!["safe summary".to_string()],
3412 token_count: Some(123),
3413 };
3414
3415 let event_data: EventData = data.into();
3416 assert_eq!(event_data.event_type(), REASON_ITEM);
3417
3418 let json = serde_json::to_string(&event_data).unwrap();
3419 assert!(json.contains("turn_id"));
3420 assert!(json.contains("openai"));
3421 assert!(json.contains("rs_abc"));
3422 assert!(json.contains("OPAQUE_BLOB"));
3423 assert!(json.contains("safe summary"));
3424 }
3425
3426 #[test]
3427 fn test_event_deserialize_reason_item_uses_event_type_dispatch() {
3428 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3429 let payload = serde_json::json!({
3430 "id": EventId::new().to_string(),
3431 "type": REASON_ITEM,
3432 "ts": Utc::now().to_rfc3339(),
3433 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
3434 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
3435 "data": {
3436 "turn_id": turn_id.to_string(),
3437 "provider": "openai",
3438 "model": "gpt-5",
3439 "item_id": "rs_event",
3440 "encrypted_content": "ENC",
3441 "summary": ["safe"],
3442 "token_count": 9
3443 }
3444 });
3445
3446 let event: Event = serde_json::from_value(payload).expect("event deserializes");
3447 match event.data {
3448 EventData::ReasonItem(data) => {
3449 assert_eq!(data.turn_id, turn_id);
3450 assert_eq!(data.provider, "openai");
3451 assert_eq!(data.item_id, "rs_event");
3452 assert_eq!(data.token_count, Some(9));
3453 }
3454 other => panic!("expected reason.item data, got {}", other.event_type()),
3455 }
3456 }
3457
3458 #[test]
3459 fn test_event_request_deserialize_reason_item_uses_event_type_dispatch() {
3460 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3461 let payload = serde_json::json!({
3462 "type": REASON_ITEM,
3463 "ts": Utc::now().to_rfc3339(),
3464 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
3465 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
3466 "data": {
3467 "turn_id": turn_id.to_string(),
3468 "provider": "openai",
3469 "item_id": "rs_request",
3470 "encrypted_content": "ENC",
3471 "summary": ["safe"]
3472 }
3473 });
3474
3475 let req: EventRequest = serde_json::from_value(payload).expect("request deserializes");
3476 match req.data {
3477 EventData::ReasonItem(data) => {
3478 assert_eq!(data.turn_id, turn_id);
3479 assert_eq!(data.provider, "openai");
3480 assert_eq!(data.item_id, "rs_request");
3481 }
3482 other => panic!("expected reason.item data, got {}", other.event_type()),
3483 }
3484 }
3485
3486 #[test]
3487 fn test_event_deserialize_subagent_uses_event_type_dispatch() {
3488 let subagent_session_id = SessionId::from_uuid(Uuid::now_v7());
3489 let payload = serde_json::json!({
3490 "id": EventId::new().to_string(),
3491 "type": SUBAGENT_COMPLETED,
3492 "ts": Utc::now().to_rfc3339(),
3493 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
3494 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
3495 "data": {
3496 "subagent_session_id": subagent_session_id.to_string(),
3497 "subagent_name": "researcher",
3498 "task": "summarize docs",
3499 "status": "completed",
3500 "result": "done"
3501 }
3502 });
3503
3504 let event: Event = serde_json::from_value(payload).expect("event deserializes");
3505 match event.data {
3506 EventData::SubagentCompleted(data) => {
3507 assert_eq!(data.subagent_session_id, subagent_session_id);
3508 assert_eq!(data.subagent_name, "researcher");
3509 assert_eq!(data.status, "completed");
3510 }
3511 other => panic!("expected subagent.completed, got {}", other.event_type()),
3512 }
3513 }
3514
3515 #[test]
3516 fn test_reason_item_data_round_trip_uses_typed_dispatch() {
3517 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3521 let data = ReasonItemData {
3522 turn_id,
3523 provider: "openai".to_string(),
3524 model: Some("gpt-5".to_string()),
3525 item_id: "rs_xyz".to_string(),
3526 encrypted_content: Some("ENC".to_string()),
3527 summary: vec![],
3528 token_count: None,
3529 };
3530
3531 let json = serde_json::to_value(&data).unwrap();
3532 let deserialized = deserialize_event_data(REASON_ITEM, json);
3533
3534 match deserialized {
3535 EventData::ReasonItem(out) => {
3536 assert_eq!(out.turn_id, turn_id);
3537 assert_eq!(out.provider, "openai");
3538 assert_eq!(out.item_id, "rs_xyz");
3539 assert_eq!(out.encrypted_content.as_deref(), Some("ENC"));
3540 }
3541 other => panic!("Expected ReasonItem, got {}", other.event_type()),
3542 }
3543 }
3544
3545 #[test]
3554 fn test_reason_item_variant_precedes_reason_thinking_started() {
3555 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3562 let json = serde_json::json!({
3563 "turn_id": turn_id.to_string(),
3564 "provider": "openai",
3565 "model": "gpt-5",
3566 "item_id": "rs_keep",
3567 "encrypted_content": "ENC",
3568 "summary": ["s"],
3569 "token_count": 7,
3570 });
3571
3572 let as_thinking: ReasonThinkingStartedData =
3578 serde_json::from_value(json.clone()).expect("thinking ignores extra fields");
3579 assert_eq!(as_thinking.turn_id, turn_id);
3580 assert_eq!(as_thinking.model.as_deref(), Some("gpt-5"));
3581
3582 let as_item: ReasonItemData =
3583 serde_json::from_value(json.clone()).expect("ReasonItem accepts payload");
3584 assert_eq!(as_item.item_id, "rs_keep");
3585 assert_eq!(as_item.provider, "openai");
3586
3587 let event_data = deserialize_event_data(REASON_ITEM, json);
3590 match event_data {
3591 EventData::ReasonItem(out) => {
3592 assert_eq!(out.item_id, "rs_keep");
3593 assert_eq!(out.provider, "openai");
3594 }
3595 other => panic!(
3596 "Typed dispatcher must select ReasonItem for {REASON_ITEM}, got {}",
3597 other.event_type()
3598 ),
3599 }
3600 }
3601
3602 #[test]
3609 fn test_reason_item_data_excludes_plaintext_reasoning() {
3610 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3611 let data = ReasonItemData {
3612 turn_id,
3613 provider: "openai".to_string(),
3614 model: Some("gpt-5".to_string()),
3615 item_id: "rs_secret".to_string(),
3616 encrypted_content: Some("opaque_blob_thinking_content_reasoning_text".to_string()),
3620 summary: vec!["safe summary mentioning content and thinking".to_string()],
3621 token_count: Some(1),
3622 };
3623
3624 let value = serde_json::to_value(&data).expect("serializable");
3625 let object = value.as_object().expect("data serializes to JSON object");
3626 for forbidden in [
3627 "content",
3628 "reasoning_text",
3629 "thinking",
3630 "reasoning_content",
3631 "raw_reasoning",
3632 ] {
3633 assert!(
3634 !object.contains_key(forbidden),
3635 "ReasonItemData JSON must not expose `{forbidden}` key, got: {object:?}",
3636 );
3637 }
3638 assert!(object.contains_key("encrypted_content"));
3640 assert!(object.contains_key("summary"));
3641 }
3642
3643 #[test]
3644 fn test_llm_generation_ttft_omitted_when_none() {
3645 let messages = vec![Message::user("test")];
3646 let data = LlmGenerationData::success(
3647 messages,
3648 vec![],
3649 Some("response".to_string()),
3650 vec![],
3651 "model".to_string(),
3652 None,
3653 None,
3654 None,
3655 None, );
3657
3658 assert!(data.metadata.time_to_first_token_ms.is_none());
3660
3661 let json = serde_json::to_string(&data).unwrap();
3663 assert!(!json.contains("time_to_first_token_ms"));
3664 }
3665}
3666
3667#[cfg(test)]
3676mod contract_tests {
3677 use super::*;
3678 use insta::{assert_json_snapshot, with_settings};
3679
3680 fn test_session_id() -> SessionId {
3682 SessionId::from_uuid(uuid::Uuid::from_u128(
3683 0x0000_0000_0000_0000_0000_0000_0000_0001,
3684 ))
3685 }
3686
3687 fn test_turn_id() -> TurnId {
3688 TurnId::from_uuid(uuid::Uuid::from_u128(
3689 0x0000_0000_0000_0000_0000_0000_0000_0002,
3690 ))
3691 }
3692
3693 fn test_message_id() -> MessageId {
3694 MessageId::from_uuid(uuid::Uuid::from_u128(
3695 0x0000_0000_0000_0000_0000_0000_0000_0003,
3696 ))
3697 }
3698
3699 fn test_agent_id() -> AgentId {
3700 AgentId::from_uuid(uuid::Uuid::from_u128(
3701 0x0000_0000_0000_0000_0000_0000_0000_0004,
3702 ))
3703 }
3704
3705 fn test_harness_id() -> HarnessId {
3706 HarnessId::from_uuid(uuid::Uuid::from_u128(
3707 0x0000_0000_0000_0000_0000_0000_0000_0005,
3708 ))
3709 }
3710
3711 #[test]
3718 fn snapshot_input_message() {
3719 let data = InputMessageData::new(Message::user("Hello, world!"));
3720 with_settings!({
3721 sort_maps => true,
3722 }, {
3723 assert_json_snapshot!("event_data_input_message", data, {
3725 ".message.id" => "[MESSAGE_ID]",
3726 ".message.created_at" => "[TIMESTAMP]"
3727 });
3728 });
3729 }
3730
3731 #[test]
3732 fn snapshot_output_message_started() {
3733 let data = OutputMessageStartedData {
3734 turn_id: test_turn_id(),
3735 model: Some("gpt-4o".to_string()),
3736 iteration: None,
3737 };
3738 with_settings!({
3739 sort_maps => true,
3740 }, {
3741 assert_json_snapshot!("event_data_output_message_started", data);
3742 });
3743 }
3744
3745 #[test]
3746 fn snapshot_output_message_delta() {
3747 let data = OutputMessageDeltaData {
3748 turn_id: test_turn_id(),
3749 delta: "Hello".to_string(),
3750 accumulated: "Hello".to_string(),
3751 };
3752 with_settings!({
3753 sort_maps => true,
3754 }, {
3755 assert_json_snapshot!("event_data_output_message_delta", data);
3756 });
3757 }
3758
3759 #[test]
3760 fn snapshot_output_message_completed() {
3761 let data = OutputMessageCompletedData::new(Message::assistant("Hello!"));
3762 with_settings!({
3763 sort_maps => true,
3764 }, {
3765 assert_json_snapshot!("event_data_output_message_completed", data, {
3767 ".message.id" => "[MESSAGE_ID]",
3768 ".message.created_at" => "[TIMESTAMP]"
3769 });
3770 });
3771 }
3772
3773 #[test]
3774 fn snapshot_turn_started() {
3775 let data = TurnStartedData {
3776 turn_id: test_turn_id(),
3777 input_message_id: test_message_id(),
3778 input_content: Some("Hello".to_string()),
3779 };
3780 with_settings!({
3781 sort_maps => true,
3782 }, {
3783 assert_json_snapshot!("event_data_turn_started", data);
3784 });
3785 }
3786
3787 #[test]
3788 fn snapshot_turn_completed() {
3789 let data = TurnCompletedData {
3790 turn_id: test_turn_id(),
3791 iterations: 3,
3792 duration_ms: Some(1500),
3793 usage: Some(TokenUsage::new(100, 50)),
3794 input_content: None,
3795 final_message_id: Some(test_message_id()),
3796 final_answer_preview: Some("Done.".to_string()),
3797 time_to_first_token_ms: Some(120),
3798 tool_call_count: Some(2),
3799 llm_call_count: Some(3),
3800 status: Some("completed".to_string()),
3801 };
3802 with_settings!({
3803 sort_maps => true,
3804 }, {
3805 assert_json_snapshot!("event_data_turn_completed", data);
3806 });
3807 }
3808
3809 #[test]
3810 fn snapshot_turn_failed() {
3811 let data = TurnFailedData {
3812 turn_id: test_turn_id(),
3813 error: "Rate limit exceeded".to_string(),
3814 error_code: Some("RATE_LIMIT".to_string()),
3815 error_fields: None,
3816 };
3817 with_settings!({
3818 sort_maps => true,
3819 }, {
3820 assert_json_snapshot!("event_data_turn_failed", data);
3821 });
3822 }
3823
3824 #[test]
3825 fn snapshot_turn_cancelled() {
3826 let data = TurnCancelledData {
3827 turn_id: test_turn_id(),
3828 reason: Some("User requested".to_string()),
3829 usage: Some(TokenUsage::new(50, 25)),
3830 };
3831 with_settings!({
3832 sort_maps => true,
3833 }, {
3834 assert_json_snapshot!("event_data_turn_cancelled", data);
3835 });
3836 }
3837
3838 #[test]
3839 fn snapshot_reason_started() {
3840 let data = ReasonStartedData {
3841 harness_id: test_harness_id(),
3842 agent_id: Some(test_agent_id()),
3843 metadata: Some(ModelMetadata {
3844 model: "gpt-4o".to_string(),
3845 model_id: None,
3846 provider_id: None,
3847 }),
3848 };
3849 with_settings!({
3850 sort_maps => true,
3851 }, {
3852 assert_json_snapshot!("event_data_reason_started", data);
3853 });
3854 }
3855
3856 #[test]
3857 fn snapshot_reason_completed() {
3858 let data = ReasonCompletedData::success(
3859 "Hello world",
3860 true,
3861 2,
3862 Some(1000),
3863 Some(TokenUsage::new(100, 50)),
3864 );
3865 with_settings!({
3866 sort_maps => true,
3867 }, {
3868 assert_json_snapshot!("event_data_reason_completed", data);
3869 });
3870 }
3871
3872 #[test]
3873 fn snapshot_act_started() {
3874 let data = ActStartedData {
3875 tool_calls: vec![ToolCallSummary {
3876 id: "tc_1".to_string(),
3877 name: "get_weather".to_string(),
3878 display_name: None,
3879 narration: None,
3880 }],
3881 headline: None,
3882 };
3883 with_settings!({
3884 sort_maps => true,
3885 }, {
3886 assert_json_snapshot!("event_data_act_started", data);
3887 });
3888 }
3889
3890 #[test]
3891 fn snapshot_act_completed() {
3892 let data = ActCompletedData {
3893 completed: true,
3894 success_count: 2,
3895 error_count: 0,
3896 duration_ms: Some(500),
3897 headline: None,
3898 };
3899 with_settings!({
3900 sort_maps => true,
3901 }, {
3902 assert_json_snapshot!("event_data_act_completed", data);
3903 });
3904 }
3905
3906 #[test]
3907 fn snapshot_tool_started() {
3908 let data = ToolStartedData {
3909 tool_call: ToolCall {
3910 id: "tc_1".to_string(),
3911 name: "get_weather".to_string(),
3912 arguments: serde_json::json!({"city": "London"}),
3913 },
3914 tool_call_fingerprint: None,
3915 display_name: None,
3916 narration: None,
3917 };
3918 with_settings!({
3919 sort_maps => true,
3920 }, {
3921 assert_json_snapshot!("event_data_tool_started", data);
3922 });
3923 }
3924
3925 #[test]
3926 fn snapshot_tool_completed() {
3927 let data = ToolCompletedData::success(
3928 "tc_1".to_string(),
3929 "get_weather".to_string(),
3930 vec![crate::message::ContentPart::text("Sunny, 22°C")],
3931 Some(250),
3932 );
3933 with_settings!({
3934 sort_maps => true,
3935 }, {
3936 assert_json_snapshot!("event_data_tool_completed", data);
3937 });
3938 }
3939
3940 #[test]
3941 fn snapshot_llm_generation() {
3942 let data = LlmGenerationData::success(
3943 vec![Message::user("Hello")],
3944 vec![ToolDefinitionSummary {
3945 name: "tool1".to_string(),
3946 display_name: None,
3947 category: None,
3948 capability_id: None,
3949 capability_name: None,
3950 description: "A tool".to_string(),
3951 }],
3952 Some("Hi there!".to_string()),
3953 vec![],
3954 "gpt-4o".to_string(),
3955 Some("openai".to_string()),
3956 Some(TokenUsage::new(10, 5)),
3957 Some(100),
3958 Some(25),
3959 );
3960 with_settings!({
3961 sort_maps => true,
3962 }, {
3963 assert_json_snapshot!("event_data_llm_generation", data, {
3965 ".messages[].id" => "[MESSAGE_ID]",
3966 ".messages[].created_at" => "[TIMESTAMP]"
3967 });
3968 });
3969 }
3970
3971 #[test]
3972 fn snapshot_reason_thinking_started() {
3973 let data = ReasonThinkingStartedData {
3974 turn_id: test_turn_id(),
3975 model: Some("claude-4-opus".to_string()),
3976 };
3977 with_settings!({
3978 sort_maps => true,
3979 }, {
3980 assert_json_snapshot!("event_data_reason_thinking_started", data);
3981 });
3982 }
3983
3984 #[test]
3985 fn snapshot_reason_thinking_delta() {
3986 let data = ReasonThinkingDeltaData {
3987 turn_id: test_turn_id(),
3988 delta: "Let me think...".to_string(),
3989 accumulated: "Let me think...".to_string(),
3990 };
3991 with_settings!({
3992 sort_maps => true,
3993 }, {
3994 assert_json_snapshot!("event_data_reason_thinking_delta", data);
3995 });
3996 }
3997
3998 #[test]
3999 fn snapshot_reason_thinking_completed() {
4000 let data = ReasonThinkingCompletedData {
4001 turn_id: test_turn_id(),
4002 thinking: "I need to consider...".to_string(),
4003 };
4004 with_settings!({
4005 sort_maps => true,
4006 }, {
4007 assert_json_snapshot!("event_data_reason_thinking_completed", data);
4008 });
4009 }
4010
4011 #[test]
4012 fn snapshot_reason_item() {
4013 let data = ReasonItemData {
4014 turn_id: test_turn_id(),
4015 provider: "openai".to_string(),
4016 model: Some("gpt-5.5".to_string()),
4017 item_id: "rs_test".to_string(),
4018 encrypted_content: Some("OPAQUE".to_string()),
4019 summary: vec!["safe summary".to_string()],
4020 token_count: Some(42),
4021 };
4022 with_settings!({
4023 sort_maps => true,
4024 }, {
4025 assert_json_snapshot!("event_data_reason_item", data);
4026 });
4027 }
4028
4029 #[test]
4030 fn snapshot_session_started() {
4031 let data = SessionStartedData {
4032 harness_id: test_harness_id(),
4033 agent_id: Some(test_agent_id()),
4034 model_id: None,
4035 };
4036 with_settings!({
4037 sort_maps => true,
4038 }, {
4039 assert_json_snapshot!("event_data_session_started", data);
4040 });
4041 }
4042
4043 #[test]
4044 fn snapshot_session_activated() {
4045 let data = SessionActivatedData {
4046 turn_id: test_turn_id(),
4047 input_message_id: test_message_id(),
4048 };
4049 with_settings!({
4050 sort_maps => true,
4051 }, {
4052 assert_json_snapshot!("event_data_session_activated", data);
4053 });
4054 }
4055
4056 #[test]
4057 fn snapshot_session_idled() {
4058 let data = SessionIdledData {
4059 turn_id: test_turn_id(),
4060 iterations: Some(3),
4061 usage: Some(TokenUsage::new(500, 200)),
4062 };
4063 with_settings!({
4064 sort_maps => true,
4065 }, {
4066 assert_json_snapshot!("event_data_session_idled", data);
4067 });
4068 }
4069
4070 #[test]
4076 fn tool_call_summary_with_display_name() {
4077 let summary = ToolCallSummary {
4078 id: "tc_1".to_string(),
4079 name: "get_weather".to_string(),
4080 display_name: Some("Get Weather".to_string()),
4081 narration: None,
4082 };
4083 let json = serde_json::to_value(&summary).unwrap();
4084 assert_eq!(json["display_name"], "Get Weather");
4085
4086 let deserialized: ToolCallSummary = serde_json::from_value(json).unwrap();
4088 assert_eq!(deserialized.display_name.as_deref(), Some("Get Weather"));
4089 }
4090
4091 #[test]
4092 fn tool_call_summary_without_display_name_omits_field() {
4093 let summary = ToolCallSummary {
4094 id: "tc_1".to_string(),
4095 name: "get_weather".to_string(),
4096 display_name: None,
4097 narration: None,
4098 };
4099 let json = serde_json::to_string(&summary).unwrap();
4100 assert!(!json.contains("display_name"));
4101
4102 let json_without = r#"{"id":"tc_1","name":"get_weather"}"#;
4104 let deserialized: ToolCallSummary = serde_json::from_str(json_without).unwrap();
4105 assert_eq!(deserialized.display_name, None);
4106 }
4107
4108 #[test]
4109 fn act_started_with_definitions_populates_display_names() {
4110 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4111
4112 let tool_calls = vec![
4113 ToolCall {
4114 id: "tc_1".to_string(),
4115 name: "get_weather".to_string(),
4116 arguments: serde_json::json!({}),
4117 },
4118 ToolCall {
4119 id: "tc_2".to_string(),
4120 name: "unknown_tool".to_string(),
4121 arguments: serde_json::json!({}),
4122 },
4123 ];
4124 let tool_defs = vec![crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4125 name: "get_weather".to_string(),
4126 display_name: Some("Get Weather".to_string()),
4127 description: "Gets weather".to_string(),
4128 parameters: serde_json::json!({}),
4129 policy: ToolPolicy::Auto,
4130 category: None,
4131 deferrable: DeferrablePolicy::default(),
4132 hints: crate::tool_types::ToolHints::default(),
4133 full_parameters: None,
4134 })];
4135
4136 let data = ActStartedData::with_definitions(&tool_calls, &tool_defs);
4137 assert_eq!(data.tool_calls.len(), 2);
4138 assert_eq!(
4139 data.tool_calls[0].display_name.as_deref(),
4140 Some("Get Weather")
4141 );
4142 assert_eq!(data.tool_calls[1].display_name, None);
4143 }
4144
4145 #[test]
4146 fn tool_completed_with_display_name_roundtrip() {
4147 let data = ToolCompletedData::success(
4148 "tc_1".to_string(),
4149 "get_weather".to_string(),
4150 vec![crate::message::ContentPart::text("Sunny")],
4151 Some(100),
4152 )
4153 .with_display_name(Some("Get Weather".to_string()));
4154
4155 assert_eq!(data.display_name.as_deref(), Some("Get Weather"));
4156
4157 let json = serde_json::to_value(&data).unwrap();
4158 assert_eq!(json["display_name"], "Get Weather");
4159
4160 let deserialized: ToolCompletedData = serde_json::from_value(json).unwrap();
4161 assert_eq!(deserialized.display_name.as_deref(), Some("Get Weather"));
4162 }
4163
4164 #[test]
4165 fn tool_started_display_name_serialization() {
4166 let data = ToolStartedData {
4167 tool_call: ToolCall {
4168 id: "tc_1".to_string(),
4169 name: "bash".to_string(),
4170 arguments: serde_json::json!({"command": "ls"}),
4171 },
4172 tool_call_fingerprint: None,
4173 display_name: Some("Bash".to_string()),
4174 narration: None,
4175 };
4176
4177 let json = serde_json::to_value(&data).unwrap();
4178 assert_eq!(json["display_name"], "Bash");
4179 }
4180
4181 #[test]
4182 fn tool_definition_summary_display_name() {
4183 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4184
4185 let def = crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4186 name: "read_file".to_string(),
4187 display_name: Some("Read File".to_string()),
4188 description: "Reads a file".to_string(),
4189 parameters: serde_json::json!({}),
4190 policy: ToolPolicy::Auto,
4191 category: None,
4192 deferrable: DeferrablePolicy::default(),
4193 hints: crate::tool_types::ToolHints::default(),
4194 full_parameters: None,
4195 });
4196
4197 let summary = ToolDefinitionSummary::from(&def);
4198 assert_eq!(summary.display_name.as_deref(), Some("Read File"));
4199
4200 let json = serde_json::to_value(&summary).unwrap();
4201 assert_eq!(json["display_name"], "Read File");
4202 }
4203
4204 #[test]
4211 fn forward_compat_unknown_fields_ignored() {
4212 let json = r#"{
4214 "turn_id": "turn_00000000000000000000000000000002",
4215 "iterations": 3,
4216 "duration_ms": 1500,
4217 "usage": {"input_tokens": 100, "output_tokens": 50},
4218 "future_field": "should be ignored",
4219 "another_new_field": 42
4220 }"#;
4221
4222 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4223 assert_eq!(data.iterations, 3);
4224 assert_eq!(data.duration_ms, Some(1500));
4225 }
4226
4227 #[test]
4228 fn forward_compat_unknown_event_type_becomes_unsupported() {
4229 let json = serde_json::json!({"some_field": "value"});
4231 let data = deserialize_event_data("future.event.type", json);
4232
4233 assert!(data.is_unsupported());
4234 assert_eq!(data.event_type(), "unsupported");
4235 }
4236
4237 #[test]
4238 fn forward_compat_unsupported_preserves_data() {
4239 let original = serde_json::json!({"key": "value", "nested": {"a": 1}});
4241 let data = deserialize_event_data("unknown.event", original.clone());
4242
4243 match data {
4244 EventData::Unsupported { event_type, data } => {
4245 assert_eq!(event_type, "unknown.event");
4246 assert_eq!(data, original);
4247 }
4248 _ => panic!("Expected Unsupported variant"),
4249 }
4250 }
4251
4252 #[test]
4253 fn forward_compat_optional_fields_absent() {
4254 let json = r#"{
4256 "turn_id": "turn_00000000000000000000000000000002",
4257 "iterations": 3
4258 }"#;
4259
4260 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4261 assert_eq!(data.iterations, 3);
4262 assert!(data.duration_ms.is_none());
4263 assert!(data.usage.is_none());
4264 assert!(data.input_content.is_none());
4265 assert!(data.final_message_id.is_none());
4266 assert!(data.final_answer_preview.is_none());
4267 assert!(data.time_to_first_token_ms.is_none());
4268 assert!(data.tool_call_count.is_none());
4269 assert!(data.llm_call_count.is_none());
4270 assert!(data.status.is_none());
4271 }
4272
4273 #[test]
4279 fn round_trip_all_event_data_types() {
4280 let test_cases: Vec<(&str, EventData)> = vec![
4282 (
4283 INPUT_MESSAGE,
4284 InputMessageData::new(Message::user("test")).into(),
4285 ),
4286 (
4287 OUTPUT_MESSAGE_STARTED,
4288 OutputMessageStartedData {
4289 turn_id: test_turn_id(),
4290 model: None,
4291 iteration: None,
4292 }
4293 .into(),
4294 ),
4295 (
4296 OUTPUT_MESSAGE_DELTA,
4297 OutputMessageDeltaData {
4298 turn_id: test_turn_id(),
4299 delta: "x".to_string(),
4300 accumulated: "x".to_string(),
4301 }
4302 .into(),
4303 ),
4304 (
4305 OUTPUT_MESSAGE_COMPLETED,
4306 OutputMessageCompletedData::new(Message::assistant("hi")).into(),
4307 ),
4308 (
4309 TURN_STARTED,
4310 TurnStartedData {
4311 turn_id: test_turn_id(),
4312 input_message_id: test_message_id(),
4313 input_content: None,
4314 }
4315 .into(),
4316 ),
4317 (
4318 TURN_COMPLETED,
4319 TurnCompletedData {
4320 turn_id: test_turn_id(),
4321 iterations: 1,
4322 duration_ms: None,
4323 usage: None,
4324 input_content: None,
4325 final_message_id: None,
4326 final_answer_preview: None,
4327 time_to_first_token_ms: None,
4328 tool_call_count: None,
4329 llm_call_count: None,
4330 status: None,
4331 }
4332 .into(),
4333 ),
4334 (
4335 TURN_FAILED,
4336 TurnFailedData {
4337 turn_id: test_turn_id(),
4338 error: "err".to_string(),
4339 error_code: None,
4340 error_fields: None,
4341 }
4342 .into(),
4343 ),
4344 (
4345 TURN_CANCELLED,
4346 TurnCancelledData {
4347 turn_id: test_turn_id(),
4348 reason: None,
4349 usage: None,
4350 }
4351 .into(),
4352 ),
4353 (
4354 REASON_STARTED,
4355 ReasonStartedData {
4356 harness_id: test_harness_id(),
4357 agent_id: Some(test_agent_id()),
4358 metadata: None,
4359 }
4360 .into(),
4361 ),
4362 (
4363 REASON_COMPLETED,
4364 ReasonCompletedData::success("", false, 0, None, None).into(),
4365 ),
4366 (
4367 ACT_STARTED,
4368 ActStartedData {
4369 tool_calls: vec![],
4370 headline: None,
4371 }
4372 .into(),
4373 ),
4374 (
4375 ACT_COMPLETED,
4376 ActCompletedData {
4377 completed: true,
4378 success_count: 0,
4379 error_count: 0,
4380 duration_ms: None,
4381 headline: None,
4382 }
4383 .into(),
4384 ),
4385 (
4386 SESSION_STARTED,
4387 SessionStartedData {
4388 harness_id: test_harness_id(),
4389 agent_id: Some(test_agent_id()),
4390 model_id: None,
4391 }
4392 .into(),
4393 ),
4394 (
4395 SESSION_ACTIVATED,
4396 SessionActivatedData {
4397 turn_id: test_turn_id(),
4398 input_message_id: test_message_id(),
4399 }
4400 .into(),
4401 ),
4402 (
4403 SESSION_IDLED,
4404 SessionIdledData {
4405 turn_id: test_turn_id(),
4406 iterations: None,
4407 usage: None,
4408 }
4409 .into(),
4410 ),
4411 ];
4412
4413 for (event_type, original) in test_cases {
4414 let json = serde_json::to_value(&original).unwrap();
4416 let deserialized = deserialize_event_data(event_type, json);
4418 assert_eq!(
4420 original.event_type(),
4421 deserialized.event_type(),
4422 "Event type mismatch for {}",
4423 event_type
4424 );
4425 }
4426 }
4427
4428 #[test]
4434 fn event_structure_has_required_fields() {
4435 let session_id = test_session_id();
4436 let context = EventContext::turn(test_turn_id(), test_message_id());
4437 let event = Event::new(
4438 session_id,
4439 context,
4440 InputMessageData::new(Message::user("test")),
4441 );
4442
4443 let json = serde_json::to_value(&event).unwrap();
4445 assert!(json.get("id").is_some(), "Missing id field");
4446 assert!(json.get("type").is_some(), "Missing type field");
4447 assert!(json.get("ts").is_some(), "Missing ts field");
4448 assert!(json.get("session_id").is_some(), "Missing session_id field");
4449 assert!(json.get("context").is_some(), "Missing context field");
4450 assert!(json.get("data").is_some(), "Missing data field");
4451 }
4452
4453 #[test]
4454 fn event_context_span_fields() {
4455 let context = EventContext::empty().with_span(
4456 "trace123".to_string(),
4457 "span456".to_string(),
4458 Some("parent789".to_string()),
4459 );
4460
4461 let json = serde_json::to_value(&context).unwrap();
4462 assert_eq!(
4463 json.get("trace_id").and_then(|v| v.as_str()),
4464 Some("trace123")
4465 );
4466 assert_eq!(
4467 json.get("span_id").and_then(|v| v.as_str()),
4468 Some("span456")
4469 );
4470 assert_eq!(
4471 json.get("parent_span_id").and_then(|v| v.as_str()),
4472 Some("parent789")
4473 );
4474 }
4475
4476 #[test]
4477 fn is_unsupported_returns_false_for_known_types() {
4478 let data = InputMessageData::new(Message::user("test"));
4479 let event_data: EventData = data.into();
4480 assert!(!event_data.is_unsupported());
4481 }
4482
4483 #[test]
4484 fn is_unsupported_returns_true_for_unsupported() {
4485 let data = deserialize_event_data("unknown.type", serde_json::json!({}));
4486 assert!(data.is_unsupported());
4487 }
4488}