1use chrono::{DateTime, Utc};
22use serde::{Deserialize, Deserializer, Serialize};
23use serde_json::Value;
24use std::collections::HashMap;
25use uuid::Uuid;
26
27#[cfg(feature = "openapi")]
28use utoipa::ToSchema;
29
30use crate::localization::localized_tool_display_name;
31use crate::typed_id::{AgentId, EventId, ExecId, HarnessId, MessageId, ModelId, SessionId, TurnId};
32use crate::user_facing_error::{UserFacingError, UserFacingErrorFields};
33
34pub const INPUT_MESSAGE: &str = "input.message";
40
41pub const OUTPUT_MESSAGE_STARTED: &str = "output.message.started";
43pub const OUTPUT_MESSAGE_DELTA: &str = "output.message.delta";
44pub const OUTPUT_MESSAGE_COMPLETED: &str = "output.message.completed";
45pub const OUTPUT_MESSAGE_REPLACED: &str = "output.message.replaced";
50
51pub const TURN_STARTED: &str = "turn.started";
53pub const TURN_COMPLETED: &str = "turn.completed";
54pub const TURN_FAILED: &str = "turn.failed";
55pub const TURN_SEALED: &str = "turn.sealed";
60pub const TURN_CANCELLED: &str = "turn.cancelled";
61
62pub const REASON_STARTED: &str = "reason.started";
64pub const REASON_COMPLETED: &str = "reason.completed";
65pub const REASON_RECOVERED: &str = "reason.recovered";
66pub const CAPABILITY_USAGE: &str = "capability.usage";
67pub const ACT_STARTED: &str = "act.started";
68pub const ACT_COMPLETED: &str = "act.completed";
69pub const TOOL_STARTED: &str = "tool.started";
70pub const TOOL_COMPLETED: &str = "tool.completed";
71pub const TOOL_PROGRESS: &str = "tool.progress";
72pub const TOOL_OUTPUT_DELTA: &str = "tool.output.delta";
73pub const TOOL_CALL_REQUESTED: &str = "tool.call_requested";
74pub const TRANSCRIPT_REPAIRED: &str = "transcript.repaired";
75pub const TOOL_CALL_REPAIRED: &str = "tool.call_repaired";
79
80pub const LLM_GENERATION: &str = "llm.generation";
82
83fn is_ephemeral_event_type(event_type: &str) -> bool {
87 matches!(
88 event_type,
89 OUTPUT_MESSAGE_DELTA
90 | REASON_THINKING_DELTA
91 | TOOL_OUTPUT_DELTA
92 | VOICE_INPUT_TRANSCRIPT_DELTA
93 | VOICE_OUTPUT_TRANSCRIPT_DELTA
94 )
95}
96
97pub const REASON_THINKING_STARTED: &str = "reason.thinking.started";
99pub const REASON_THINKING_DELTA: &str = "reason.thinking.delta";
100pub const REASON_THINKING_COMPLETED: &str = "reason.thinking.completed";
101
102pub const REASON_ITEM: &str = "reason.item";
109
110pub const SESSION_STARTED: &str = "session.started";
112pub const SESSION_ACTIVATED: &str = "session.activated";
113pub const SESSION_IDLED: &str = "session.idled";
114
115pub const SCHEDULE_TRIGGERED: &str = "schedule.triggered";
117
118pub const TASK_CREATED: &str = "task.created";
125pub const TASK_UPDATED: &str = "task.updated";
126pub const TASK_MESSAGE_SENT: &str = "task.message.sent";
127pub const TASK_MESSAGE_RECEIVED: &str = "task.message.received";
128
129pub const CONTEXT_COMPACTING: &str = "context.compacting";
131pub const CONTEXT_COMPACTED: &str = "context.compacted";
132
133pub const FILE_WRITTEN: &str = "file.written";
135
136pub const BUDGET_WARNING: &str = "budget.warning";
138pub const BUDGET_PAUSED: &str = "budget.paused";
139pub const BUDGET_EXHAUSTED: &str = "budget.exhausted";
140pub const BUDGET_RESUMED: &str = "budget.resumed";
141
142pub const VOICE_SESSION_STARTED: &str = "voice.session.started";
144pub const VOICE_INPUT_TRANSCRIPT_DELTA: &str = "voice.input_transcript.delta";
145pub const VOICE_INPUT_TRANSCRIPT_COMPLETED: &str = "voice.input_transcript.completed";
146pub const VOICE_OUTPUT_TRANSCRIPT_DELTA: &str = "voice.output_transcript.delta";
147pub const VOICE_OUTPUT_TRANSCRIPT_COMPLETED: &str = "voice.output_transcript.completed";
148pub const VOICE_SESSION_ENDED: &str = "voice.session.ended";
149pub const VOICE_SESSION_FAILED: &str = "voice.session.failed";
150
151pub const VALID_EVENT_TYPES: &[&str] = &[
155 INPUT_MESSAGE,
156 OUTPUT_MESSAGE_STARTED,
157 OUTPUT_MESSAGE_DELTA,
158 OUTPUT_MESSAGE_COMPLETED,
159 OUTPUT_MESSAGE_REPLACED,
160 TURN_STARTED,
161 TURN_COMPLETED,
162 TURN_FAILED,
163 TURN_SEALED,
164 TURN_CANCELLED,
165 REASON_STARTED,
166 REASON_COMPLETED,
167 REASON_RECOVERED,
168 ACT_STARTED,
169 ACT_COMPLETED,
170 TOOL_STARTED,
171 TOOL_COMPLETED,
172 TOOL_PROGRESS,
173 TOOL_OUTPUT_DELTA,
174 TOOL_CALL_REQUESTED,
175 TRANSCRIPT_REPAIRED,
176 TOOL_CALL_REPAIRED,
177 LLM_GENERATION,
178 REASON_THINKING_STARTED,
179 REASON_THINKING_DELTA,
180 REASON_THINKING_COMPLETED,
181 REASON_ITEM,
182 SESSION_STARTED,
183 SESSION_ACTIVATED,
184 SESSION_IDLED,
185 SCHEDULE_TRIGGERED,
186 CONTEXT_COMPACTING,
187 CONTEXT_COMPACTED,
188 BUDGET_WARNING,
189 BUDGET_PAUSED,
190 BUDGET_EXHAUSTED,
191 BUDGET_RESUMED,
192 VOICE_SESSION_STARTED,
193 VOICE_INPUT_TRANSCRIPT_DELTA,
194 VOICE_INPUT_TRANSCRIPT_COMPLETED,
195 VOICE_OUTPUT_TRANSCRIPT_DELTA,
196 VOICE_OUTPUT_TRANSCRIPT_COMPLETED,
197 VOICE_SESSION_ENDED,
198 VOICE_SESSION_FAILED,
199 FILE_WRITTEN,
200 CAPABILITY_USAGE,
201];
202
203use crate::atoms::AtomContext;
208
209#[derive(Debug, Clone, Serialize, Deserialize, Default)]
216#[cfg_attr(feature = "openapi", derive(ToSchema))]
217pub struct EventContext {
218 #[serde(skip_serializing_if = "Option::is_none")]
220 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "turn_01933b5a00007000800000000000001"))]
221 pub turn_id: Option<TurnId>,
222
223 #[serde(skip_serializing_if = "Option::is_none")]
225 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "message_01933b5a00007000800000000000001"))]
226 pub input_message_id: Option<MessageId>,
227
228 #[serde(skip_serializing_if = "Option::is_none")]
230 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "exec_01933b5a00007000800000000000001"))]
231 pub exec_id: Option<ExecId>,
232
233 #[serde(skip_serializing_if = "Option::is_none")]
236 pub trace_id: Option<String>,
237
238 #[serde(skip_serializing_if = "Option::is_none")]
241 pub span_id: Option<String>,
242
243 #[serde(skip_serializing_if = "Option::is_none")]
246 pub parent_span_id: Option<String>,
247}
248
249impl EventContext {
250 pub fn empty() -> Self {
252 Self::default()
253 }
254
255 pub fn from_atom_context(ctx: &AtomContext) -> Self {
257 Self {
258 turn_id: Some(ctx.turn_id),
259 input_message_id: Some(ctx.input_message_id),
260 exec_id: Some(ctx.exec_id),
261 trace_id: None,
262 span_id: None,
263 parent_span_id: None,
264 }
265 }
266
267 pub fn turn(turn_id: TurnId, input_message_id: MessageId) -> Self {
269 Self {
270 turn_id: Some(turn_id),
271 input_message_id: Some(input_message_id),
272 exec_id: None,
273 trace_id: None,
274 span_id: None,
275 parent_span_id: None,
276 }
277 }
278
279 pub fn with_span(
281 mut self,
282 trace_id: String,
283 span_id: String,
284 parent_span_id: Option<String>,
285 ) -> Self {
286 self.trace_id = Some(trace_id);
287 self.span_id = Some(span_id);
288 self.parent_span_id = parent_span_id;
289 self
290 }
291}
292
293#[derive(Debug, Clone, Serialize)]
309#[cfg_attr(feature = "openapi", derive(ToSchema))]
310pub struct Event {
311 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "event_01933b5a00007000800000000000001"))]
313 pub id: EventId,
314
315 #[serde(rename = "type")]
317 pub event_type: String,
318
319 pub ts: DateTime<Utc>,
321
322 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "session_01933b5a00007000800000000000001"))]
324 pub session_id: SessionId,
325
326 pub context: EventContext,
328
329 pub data: EventData,
332
333 #[serde(skip_serializing_if = "Option::is_none")]
335 pub metadata: Option<serde_json::Value>,
336
337 #[serde(skip_serializing_if = "Option::is_none")]
339 pub tags: Option<Vec<String>>,
340
341 #[serde(skip_serializing_if = "Option::is_none")]
343 pub sequence: Option<i32>,
344}
345
346#[derive(Debug, Deserialize)]
347struct RawEvent {
348 id: EventId,
349 #[serde(rename = "type")]
350 event_type: String,
351 ts: DateTime<Utc>,
352 session_id: SessionId,
353 context: EventContext,
354 data: serde_json::Value,
355 metadata: Option<serde_json::Value>,
356 tags: Option<Vec<String>>,
357 sequence: Option<i32>,
358}
359
360impl<'de> Deserialize<'de> for Event {
361 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
362 where
363 D: Deserializer<'de>,
364 {
365 let raw = RawEvent::deserialize(deserializer)?;
366 let data = deserialize_event_data(&raw.event_type, raw.data);
367 Ok(Self {
368 id: raw.id,
369 event_type: raw.event_type,
370 ts: raw.ts,
371 session_id: raw.session_id,
372 context: raw.context,
373 data,
374 metadata: raw.metadata,
375 tags: raw.tags,
376 sequence: raw.sequence,
377 })
378 }
379}
380
381impl Event {
382 pub fn new(session_id: SessionId, context: EventContext, data: impl Into<EventData>) -> Self {
386 let data = data.into();
387 let event_type = data.event_type().to_string();
388 Self {
389 id: EventId::new(),
390 event_type,
391 ts: Utc::now(),
392 session_id,
393 context,
394 data,
395 metadata: None,
396 tags: None,
397 sequence: None,
398 }
399 }
400
401 pub fn with_id(
403 id: EventId,
404 session_id: SessionId,
405 context: EventContext,
406 data: impl Into<EventData>,
407 ) -> Self {
408 let data = data.into();
409 let event_type = data.event_type().to_string();
410 Self {
411 id,
412 event_type,
413 ts: Utc::now(),
414 session_id,
415 context,
416 data,
417 metadata: None,
418 tags: None,
419 sequence: None,
420 }
421 }
422
423 pub fn with_sequence(mut self, sequence: i32) -> Self {
425 self.sequence = Some(sequence);
426 self
427 }
428
429 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
431 self.metadata = Some(metadata);
432 self
433 }
434
435 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
437 self.tags = Some(tags);
438 self
439 }
440
441 pub fn session_uuid(&self) -> Uuid {
443 self.session_id.uuid()
444 }
445
446 pub fn is_message_event(&self) -> bool {
448 self.event_type == INPUT_MESSAGE || self.event_type == OUTPUT_MESSAGE_COMPLETED
449 }
450
451 pub fn is_ephemeral(&self) -> bool {
459 is_ephemeral_event_type(&self.event_type)
460 }
461
462 pub fn is_input_event(&self) -> bool {
464 self.event_type.starts_with("input.")
465 }
466
467 pub fn is_output_event(&self) -> bool {
469 self.event_type.starts_with("output.")
470 }
471
472 pub fn is_atom_event(&self) -> bool {
474 matches!(
475 self.event_type.as_str(),
476 REASON_STARTED
477 | REASON_COMPLETED
478 | REASON_RECOVERED
479 | ACT_STARTED
480 | ACT_COMPLETED
481 | TOOL_STARTED
482 | TOOL_COMPLETED
483 | TOOL_PROGRESS
484 | TOOL_CALL_REQUESTED
485 | TRANSCRIPT_REPAIRED
486 )
487 }
488
489 pub fn is_turn_event(&self) -> bool {
491 self.event_type.starts_with("turn.")
492 }
493
494 pub fn is_session_event(&self) -> bool {
496 self.event_type.starts_with("session.")
497 }
498
499 pub fn is_unsupported(&self) -> bool {
502 self.data.is_unsupported()
503 }
504}
505
506use crate::message::{ContentPart, Message};
511use crate::tool_narration::{
512 ToolNarrationPhase, render_group_headline_with_locale, render_tool_narration_with_locale,
513};
514use crate::tool_types::ToolCall;
515
516#[derive(Debug, Clone, Serialize, Deserialize)]
518#[cfg_attr(feature = "openapi", derive(ToSchema))]
519pub struct ModelMetadata {
520 pub model: String,
522
523 #[serde(skip_serializing_if = "Option::is_none")]
525 pub model_id: Option<Uuid>,
526
527 #[serde(skip_serializing_if = "Option::is_none")]
529 pub provider_id: Option<Uuid>,
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize, Default)]
559#[cfg_attr(feature = "openapi", derive(ToSchema))]
560pub struct TokenUsage {
561 pub input_tokens: u32,
564 pub output_tokens: u32,
566 #[serde(skip_serializing_if = "Option::is_none")]
568 pub cache_read_tokens: Option<u32>,
569 #[serde(skip_serializing_if = "Option::is_none")]
571 pub cache_creation_tokens: Option<u32>,
572
573 #[serde(skip_serializing_if = "Option::is_none")]
577 pub actual_cost_usd: Option<f64>,
578
579 #[serde(skip_serializing_if = "Option::is_none")]
584 pub estimated_cost_usd: Option<f64>,
585
586 #[serde(skip_serializing_if = "Option::is_none")]
589 pub effective_cost_usd: Option<f64>,
590}
591
592impl TokenUsage {
593 pub fn new(input_tokens: u32, output_tokens: u32) -> Self {
595 Self {
596 input_tokens,
597 output_tokens,
598 cache_read_tokens: None,
599 cache_creation_tokens: None,
600 actual_cost_usd: None,
601 estimated_cost_usd: None,
602 effective_cost_usd: None,
603 }
604 }
605
606 pub fn with_cache(
608 input_tokens: u32,
609 output_tokens: u32,
610 cache_read_tokens: Option<u32>,
611 cache_creation_tokens: Option<u32>,
612 ) -> Self {
613 Self {
614 input_tokens,
615 output_tokens,
616 cache_read_tokens,
617 cache_creation_tokens,
618 actual_cost_usd: None,
619 estimated_cost_usd: None,
620 effective_cost_usd: None,
621 }
622 }
623
624 pub fn with_cost(
628 mut self,
629 actual_cost_usd: Option<f64>,
630 estimated_cost_usd: Option<f64>,
631 ) -> Self {
632 self.actual_cost_usd = actual_cost_usd;
633 self.estimated_cost_usd = estimated_cost_usd;
634 self
635 }
636
637 pub fn with_effective_cost(mut self, effective_cost_usd: Option<f64>) -> Self {
641 self.effective_cost_usd = effective_cost_usd;
642 self
643 }
644
645 pub fn effective_cost_usd(&self) -> Option<f64> {
649 self.effective_cost_usd
650 .or(self.actual_cost_usd.or(self.estimated_cost_usd))
651 }
652
653 pub fn total_tokens(&self) -> u32 {
655 self.input_tokens + self.output_tokens
656 }
657
658 pub fn add(&mut self, other: &TokenUsage) {
660 self.input_tokens += other.input_tokens;
661 self.output_tokens += other.output_tokens;
662 if let Some(cache) = other.cache_read_tokens {
663 *self.cache_read_tokens.get_or_insert(0) += cache;
664 }
665 if let Some(cache) = other.cache_creation_tokens {
666 *self.cache_creation_tokens.get_or_insert(0) += cache;
667 }
668 if let Some(cost) = other.actual_cost_usd {
669 *self.actual_cost_usd.get_or_insert(0.0) += cost;
670 }
671 if let Some(cost) = other.estimated_cost_usd {
672 *self.estimated_cost_usd.get_or_insert(0.0) += cost;
673 }
674 if let Some(cost) = other.effective_cost_usd() {
675 *self.effective_cost_usd.get_or_insert(0.0) += cost;
676 }
677 }
678}
679
680#[cfg(test)]
681mod token_usage_tests {
682 use super::TokenUsage;
683
684 #[test]
685 fn aggregate_effective_cost_preserves_mixed_actual_and_estimated_total() {
686 let aggregate = TokenUsage::with_cache(30, 15, None, None)
687 .with_cost(Some(1.0), Some(2.0))
688 .with_effective_cost(Some(3.0));
689
690 assert_eq!(aggregate.actual_cost_usd, Some(1.0));
691 assert_eq!(aggregate.estimated_cost_usd, Some(2.0));
692 assert_eq!(aggregate.effective_cost_usd(), Some(3.0));
693 }
694
695 #[test]
696 fn generation_effective_cost_still_prefers_actual_over_estimated() {
697 let generation = TokenUsage::new(10, 5).with_cost(Some(1.0), Some(2.0));
698
699 assert_eq!(generation.effective_cost_usd(), Some(1.0));
700 }
701}
702
703#[derive(Debug, Clone, Serialize, Deserialize)]
705#[cfg_attr(feature = "openapi", derive(ToSchema))]
706pub struct InputMessageData {
707 pub message: Message,
709}
710
711impl InputMessageData {
712 pub fn new(message: Message) -> Self {
713 Self { message }
714 }
715}
716
717#[derive(Debug, Clone, Serialize, Deserialize)]
726#[cfg_attr(feature = "openapi", derive(ToSchema))]
727pub struct OutputMessageStartedData {
728 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
730 pub turn_id: TurnId,
731
732 #[serde(skip_serializing_if = "Option::is_none")]
734 pub model: Option<String>,
735
736 #[serde(skip_serializing_if = "Option::is_none")]
739 pub iteration: Option<u32>,
740}
741
742#[derive(Debug, Clone, Serialize, Deserialize)]
747#[cfg_attr(feature = "openapi", derive(ToSchema))]
748pub struct OutputMessageDeltaData {
749 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
751 pub turn_id: TurnId,
752
753 pub delta: String,
755
756 pub accumulated: String,
758}
759
760#[derive(Debug, Clone, Serialize, Deserialize)]
762#[cfg_attr(feature = "openapi", derive(ToSchema))]
763pub struct OutputMessageCompletedData {
764 pub message: Message,
766
767 #[serde(skip_serializing_if = "Option::is_none")]
769 pub metadata: Option<ModelMetadata>,
770
771 #[serde(skip_serializing_if = "Option::is_none")]
773 pub usage: Option<TokenUsage>,
774
775 #[serde(default, skip_serializing_if = "Option::is_none")]
777 pub error_code: Option<String>,
778
779 #[serde(default, skip_serializing_if = "Option::is_none")]
781 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
782 pub error_fields: Option<UserFacingErrorFields>,
783
784 #[serde(default, skip_serializing_if = "Option::is_none")]
788 pub error_disclosure: Option<String>,
789}
790
791impl OutputMessageCompletedData {
792 pub fn new(message: Message) -> Self {
793 Self {
794 message,
795 metadata: None,
796 usage: None,
797 error_code: None,
798 error_fields: None,
799 error_disclosure: None,
800 }
801 }
802
803 pub fn with_metadata(mut self, metadata: ModelMetadata) -> Self {
804 self.metadata = Some(metadata);
805 self
806 }
807
808 pub fn with_usage(mut self, usage: TokenUsage) -> Self {
809 self.usage = Some(usage);
810 self
811 }
812
813 pub fn with_user_facing_error(mut self, error: &UserFacingError) -> Self {
814 error.apply_to_event_fields(&mut self.error_code, &mut self.error_fields);
815 self
816 }
817
818 pub fn with_error_disclosure(mut self, mode: crate::ErrorDisclosure) -> Self {
819 self.error_disclosure = Some(mode.as_str().to_string());
820 self
821 }
822}
823
824#[derive(Debug, Clone, Serialize, Deserialize)]
831#[cfg_attr(feature = "openapi", derive(ToSchema))]
832pub struct OutputMessageReplacedData {
833 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
835 pub turn_id: TurnId,
836
837 pub guardrail_capability_id: String,
840
841 pub guardrail_id: String,
843
844 pub reason_code: String,
847
848 pub replacement: String,
850}
851
852#[derive(Debug, Clone, Serialize, Deserialize)]
858#[cfg_attr(feature = "openapi", derive(ToSchema))]
859pub struct ReasonStartedData {
860 pub harness_id: HarnessId,
862
863 #[serde(skip_serializing_if = "Option::is_none")]
865 pub agent_id: Option<AgentId>,
866
867 #[serde(skip_serializing_if = "Option::is_none")]
869 pub metadata: Option<ModelMetadata>,
870}
871
872#[derive(Debug, Clone, Serialize, Deserialize)]
874#[cfg_attr(feature = "openapi", derive(ToSchema))]
875pub struct ReasonCompletedData {
876 pub success: bool,
878
879 #[serde(skip_serializing_if = "Option::is_none")]
881 pub text_preview: Option<String>,
882
883 pub has_tool_calls: bool,
885
886 pub tool_call_count: u32,
888
889 #[serde(skip_serializing_if = "Option::is_none")]
891 pub error: Option<String>,
892
893 #[serde(skip_serializing_if = "Option::is_none")]
895 pub duration_ms: Option<u64>,
896
897 #[serde(skip_serializing_if = "Option::is_none")]
899 pub usage: Option<TokenUsage>,
900}
901
902impl ReasonCompletedData {
903 pub fn success(
904 text: &str,
905 has_tool_calls: bool,
906 tool_call_count: u32,
907 duration_ms: Option<u64>,
908 usage: Option<TokenUsage>,
909 ) -> Self {
910 let text_preview = if text.is_empty() {
911 None
912 } else {
913 Some(text.chars().take(200).collect())
914 };
915
916 Self {
917 success: true,
918 text_preview,
919 has_tool_calls,
920 tool_call_count,
921 error: None,
922 duration_ms,
923 usage,
924 }
925 }
926
927 pub fn failure(error: String, duration_ms: Option<u64>) -> Self {
928 Self {
929 success: false,
930 text_preview: None,
931 has_tool_calls: false,
932 tool_call_count: 0,
933 error: Some(error),
934 duration_ms,
935 usage: None,
936 }
937 }
938}
939
940#[derive(Debug, Clone, Serialize, Deserialize)]
942#[cfg_attr(feature = "openapi", derive(ToSchema))]
943#[serde(rename_all = "snake_case")]
944pub enum RecoveryMode {
945 Finalize,
948 Restart,
950}
951
952#[derive(Debug, Clone, Serialize, Deserialize)]
958#[cfg_attr(feature = "openapi", derive(ToSchema))]
959pub struct ReasonRecoveredData {
960 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
962 pub turn_id: TurnId,
963
964 pub mode: RecoveryMode,
966
967 pub accumulated_len: usize,
969}
970
971#[derive(Debug, Clone, Serialize, Deserialize)]
973#[serde(rename_all = "snake_case")]
974#[cfg_attr(feature = "openapi", derive(ToSchema))]
975pub enum CapabilityUsageKind {
976 Configured,
977 Resolved,
978 Exposed,
979 Invoked,
980 EffectRan,
981}
982
983#[derive(Debug, Clone, Serialize, Deserialize)]
987#[cfg_attr(feature = "openapi", derive(ToSchema))]
988pub struct CapabilityUsageRecord {
989 pub capability_id: String,
991 #[serde(default, skip_serializing_if = "Option::is_none")]
993 pub capability_name: Option<String>,
994 pub usage_kind: CapabilityUsageKind,
996 #[serde(default, skip_serializing_if = "Option::is_none")]
998 pub tool_name: Option<String>,
999 #[serde(default, skip_serializing_if = "Option::is_none")]
1001 pub usage_count: Option<u64>,
1002 #[serde(default, skip_serializing_if = "Option::is_none")]
1004 pub duration_ms: Option<u64>,
1005}
1006
1007#[derive(Debug, Clone, Serialize, Deserialize)]
1009#[cfg_attr(feature = "openapi", derive(ToSchema))]
1010pub struct CapabilityUsageData {
1011 pub records: Vec<CapabilityUsageRecord>,
1012}
1013
1014#[derive(Debug, Clone, Serialize, Deserialize)]
1016#[cfg_attr(feature = "openapi", derive(ToSchema))]
1017pub struct ToolCallSummary {
1018 pub id: String,
1019 pub name: String,
1020 #[serde(default, skip_serializing_if = "Option::is_none")]
1022 pub display_name: Option<String>,
1023 #[serde(default, skip_serializing_if = "Option::is_none")]
1025 pub narration: Option<String>,
1026}
1027
1028impl From<&ToolCall> for ToolCallSummary {
1029 fn from(tc: &ToolCall) -> Self {
1030 Self {
1031 id: tc.id.clone(),
1032 name: tc.name.clone(),
1033 display_name: None,
1034 narration: None,
1035 }
1036 }
1037}
1038
1039#[derive(Debug, Clone, Serialize, Deserialize)]
1041#[cfg_attr(feature = "openapi", derive(ToSchema))]
1042pub struct ToolDefinitionSummary {
1043 pub name: String,
1045 #[serde(default, skip_serializing_if = "Option::is_none")]
1047 pub display_name: Option<String>,
1048 #[serde(default, skip_serializing_if = "Option::is_none")]
1050 pub category: Option<String>,
1051 #[serde(default, skip_serializing_if = "Option::is_none")]
1053 pub capability_id: Option<String>,
1054 #[serde(default, skip_serializing_if = "Option::is_none")]
1056 pub capability_name: Option<String>,
1057 pub description: String,
1059}
1060
1061impl From<&crate::tool_types::ToolDefinition> for ToolDefinitionSummary {
1062 fn from(tool: &crate::tool_types::ToolDefinition) -> Self {
1063 let capability_attribution = tool.capability_attribution();
1064 Self {
1065 name: tool.name().to_string(),
1066 display_name: tool.display_name().map(|s| s.to_string()),
1067 category: tool.category().map(|s| s.to_string()),
1068 capability_id: capability_attribution.map(|(id, _)| id.to_string()),
1069 capability_name: capability_attribution.and_then(|(_, name)| name.map(str::to_string)),
1070 description: tool.description().to_string(),
1071 }
1072 }
1073}
1074
1075#[derive(Debug, Clone, Serialize, Deserialize)]
1077#[cfg_attr(feature = "openapi", derive(ToSchema))]
1078pub struct ActStartedData {
1079 pub tool_calls: Vec<ToolCallSummary>,
1081 #[serde(default, skip_serializing_if = "Option::is_none")]
1083 pub headline: Option<String>,
1084}
1085
1086impl ActStartedData {
1087 pub fn new(tool_calls: &[ToolCall]) -> Self {
1088 Self::new_with_locale(tool_calls, None)
1089 }
1090
1091 pub fn new_with_locale(tool_calls: &[ToolCall], locale: Option<&str>) -> Self {
1092 Self {
1093 tool_calls: tool_calls.iter().map(ToolCallSummary::from).collect(),
1094 headline: render_group_headline_with_locale(
1095 tool_calls,
1096 &[],
1097 ToolNarrationPhase::Started,
1098 locale,
1099 ),
1100 }
1101 }
1102
1103 pub fn with_definitions(
1105 tool_calls: &[ToolCall],
1106 tool_defs: &[crate::tool_types::ToolDefinition],
1107 ) -> Self {
1108 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
1109 }
1110
1111 pub fn with_definitions_and_locale(
1112 tool_calls: &[ToolCall],
1113 tool_defs: &[crate::tool_types::ToolDefinition],
1114 locale: Option<&str>,
1115 ) -> Self {
1116 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
1117 tool_defs.iter().map(|d| (d.name(), d)).collect();
1118 Self {
1119 tool_calls: tool_calls
1120 .iter()
1121 .map(|tc| {
1122 let tool_def = def_map.get(tc.name.as_str()).copied();
1123 let display_name = localized_tool_display_name(
1124 &tc.name,
1125 tool_def.and_then(|d| d.display_name()),
1126 locale,
1127 );
1128 ToolCallSummary {
1129 id: tc.id.clone(),
1130 name: tc.name.clone(),
1131 display_name,
1132 narration: Some(render_tool_narration_with_locale(
1133 tool_def,
1134 tc,
1135 ToolNarrationPhase::Started,
1136 locale,
1137 )),
1138 }
1139 })
1140 .collect(),
1141 headline: render_group_headline_with_locale(
1142 tool_calls,
1143 tool_defs,
1144 ToolNarrationPhase::Started,
1145 locale,
1146 ),
1147 }
1148 }
1149}
1150
1151#[derive(Debug, Clone, Serialize, Deserialize)]
1153#[cfg_attr(feature = "openapi", derive(ToSchema))]
1154pub struct ActCompletedData {
1155 pub completed: bool,
1157
1158 pub success_count: u32,
1160
1161 pub error_count: u32,
1163
1164 #[serde(skip_serializing_if = "Option::is_none")]
1166 pub duration_ms: Option<u64>,
1167 #[serde(default, skip_serializing_if = "Option::is_none")]
1169 pub headline: Option<String>,
1170}
1171
1172#[derive(Debug, Clone, Serialize, Deserialize)]
1174#[cfg_attr(feature = "openapi", derive(ToSchema))]
1175pub struct ToolStartedData {
1176 pub tool_call: ToolCall,
1178 #[serde(default, skip_serializing_if = "Option::is_none")]
1180 pub tool_call_fingerprint: Option<String>,
1181 #[serde(default, skip_serializing_if = "Option::is_none")]
1183 pub display_name: Option<String>,
1184 #[serde(default, skip_serializing_if = "Option::is_none")]
1186 pub narration: Option<String>,
1187}
1188
1189#[derive(Debug, Clone, Serialize, Deserialize)]
1191#[cfg_attr(feature = "openapi", derive(ToSchema))]
1192pub struct ToolCompletedData {
1193 pub tool_call_id: String,
1195
1196 pub tool_name: String,
1198
1199 #[serde(default, skip_serializing_if = "Option::is_none")]
1201 pub tool_call_fingerprint: Option<String>,
1202
1203 #[serde(default, skip_serializing_if = "Option::is_none")]
1205 pub tool_result_fingerprint: Option<String>,
1206
1207 #[serde(default, skip_serializing_if = "Option::is_none")]
1209 pub display_name: Option<String>,
1210
1211 pub success: bool,
1213
1214 pub status: String,
1216
1217 #[serde(skip_serializing_if = "Option::is_none")]
1219 pub result: Option<Vec<ContentPart>>,
1220
1221 #[serde(skip_serializing_if = "Option::is_none")]
1223 pub error: Option<String>,
1224
1225 #[serde(skip_serializing_if = "Option::is_none")]
1227 pub duration_ms: Option<u64>,
1228
1229 #[serde(default, skip_serializing_if = "Option::is_none")]
1231 pub capability_id: Option<String>,
1232
1233 #[serde(default, skip_serializing_if = "Option::is_none")]
1235 pub capability_name: Option<String>,
1236
1237 #[serde(default, skip_serializing_if = "Option::is_none")]
1239 pub narration: Option<String>,
1240}
1241
1242impl ToolCompletedData {
1243 pub fn success(
1244 tool_call_id: String,
1245 tool_name: String,
1246 result: Vec<ContentPart>,
1247 duration_ms: Option<u64>,
1248 ) -> Self {
1249 Self {
1250 tool_call_id,
1251 tool_name,
1252 tool_call_fingerprint: None,
1253 tool_result_fingerprint: None,
1254 display_name: None,
1255 success: true,
1256 status: "success".to_string(),
1257 result: Some(result),
1258 error: None,
1259 duration_ms,
1260 capability_id: None,
1261 capability_name: None,
1262 narration: None,
1263 }
1264 }
1265
1266 pub fn failure(
1267 tool_call_id: String,
1268 tool_name: String,
1269 status: String,
1270 error: String,
1271 duration_ms: Option<u64>,
1272 ) -> Self {
1273 Self {
1274 tool_call_id,
1275 tool_name,
1276 tool_call_fingerprint: None,
1277 tool_result_fingerprint: None,
1278 display_name: None,
1279 success: false,
1280 status,
1281 result: None,
1282 error: Some(error),
1283 duration_ms,
1284 capability_id: None,
1285 capability_name: None,
1286 narration: None,
1287 }
1288 }
1289
1290 pub fn with_display_name(mut self, display_name: Option<String>) -> Self {
1292 self.display_name = display_name;
1293 self
1294 }
1295
1296 pub fn with_fingerprints(
1297 mut self,
1298 tool_call_fingerprint: String,
1299 tool_result_fingerprint: String,
1300 ) -> Self {
1301 self.tool_call_fingerprint = Some(tool_call_fingerprint);
1302 self.tool_result_fingerprint = Some(tool_result_fingerprint);
1303 self
1304 }
1305
1306 pub fn with_narration(mut self, narration: Option<String>) -> Self {
1308 self.narration = narration;
1309 self
1310 }
1311
1312 pub fn with_capability_attribution(
1314 mut self,
1315 capability_id: Option<String>,
1316 capability_name: Option<String>,
1317 ) -> Self {
1318 self.capability_id = capability_id;
1319 self.capability_name = capability_name;
1320 self
1321 }
1322}
1323
1324#[derive(Debug, Clone, Serialize, Deserialize)]
1330#[cfg_attr(feature = "openapi", derive(ToSchema))]
1331pub struct ToolProgressData {
1332 pub tool_call_id: String,
1334
1335 pub tool_name: String,
1337
1338 pub message: String,
1340
1341 #[serde(default, skip_serializing_if = "Option::is_none")]
1343 pub display_name: Option<String>,
1344}
1345
1346#[derive(Debug, Clone, Serialize, Deserialize)]
1356#[cfg_attr(feature = "openapi", derive(ToSchema))]
1357pub struct ToolOutputDeltaData {
1358 pub tool_call_id: String,
1360
1361 pub tool_name: String,
1363
1364 pub delta: String,
1366
1367 pub stream: String,
1369}
1370
1371#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1373#[cfg_attr(feature = "openapi", derive(ToSchema))]
1374#[serde(rename_all = "snake_case")]
1375pub enum TranscriptRepairAction {
1376 Replay,
1378 Synthesize,
1380}
1381
1382#[derive(Debug, Clone, Serialize, Deserialize)]
1388#[cfg_attr(feature = "openapi", derive(ToSchema))]
1389pub struct TranscriptRepairedData {
1390 pub tool_call_id: String,
1392
1393 #[serde(default, skip_serializing_if = "Option::is_none")]
1395 pub tool_name: Option<String>,
1396
1397 pub action: TranscriptRepairAction,
1399}
1400
1401#[derive(Debug, Clone, Serialize, Deserialize)]
1407#[cfg_attr(feature = "openapi", derive(ToSchema))]
1408pub struct ToolCallRepairedData {
1409 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1411 pub turn_id: TurnId,
1412
1413 pub tool_call_id: String,
1415
1416 pub tool_name: String,
1418
1419 pub outcome: String,
1421}
1422
1423#[derive(Debug, Clone, Serialize, Deserialize)]
1428#[cfg_attr(feature = "openapi", derive(ToSchema))]
1429pub struct ToolCallRequestedData {
1430 pub tool_calls: Vec<ToolCall>,
1432 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1434 pub tool_summaries: Vec<ToolCallSummary>,
1435 #[serde(default, skip_serializing_if = "Option::is_none")]
1437 pub headline: Option<String>,
1438}
1439
1440impl ToolCallRequestedData {
1441 pub fn with_definitions(
1442 tool_calls: &[ToolCall],
1443 tool_defs: &[crate::tool_types::ToolDefinition],
1444 ) -> Self {
1445 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
1446 }
1447
1448 pub fn with_definitions_and_locale(
1449 tool_calls: &[ToolCall],
1450 tool_defs: &[crate::tool_types::ToolDefinition],
1451 locale: Option<&str>,
1452 ) -> Self {
1453 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
1454 tool_defs.iter().map(|d| (d.name(), d)).collect();
1455
1456 let tool_summaries = tool_calls
1457 .iter()
1458 .map(|tool_call| {
1459 let tool_def = def_map.get(tool_call.name.as_str()).copied();
1460 ToolCallSummary {
1461 id: tool_call.id.clone(),
1462 name: tool_call.name.clone(),
1463 display_name: localized_tool_display_name(
1464 &tool_call.name,
1465 tool_def.and_then(|def| def.display_name()),
1466 locale,
1467 ),
1468 narration: Some(render_tool_narration_with_locale(
1469 tool_def,
1470 tool_call,
1471 ToolNarrationPhase::Waiting,
1472 locale,
1473 )),
1474 }
1475 })
1476 .collect();
1477
1478 Self {
1479 tool_calls: tool_calls.to_vec(),
1480 tool_summaries,
1481 headline: render_group_headline_with_locale(
1482 tool_calls,
1483 tool_defs,
1484 ToolNarrationPhase::Waiting,
1485 locale,
1486 ),
1487 }
1488 }
1489}
1490
1491#[derive(Debug, Clone, Serialize, Deserialize)]
1497#[cfg_attr(feature = "openapi", derive(ToSchema))]
1498pub struct LlmGenerationOutput {
1499 #[serde(skip_serializing_if = "Option::is_none")]
1501 pub text: Option<String>,
1502
1503 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1505 pub tool_calls: Vec<ToolCall>,
1506}
1507
1508#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1513#[cfg_attr(feature = "openapi", derive(ToSchema))]
1514pub struct LlmRequestOptions {
1515 #[serde(skip_serializing_if = "Option::is_none")]
1517 pub prompt_cache: Option<LlmPromptCacheInfo>,
1518 #[serde(skip_serializing_if = "Option::is_none")]
1520 pub tool_search: Option<LlmToolSearchInfo>,
1521 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1523 pub provider_options: HashMap<String, Value>,
1524 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1527 pub metadata: HashMap<String, String>,
1528}
1529
1530impl LlmRequestOptions {
1531 pub fn is_empty(&self) -> bool {
1532 self.prompt_cache.is_none()
1533 && self.tool_search.is_none()
1534 && self.provider_options.is_empty()
1535 && self.metadata.is_empty()
1536 }
1537}
1538
1539#[derive(Debug, Clone, Serialize, Deserialize)]
1541#[cfg_attr(feature = "openapi", derive(ToSchema))]
1542pub struct LlmPromptCacheInfo {
1543 pub enabled: bool,
1545 pub strategy: crate::driver_registry::PromptCacheStrategy,
1547 #[serde(skip_serializing_if = "Option::is_none")]
1549 pub provider_mode: Option<String>,
1550}
1551
1552#[derive(Debug, Clone, Serialize, Deserialize)]
1554#[cfg_attr(feature = "openapi", derive(ToSchema))]
1555pub struct LlmToolSearchInfo {
1556 pub enabled: bool,
1558 pub threshold: usize,
1560}
1561
1562#[derive(Debug, Clone, Serialize, Deserialize)]
1564#[cfg_attr(feature = "openapi", derive(ToSchema))]
1565pub struct LlmGenerationMetadata {
1566 #[cfg_attr(feature = "openapi", schema(example = "claude-sonnet-4-5"))]
1568 pub model: String,
1569
1570 #[serde(skip_serializing_if = "Option::is_none")]
1572 #[cfg_attr(feature = "openapi", schema(example = "anthropic"))]
1573 pub provider: Option<String>,
1574
1575 #[serde(skip_serializing_if = "Option::is_none")]
1577 pub usage: Option<TokenUsage>,
1578
1579 #[serde(skip_serializing_if = "Option::is_none")]
1581 #[cfg_attr(feature = "openapi", schema(example = 1_842u64))]
1582 pub duration_ms: Option<u64>,
1583
1584 #[serde(skip_serializing_if = "Option::is_none")]
1586 #[cfg_attr(feature = "openapi", schema(example = 312u64))]
1587 pub time_to_first_token_ms: Option<u64>,
1588
1589 #[cfg_attr(feature = "openapi", schema(example = true))]
1591 pub success: bool,
1592
1593 #[serde(skip_serializing_if = "Option::is_none")]
1595 #[cfg_attr(feature = "openapi", schema(example = "provider returned 503"))]
1596 pub error: Option<String>,
1597
1598 #[serde(skip_serializing_if = "Option::is_none")]
1601 #[cfg_attr(feature = "openapi", schema(example = json!(["tool_calls"])))]
1602 pub finish_reasons: Option<Vec<String>>,
1603
1604 #[serde(skip_serializing_if = "Option::is_none")]
1607 #[cfg_attr(feature = "openapi", schema(example = "msg_01ABCDef0123456789"))]
1608 pub response_id: Option<String>,
1609
1610 #[serde(skip_serializing_if = "Option::is_none")]
1613 pub retry: Option<LlmRetryInfo>,
1614
1615 #[serde(skip_serializing_if = "Option::is_none")]
1618 pub compaction: Option<LlmCompactionInfo>,
1619
1620 #[serde(skip_serializing_if = "Option::is_none")]
1622 pub request_options: Option<LlmRequestOptions>,
1623}
1624
1625#[derive(Debug, Clone, Serialize, Deserialize)]
1627#[cfg_attr(feature = "openapi", derive(ToSchema))]
1628pub struct LlmRetryInfo {
1629 pub attempts: u32,
1631
1632 pub total_wait_ms: u64,
1634}
1635
1636#[derive(Debug, Clone, Serialize, Deserialize)]
1641#[cfg_attr(feature = "openapi", derive(ToSchema))]
1642pub struct LlmCompactionInfo {
1643 pub compacted: bool,
1645
1646 #[serde(skip_serializing_if = "Option::is_none")]
1648 pub input_tokens_before: Option<u32>,
1649
1650 #[serde(skip_serializing_if = "Option::is_none")]
1652 pub input_tokens_after: Option<u32>,
1653
1654 #[serde(skip_serializing_if = "Option::is_none")]
1656 pub duration_ms: Option<u64>,
1657}
1658
1659impl LlmCompactionInfo {
1660 pub fn new(
1662 input_tokens_before: Option<u32>,
1663 input_tokens_after: Option<u32>,
1664 duration_ms: Option<u64>,
1665 ) -> Self {
1666 Self {
1667 compacted: true,
1668 input_tokens_before,
1669 input_tokens_after,
1670 duration_ms,
1671 }
1672 }
1673}
1674
1675#[derive(Debug, Clone, Serialize, Deserialize)]
1680#[cfg_attr(feature = "openapi", derive(ToSchema))]
1681pub struct LlmGenerationData {
1682 pub messages: Vec<Message>,
1684
1685 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1687 pub tools: Vec<ToolDefinitionSummary>,
1688
1689 pub output: LlmGenerationOutput,
1691
1692 pub metadata: LlmGenerationMetadata,
1694}
1695
1696impl LlmGenerationData {
1697 #[allow(clippy::too_many_arguments)]
1699 pub fn success(
1700 messages: Vec<Message>,
1701 tools: Vec<ToolDefinitionSummary>,
1702 text: Option<String>,
1703 tool_calls: Vec<ToolCall>,
1704 model: String,
1705 provider: Option<String>,
1706 usage: Option<TokenUsage>,
1707 duration_ms: Option<u64>,
1708 time_to_first_token_ms: Option<u64>,
1709 ) -> Self {
1710 let finish_reasons = if !tool_calls.is_empty() {
1712 Some(vec!["tool_calls".to_string()])
1713 } else {
1714 Some(vec!["stop".to_string()])
1715 };
1716
1717 Self {
1718 messages,
1719 tools,
1720 output: LlmGenerationOutput { text, tool_calls },
1721 metadata: LlmGenerationMetadata {
1722 model,
1723 provider,
1724 usage,
1725 duration_ms,
1726 time_to_first_token_ms,
1727 success: true,
1728 error: None,
1729 finish_reasons,
1730 response_id: None,
1731 retry: None,
1732 compaction: None,
1733 request_options: None,
1734 },
1735 }
1736 }
1737
1738 #[allow(clippy::too_many_arguments)]
1740 pub fn success_with_metadata(
1741 messages: Vec<Message>,
1742 tools: Vec<ToolDefinitionSummary>,
1743 text: Option<String>,
1744 tool_calls: Vec<ToolCall>,
1745 model: String,
1746 provider: Option<String>,
1747 usage: Option<TokenUsage>,
1748 duration_ms: Option<u64>,
1749 time_to_first_token_ms: Option<u64>,
1750 finish_reasons: Option<Vec<String>>,
1751 response_id: Option<String>,
1752 ) -> Self {
1753 Self {
1754 messages,
1755 tools,
1756 output: LlmGenerationOutput { text, tool_calls },
1757 metadata: LlmGenerationMetadata {
1758 model,
1759 provider,
1760 usage,
1761 duration_ms,
1762 time_to_first_token_ms,
1763 success: true,
1764 error: None,
1765 finish_reasons,
1766 response_id,
1767 retry: None,
1768 compaction: None,
1769 request_options: None,
1770 },
1771 }
1772 }
1773
1774 #[allow(clippy::too_many_arguments)]
1776 pub fn success_with_retry(
1777 messages: Vec<Message>,
1778 tools: Vec<ToolDefinitionSummary>,
1779 text: Option<String>,
1780 tool_calls: Vec<ToolCall>,
1781 model: String,
1782 provider: Option<String>,
1783 usage: Option<TokenUsage>,
1784 duration_ms: Option<u64>,
1785 time_to_first_token_ms: Option<u64>,
1786 finish_reasons: Option<Vec<String>>,
1787 response_id: Option<String>,
1788 retry: Option<LlmRetryInfo>,
1789 ) -> Self {
1790 Self {
1791 messages,
1792 tools,
1793 output: LlmGenerationOutput { text, tool_calls },
1794 metadata: LlmGenerationMetadata {
1795 model,
1796 provider,
1797 usage,
1798 duration_ms,
1799 time_to_first_token_ms,
1800 success: true,
1801 error: None,
1802 finish_reasons,
1803 response_id,
1804 retry,
1805 compaction: None,
1806 request_options: None,
1807 },
1808 }
1809 }
1810
1811 pub fn failure(
1813 messages: Vec<Message>,
1814 tools: Vec<ToolDefinitionSummary>,
1815 model: String,
1816 provider: Option<String>,
1817 error: String,
1818 duration_ms: Option<u64>,
1819 time_to_first_token_ms: Option<u64>,
1820 ) -> Self {
1821 Self {
1822 messages,
1823 tools,
1824 output: LlmGenerationOutput {
1825 text: None,
1826 tool_calls: vec![],
1827 },
1828 metadata: LlmGenerationMetadata {
1829 model,
1830 provider,
1831 usage: None,
1832 duration_ms,
1833 time_to_first_token_ms,
1834 success: false,
1835 error: Some(error),
1836 finish_reasons: Some(vec!["error".to_string()]),
1837 response_id: None,
1838 retry: None,
1839 compaction: None,
1840 request_options: None,
1841 },
1842 }
1843 }
1844
1845 pub fn with_compaction(mut self, compaction: LlmCompactionInfo) -> Self {
1849 self.metadata.compaction = Some(compaction);
1850 self
1851 }
1852
1853 pub fn with_retry(mut self, retry: LlmRetryInfo) -> Self {
1855 self.metadata.retry = Some(retry);
1856 self
1857 }
1858
1859 pub fn with_request_options(mut self, request_options: LlmRequestOptions) -> Self {
1861 if !request_options.is_empty() {
1862 self.metadata.request_options = Some(request_options);
1863 }
1864 self
1865 }
1866}
1867
1868#[derive(Debug, Clone, Serialize, Deserialize)]
1878#[cfg_attr(feature = "openapi", derive(ToSchema))]
1879pub struct ReasonThinkingStartedData {
1880 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1882 pub turn_id: TurnId,
1883
1884 #[serde(skip_serializing_if = "Option::is_none")]
1886 pub model: Option<String>,
1887}
1888
1889#[derive(Debug, Clone, Serialize, Deserialize)]
1895#[cfg_attr(feature = "openapi", derive(ToSchema))]
1896pub struct ReasonThinkingDeltaData {
1897 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1899 pub turn_id: TurnId,
1900
1901 pub delta: String,
1903
1904 pub accumulated: String,
1906}
1907
1908#[derive(Debug, Clone, Serialize, Deserialize)]
1913#[cfg_attr(feature = "openapi", derive(ToSchema))]
1914pub struct ReasonThinkingCompletedData {
1915 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1917 pub turn_id: TurnId,
1918
1919 pub thinking: String,
1921}
1922
1923#[derive(Debug, Clone, Serialize, Deserialize)]
1931#[cfg_attr(feature = "openapi", derive(ToSchema))]
1932pub struct ReasonItemData {
1933 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1935 pub turn_id: TurnId,
1936
1937 pub provider: String,
1939
1940 #[serde(skip_serializing_if = "Option::is_none")]
1942 pub model: Option<String>,
1943
1944 pub item_id: String,
1946
1947 #[serde(skip_serializing_if = "Option::is_none")]
1949 pub encrypted_content: Option<String>,
1950
1951 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1954 pub summary: Vec<String>,
1955
1956 #[serde(skip_serializing_if = "Option::is_none")]
1958 pub token_count: Option<u32>,
1959}
1960
1961#[derive(Debug, Clone, Serialize, Deserialize)]
1967#[cfg_attr(feature = "openapi", derive(ToSchema))]
1968pub struct TurnStartedData {
1969 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1971 pub turn_id: TurnId,
1972
1973 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
1975 pub input_message_id: MessageId,
1976
1977 #[serde(skip_serializing_if = "Option::is_none")]
1979 pub input_content: Option<String>,
1980}
1981
1982#[derive(Debug, Clone, Serialize, Deserialize)]
1984#[cfg_attr(feature = "openapi", derive(ToSchema))]
1985pub struct TurnCompletedData {
1986 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1988 pub turn_id: TurnId,
1989
1990 pub iterations: u32,
1992
1993 #[serde(skip_serializing_if = "Option::is_none")]
1995 pub duration_ms: Option<u64>,
1996
1997 #[serde(skip_serializing_if = "Option::is_none")]
1999 pub usage: Option<TokenUsage>,
2000
2001 #[serde(skip_serializing_if = "Option::is_none")]
2003 pub input_content: Option<String>,
2004
2005 #[serde(skip_serializing_if = "Option::is_none")]
2007 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "message_01933b5a00007000800000000000001"))]
2008 pub final_message_id: Option<MessageId>,
2009
2010 #[serde(skip_serializing_if = "Option::is_none")]
2012 pub final_answer_preview: Option<String>,
2013
2014 #[serde(skip_serializing_if = "Option::is_none")]
2016 pub time_to_first_token_ms: Option<u64>,
2017
2018 #[serde(skip_serializing_if = "Option::is_none")]
2020 pub tool_call_count: Option<u32>,
2021
2022 #[serde(skip_serializing_if = "Option::is_none")]
2024 pub llm_call_count: Option<u32>,
2025
2026 #[serde(skip_serializing_if = "Option::is_none")]
2028 pub status: Option<String>,
2029}
2030
2031#[derive(Debug, Clone, Serialize, Deserialize)]
2033#[cfg_attr(feature = "openapi", derive(ToSchema))]
2034pub struct TurnFailedData {
2035 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2037 pub turn_id: TurnId,
2038
2039 pub error: String,
2041
2042 #[serde(default, skip_serializing_if = "Option::is_none")]
2044 pub error_code: Option<String>,
2045
2046 #[serde(default, skip_serializing_if = "Option::is_none")]
2048 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
2049 pub error_fields: Option<UserFacingErrorFields>,
2050
2051 #[serde(default, skip_serializing_if = "Option::is_none")]
2055 pub error_disclosure: Option<String>,
2056}
2057
2058#[derive(Debug, Clone, Serialize, Deserialize)]
2065#[cfg_attr(feature = "openapi", derive(ToSchema))]
2066pub struct TurnSealedData {
2067 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2069 pub turn_id: TurnId,
2070
2071 pub reason: String,
2074
2075 #[serde(default, skip_serializing_if = "Option::is_none")]
2077 pub detail: Option<String>,
2078
2079 #[serde(default, skip_serializing_if = "Option::is_none")]
2081 pub iterations: Option<u32>,
2082
2083 #[serde(default, skip_serializing_if = "Option::is_none")]
2085 pub usage: Option<TokenUsage>,
2086}
2087
2088#[derive(Debug, Clone, Serialize, Deserialize)]
2090#[cfg_attr(feature = "openapi", derive(ToSchema))]
2091pub struct TurnCancelledData {
2092 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2094 pub turn_id: TurnId,
2095
2096 #[serde(skip_serializing_if = "Option::is_none")]
2098 pub reason: Option<String>,
2099
2100 #[serde(skip_serializing_if = "Option::is_none")]
2102 pub usage: Option<TokenUsage>,
2103}
2104
2105#[derive(Debug, Clone, Serialize, Deserialize)]
2111#[cfg_attr(feature = "openapi", derive(ToSchema))]
2112pub struct SessionStartedData {
2113 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "harness_01933b5a00007000800000000000001"))]
2115 pub harness_id: HarnessId,
2116
2117 #[serde(skip_serializing_if = "Option::is_none")]
2119 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agent_01933b5a00007000800000000000001"))]
2120 pub agent_id: Option<AgentId>,
2121
2122 #[serde(skip_serializing_if = "Option::is_none")]
2124 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
2125 pub model_id: Option<ModelId>,
2126}
2127
2128#[derive(Debug, Clone, Serialize, Deserialize)]
2130#[cfg_attr(feature = "openapi", derive(ToSchema))]
2131pub struct SessionActivatedData {
2132 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2134 pub turn_id: TurnId,
2135
2136 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
2138 pub input_message_id: MessageId,
2139}
2140
2141#[derive(Debug, Clone, Serialize, Deserialize)]
2143#[cfg_attr(feature = "openapi", derive(ToSchema))]
2144pub struct SessionIdledData {
2145 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2147 pub turn_id: TurnId,
2148
2149 #[serde(skip_serializing_if = "Option::is_none")]
2151 pub iterations: Option<u32>,
2152
2153 #[serde(skip_serializing_if = "Option::is_none")]
2155 pub usage: Option<TokenUsage>,
2156}
2157
2158#[derive(Debug, Clone, Serialize, Deserialize)]
2167#[cfg_attr(feature = "openapi", derive(ToSchema))]
2168pub struct SessionTaskEventData {
2169 pub task: crate::session_task::SessionTask,
2170}
2171
2172#[derive(Debug, Clone, Serialize, Deserialize)]
2174#[cfg_attr(feature = "openapi", derive(ToSchema))]
2175pub struct TaskMessageEventData {
2176 pub task_id: String,
2177 pub message: crate::session_task::TaskMessage,
2178}
2179
2180#[derive(Debug, Clone, Serialize, Deserialize)]
2186#[cfg_attr(feature = "openapi", derive(ToSchema))]
2187#[serde(rename_all = "snake_case")]
2188pub enum CompactionReason {
2189 ProactiveBudget,
2191 RequestTooLarge,
2193 Manual,
2195}
2196
2197impl std::fmt::Display for CompactionReason {
2198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2199 match self {
2200 Self::ProactiveBudget => write!(f, "proactive_budget"),
2201 Self::RequestTooLarge => write!(f, "request_too_large"),
2202 Self::Manual => write!(f, "manual"),
2203 }
2204 }
2205}
2206
2207#[derive(Debug, Clone, Serialize, Deserialize)]
2209#[cfg_attr(feature = "openapi", derive(ToSchema))]
2210pub struct ContextCompactingData {
2211 pub reason: CompactionReason,
2213 pub strategy: String,
2215 pub messages_before: usize,
2217}
2218
2219#[derive(Debug, Clone, Serialize, Deserialize)]
2221#[cfg_attr(feature = "openapi", derive(ToSchema))]
2222pub struct CompactionStepData {
2223 pub strategy: String,
2225 pub messages_after: usize,
2227 pub duration_ms: u64,
2229}
2230
2231#[derive(Debug, Clone, Serialize, Deserialize)]
2233#[cfg_attr(feature = "openapi", derive(ToSchema))]
2234pub struct ContextCompactedData {
2235 pub strategy_used: String,
2237 pub messages_before: usize,
2239 pub messages_after: usize,
2241 pub duration_ms: u64,
2243 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2245 pub steps: Vec<CompactionStepData>,
2246}
2247
2248#[derive(Debug, Clone, Serialize, Deserialize)]
2254#[cfg_attr(feature = "openapi", derive(ToSchema))]
2255pub struct FileWrittenData {
2256 pub path: String,
2258 pub operation: String,
2260 pub size_bytes: i64,
2262 pub created: bool,
2264}
2265
2266pub const FILE_OP_CREATE: &str = "create";
2268pub const FILE_OP_UPDATE: &str = "update";
2269
2270#[derive(Debug, Clone, Serialize, Deserialize)]
2276#[cfg_attr(feature = "openapi", derive(ToSchema))]
2277pub struct BudgetEventData {
2278 pub budget_id: String,
2280 pub balance: f64,
2282 pub limit: f64,
2284 pub currency: String,
2286 #[serde(skip_serializing_if = "Option::is_none")]
2288 pub message: Option<String>,
2289 #[serde(skip_serializing_if = "Option::is_none")]
2291 pub soft_limit: Option<f64>,
2292}
2293
2294#[derive(Debug, Clone, Serialize, Deserialize)]
2300#[cfg_attr(feature = "openapi", derive(ToSchema))]
2301pub struct VoiceSessionStartedData {
2302 #[cfg_attr(
2304 feature = "openapi",
2305 schema(example = "voice_01933b5a00007000800000000000001")
2306 )]
2307 pub voice_connection_id: String,
2308 #[cfg_attr(feature = "openapi", schema(example = "gpt-realtime"))]
2310 pub model: String,
2311 #[cfg_attr(feature = "openapi", schema(example = "alloy"))]
2313 pub voice: String,
2314 #[cfg_attr(feature = "openapi", schema(example = "medium"))]
2316 pub reasoning_effort: String,
2317 #[cfg_attr(feature = "openapi", schema(example = "webrtc"))]
2319 pub transport: String,
2320}
2321
2322#[derive(Debug, Clone, Serialize, Deserialize)]
2324#[cfg_attr(feature = "openapi", derive(ToSchema))]
2325pub struct VoiceTranscriptData {
2326 pub voice_connection_id: String,
2328 #[serde(default, skip_serializing_if = "Option::is_none")]
2330 pub item_id: Option<String>,
2331 #[serde(default, skip_serializing_if = "Option::is_none")]
2333 pub response_id: Option<String>,
2334 #[serde(default, skip_serializing_if = "Option::is_none")]
2336 pub phase: Option<String>,
2337 #[serde(default, skip_serializing_if = "String::is_empty")]
2339 pub delta: String,
2340 pub accumulated: String,
2342}
2343
2344#[derive(Debug, Clone, Serialize, Deserialize)]
2346#[cfg_attr(feature = "openapi", derive(ToSchema))]
2347pub struct VoiceSessionEndedData {
2348 #[cfg_attr(
2350 feature = "openapi",
2351 schema(example = "voice_01933b5a00007000800000000000001")
2352 )]
2353 pub voice_connection_id: String,
2354 #[serde(default, skip_serializing_if = "Option::is_none")]
2356 #[cfg_attr(
2357 feature = "openapi",
2358 schema(example = "User hung up after refund confirmed.")
2359 )]
2360 pub reason: Option<String>,
2361 #[serde(default, skip_serializing_if = "Option::is_none")]
2364 #[cfg_attr(feature = "openapi", schema(example = 184_500_u64))]
2365 pub duration_ms: Option<u64>,
2366}
2367
2368#[derive(Debug, Clone, Serialize, Deserialize)]
2370#[cfg_attr(feature = "openapi", derive(ToSchema))]
2371pub struct VoiceSessionFailedData {
2372 #[cfg_attr(
2374 feature = "openapi",
2375 schema(example = "voice_01933b5a00007000800000000000001")
2376 )]
2377 pub voice_connection_id: String,
2378 #[cfg_attr(
2380 feature = "openapi",
2381 schema(example = "realtime provider closed stream: 1011 internal_error")
2382 )]
2383 pub error: String,
2384}
2385
2386#[derive(Debug, Clone, Serialize)]
2433#[serde(untagged)]
2434#[cfg_attr(feature = "openapi", derive(ToSchema))]
2435#[cfg_attr(feature = "openapi", schema(
2436 title = "EventData",
2437 description = "Event-specific payload. The schema depends on the event type field.",
2438 example = json!({"message": {"id": "...", "role": "user", "content": []}})
2439))]
2440pub enum EventData {
2441 InputMessage(InputMessageData),
2443
2444 OutputMessageDelta(OutputMessageDeltaData),
2446 OutputMessageStarted(OutputMessageStartedData),
2447 OutputMessageReplaced(OutputMessageReplacedData),
2448 OutputMessageCompleted(OutputMessageCompletedData),
2449
2450 TurnStarted(TurnStartedData),
2452 TurnCompleted(TurnCompletedData),
2453 TurnFailed(TurnFailedData),
2454
2455 ReasonStarted(ReasonStartedData),
2457 ReasonCompleted(ReasonCompletedData),
2458 ReasonRecovered(ReasonRecoveredData),
2459 CapabilityUsage(CapabilityUsageData),
2460 ActStarted(ActStartedData),
2461 ActCompleted(ActCompletedData),
2462 ToolStarted(ToolStartedData),
2463 ToolCompleted(ToolCompletedData),
2464 ToolProgress(ToolProgressData),
2465 ToolOutputDelta(ToolOutputDeltaData),
2466 ToolCallRequested(ToolCallRequestedData),
2467
2468 TranscriptRepaired(TranscriptRepairedData),
2470 ToolCallRepaired(ToolCallRepairedData),
2471
2472 LlmGeneration(LlmGenerationData),
2474
2475 ReasonThinkingDelta(ReasonThinkingDeltaData),
2477 ReasonItem(ReasonItemData),
2478 ReasonThinkingStarted(ReasonThinkingStartedData),
2479 ReasonThinkingCompleted(ReasonThinkingCompletedData),
2480
2481 TurnSealed(TurnSealedData),
2482 TurnCancelled(TurnCancelledData),
2483
2484 SessionStarted(SessionStartedData),
2486 SessionActivated(SessionActivatedData),
2487 SessionIdled(SessionIdledData),
2488
2489 TaskCreated(SessionTaskEventData),
2491 TaskUpdated(SessionTaskEventData),
2492 TaskMessageSent(TaskMessageEventData),
2493 TaskMessageReceived(TaskMessageEventData),
2494
2495 ContextCompacting(ContextCompactingData),
2497 ContextCompacted(ContextCompactedData),
2498
2499 FileWritten(FileWrittenData),
2501
2502 BudgetWarning(BudgetEventData),
2504 BudgetPaused(BudgetEventData),
2505 BudgetExhausted(BudgetEventData),
2506 BudgetResumed(BudgetEventData),
2507
2508 VoiceSessionStarted(VoiceSessionStartedData),
2510 VoiceInputTranscriptDelta(VoiceTranscriptData),
2511 VoiceInputTranscriptCompleted(VoiceTranscriptData),
2512 VoiceOutputTranscriptDelta(VoiceTranscriptData),
2513 VoiceOutputTranscriptCompleted(VoiceTranscriptData),
2514 VoiceSessionEnded(VoiceSessionEndedData),
2515 VoiceSessionFailed(VoiceSessionFailedData),
2516
2517 #[serde(skip)]
2521 Unsupported {
2522 event_type: String,
2524 data: serde_json::Value,
2526 },
2527}
2528
2529impl EventData {
2530 pub fn is_unsupported(&self) -> bool {
2533 matches!(self, EventData::Unsupported { .. })
2534 }
2535
2536 pub fn unsupported(event_type: String, data: serde_json::Value) -> Self {
2539 tracing::warn!(
2540 event_type = %event_type,
2541 "Encountered unsupported event type - will be filtered from API responses"
2542 );
2543 EventData::Unsupported { event_type, data }
2544 }
2545}
2546
2547macro_rules! event_data_kinds {
2561 ($( $variant:ident($data:ty) = $type_const:path ),+ $(,)?) => {
2562 impl EventData {
2563 pub fn event_type(&self) -> &'static str {
2566 match self {
2567 $( EventData::$variant(_) => $type_const, )+
2568 EventData::Unsupported { .. } => "unsupported",
2569 }
2570 }
2571 }
2572
2573 pub fn deserialize_event_data(event_type: &str, data: serde_json::Value) -> EventData {
2585 let result = match event_type {
2586 $(
2587 $type_const => serde_json::from_value::<$data>(data.clone())
2588 .map(EventData::$variant),
2589 )+
2590 _ => return EventData::unsupported(event_type.to_string(), data),
2591 };
2592
2593 result.unwrap_or_else(|e| {
2594 tracing::warn!(
2595 event_type = %event_type,
2596 error = %e,
2597 "Failed to deserialize known event type - treating as unsupported"
2598 );
2599 EventData::Unsupported {
2600 event_type: event_type.to_string(),
2601 data,
2602 }
2603 })
2604 }
2605 };
2606}
2607
2608event_data_kinds! {
2609 InputMessage(InputMessageData) = INPUT_MESSAGE,
2611
2612 OutputMessageStarted(OutputMessageStartedData) = OUTPUT_MESSAGE_STARTED,
2614 OutputMessageDelta(OutputMessageDeltaData) = OUTPUT_MESSAGE_DELTA,
2615 OutputMessageReplaced(OutputMessageReplacedData) = OUTPUT_MESSAGE_REPLACED,
2616 OutputMessageCompleted(OutputMessageCompletedData) = OUTPUT_MESSAGE_COMPLETED,
2617
2618 TurnStarted(TurnStartedData) = TURN_STARTED,
2620 TurnCompleted(TurnCompletedData) = TURN_COMPLETED,
2621 TurnFailed(TurnFailedData) = TURN_FAILED,
2622 TurnSealed(TurnSealedData) = TURN_SEALED,
2623 TurnCancelled(TurnCancelledData) = TURN_CANCELLED,
2624
2625 ReasonStarted(ReasonStartedData) = REASON_STARTED,
2627 ReasonCompleted(ReasonCompletedData) = REASON_COMPLETED,
2628 ReasonRecovered(ReasonRecoveredData) = REASON_RECOVERED,
2629 CapabilityUsage(CapabilityUsageData) = CAPABILITY_USAGE,
2630 ActStarted(ActStartedData) = ACT_STARTED,
2631 ActCompleted(ActCompletedData) = ACT_COMPLETED,
2632 ToolStarted(ToolStartedData) = TOOL_STARTED,
2633 ToolCompleted(ToolCompletedData) = TOOL_COMPLETED,
2634 ToolProgress(ToolProgressData) = TOOL_PROGRESS,
2635 ToolOutputDelta(ToolOutputDeltaData) = TOOL_OUTPUT_DELTA,
2636 ToolCallRequested(ToolCallRequestedData) = TOOL_CALL_REQUESTED,
2637
2638 TranscriptRepaired(TranscriptRepairedData) = TRANSCRIPT_REPAIRED,
2640 ToolCallRepaired(ToolCallRepairedData) = TOOL_CALL_REPAIRED,
2641
2642 LlmGeneration(LlmGenerationData) = LLM_GENERATION,
2644
2645 ReasonThinkingStarted(ReasonThinkingStartedData) = REASON_THINKING_STARTED,
2647 ReasonThinkingDelta(ReasonThinkingDeltaData) = REASON_THINKING_DELTA,
2648 ReasonThinkingCompleted(ReasonThinkingCompletedData) = REASON_THINKING_COMPLETED,
2649 ReasonItem(ReasonItemData) = REASON_ITEM,
2650
2651 SessionStarted(SessionStartedData) = SESSION_STARTED,
2653 SessionActivated(SessionActivatedData) = SESSION_ACTIVATED,
2654 SessionIdled(SessionIdledData) = SESSION_IDLED,
2655
2656 ContextCompacting(ContextCompactingData) = CONTEXT_COMPACTING,
2658 ContextCompacted(ContextCompactedData) = CONTEXT_COMPACTED,
2659
2660 FileWritten(FileWrittenData) = FILE_WRITTEN,
2662
2663 BudgetWarning(BudgetEventData) = BUDGET_WARNING,
2665 BudgetPaused(BudgetEventData) = BUDGET_PAUSED,
2666 BudgetExhausted(BudgetEventData) = BUDGET_EXHAUSTED,
2667 BudgetResumed(BudgetEventData) = BUDGET_RESUMED,
2668
2669 VoiceSessionStarted(VoiceSessionStartedData) = VOICE_SESSION_STARTED,
2671 VoiceInputTranscriptDelta(VoiceTranscriptData) = VOICE_INPUT_TRANSCRIPT_DELTA,
2672 VoiceInputTranscriptCompleted(VoiceTranscriptData) = VOICE_INPUT_TRANSCRIPT_COMPLETED,
2673 VoiceOutputTranscriptDelta(VoiceTranscriptData) = VOICE_OUTPUT_TRANSCRIPT_DELTA,
2674 VoiceOutputTranscriptCompleted(VoiceTranscriptData) = VOICE_OUTPUT_TRANSCRIPT_COMPLETED,
2675 VoiceSessionEnded(VoiceSessionEndedData) = VOICE_SESSION_ENDED,
2676 VoiceSessionFailed(VoiceSessionFailedData) = VOICE_SESSION_FAILED,
2677
2678 TaskCreated(SessionTaskEventData) = TASK_CREATED,
2680 TaskUpdated(SessionTaskEventData) = TASK_UPDATED,
2681 TaskMessageSent(TaskMessageEventData) = TASK_MESSAGE_SENT,
2682 TaskMessageReceived(TaskMessageEventData) = TASK_MESSAGE_RECEIVED,
2683}
2684
2685macro_rules! impl_from_event_data {
2689 ($($data_type:ty => $variant:ident),* $(,)?) => {
2690 $(
2691 impl From<$data_type> for EventData {
2692 fn from(data: $data_type) -> Self {
2693 EventData::$variant(data)
2694 }
2695 }
2696 )*
2697 };
2698}
2699
2700impl_from_event_data! {
2702 InputMessageData => InputMessage,
2703 OutputMessageStartedData => OutputMessageStarted,
2704 OutputMessageDeltaData => OutputMessageDelta,
2705 OutputMessageReplacedData => OutputMessageReplaced,
2706 OutputMessageCompletedData => OutputMessageCompleted,
2707 TurnStartedData => TurnStarted,
2708 TurnCompletedData => TurnCompleted,
2709 TurnFailedData => TurnFailed,
2710 TurnSealedData => TurnSealed,
2711 TurnCancelledData => TurnCancelled,
2712 ReasonStartedData => ReasonStarted,
2713 ReasonCompletedData => ReasonCompleted,
2714 ReasonRecoveredData => ReasonRecovered,
2715 CapabilityUsageData => CapabilityUsage,
2716 ActStartedData => ActStarted,
2717 ActCompletedData => ActCompleted,
2718 ToolStartedData => ToolStarted,
2719 ToolCompletedData => ToolCompleted,
2720 ToolProgressData => ToolProgress,
2721 ToolOutputDeltaData => ToolOutputDelta,
2722 ToolCallRequestedData => ToolCallRequested,
2723 TranscriptRepairedData => TranscriptRepaired,
2724 ToolCallRepairedData => ToolCallRepaired,
2725 LlmGenerationData => LlmGeneration,
2726 ReasonThinkingStartedData => ReasonThinkingStarted,
2727 ReasonThinkingDeltaData => ReasonThinkingDelta,
2728 ReasonThinkingCompletedData => ReasonThinkingCompleted,
2729 ReasonItemData => ReasonItem,
2730 SessionStartedData => SessionStarted,
2731 SessionActivatedData => SessionActivated,
2732 SessionIdledData => SessionIdled,
2733 ContextCompactingData => ContextCompacting,
2734 ContextCompactedData => ContextCompacted,
2735 FileWrittenData => FileWritten,
2736 VoiceSessionStartedData => VoiceSessionStarted,
2737 VoiceSessionEndedData => VoiceSessionEnded,
2738 VoiceSessionFailedData => VoiceSessionFailed,
2739}
2740
2741impl EventData {
2742 pub fn voice_transcript_event(data: VoiceTranscriptData, event_type: &str) -> Self {
2743 match event_type {
2744 VOICE_INPUT_TRANSCRIPT_DELTA => EventData::VoiceInputTranscriptDelta(data),
2745 VOICE_INPUT_TRANSCRIPT_COMPLETED => EventData::VoiceInputTranscriptCompleted(data),
2746 VOICE_OUTPUT_TRANSCRIPT_DELTA => EventData::VoiceOutputTranscriptDelta(data),
2747 VOICE_OUTPUT_TRANSCRIPT_COMPLETED => EventData::VoiceOutputTranscriptCompleted(data),
2748 _ => EventData::unsupported(
2749 event_type.to_string(),
2750 serde_json::to_value(&data).unwrap_or(serde_json::Value::Null),
2751 ),
2752 }
2753 }
2754}
2755
2756impl EventData {
2759 pub fn budget_event(data: BudgetEventData, event_type: &str) -> Self {
2760 match event_type {
2761 BUDGET_WARNING => EventData::BudgetWarning(data),
2762 BUDGET_PAUSED => EventData::BudgetPaused(data),
2763 BUDGET_EXHAUSTED => EventData::BudgetExhausted(data),
2764 BUDGET_RESUMED => EventData::BudgetResumed(data),
2765 _ => EventData::unsupported(
2766 event_type.to_string(),
2767 serde_json::to_value(&data).unwrap_or(serde_json::Value::Null),
2768 ),
2769 }
2770 }
2771}
2772
2773#[derive(Debug, Clone, Serialize)]
2783#[cfg_attr(feature = "openapi", derive(ToSchema))]
2784pub struct EventRequest {
2785 #[serde(rename = "type")]
2787 pub event_type: String,
2788
2789 pub ts: DateTime<Utc>,
2791
2792 pub session_id: SessionId,
2794
2795 pub context: EventContext,
2797
2798 pub data: EventData,
2800
2801 #[serde(skip_serializing_if = "Option::is_none")]
2803 pub metadata: Option<serde_json::Value>,
2804
2805 #[serde(skip_serializing_if = "Option::is_none")]
2807 pub tags: Option<Vec<String>>,
2808}
2809
2810#[derive(Debug, Deserialize)]
2811struct RawEventRequest {
2812 #[serde(rename = "type")]
2813 event_type: String,
2814 ts: DateTime<Utc>,
2815 session_id: SessionId,
2816 context: EventContext,
2817 data: serde_json::Value,
2818 metadata: Option<serde_json::Value>,
2819 tags: Option<Vec<String>>,
2820}
2821
2822impl<'de> Deserialize<'de> for EventRequest {
2823 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2824 where
2825 D: Deserializer<'de>,
2826 {
2827 let raw = RawEventRequest::deserialize(deserializer)?;
2828 let data = deserialize_event_data(&raw.event_type, raw.data);
2829 Ok(Self {
2830 event_type: raw.event_type,
2831 ts: raw.ts,
2832 session_id: raw.session_id,
2833 context: raw.context,
2834 data,
2835 metadata: raw.metadata,
2836 tags: raw.tags,
2837 })
2838 }
2839}
2840
2841impl EventRequest {
2842 pub fn new(session_id: SessionId, context: EventContext, data: impl Into<EventData>) -> Self {
2846 let data = data.into();
2847 let event_type = data.event_type().to_string();
2848 Self {
2849 event_type,
2850 ts: Utc::now(),
2851 session_id,
2852 context,
2853 data,
2854 metadata: None,
2855 tags: None,
2856 }
2857 }
2858
2859 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
2861 self.metadata = Some(metadata);
2862 self
2863 }
2864
2865 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
2867 self.tags = Some(tags);
2868 self
2869 }
2870
2871 pub fn is_ephemeral(&self) -> bool {
2879 is_ephemeral_event_type(&self.event_type)
2880 }
2881
2882 pub fn into_event(self, id: EventId, sequence: i32) -> Event {
2884 Event {
2885 id,
2886 event_type: self.event_type,
2887 ts: self.ts,
2888 session_id: self.session_id,
2889 context: self.context,
2890 data: self.data,
2891 metadata: self.metadata,
2892 tags: self.tags,
2893 sequence: Some(sequence),
2894 }
2895 }
2896}
2897
2898pub struct EventBuilder {
2904 session_id: SessionId,
2905 context: EventContext,
2906}
2907
2908impl EventBuilder {
2909 pub fn new(session_id: SessionId) -> Self {
2910 Self {
2911 session_id,
2912 context: EventContext::empty(),
2913 }
2914 }
2915
2916 pub fn with_turn(mut self, turn_id: TurnId, input_message_id: MessageId) -> Self {
2917 self.context.turn_id = Some(turn_id);
2918 self.context.input_message_id = Some(input_message_id);
2919 self
2920 }
2921
2922 pub fn with_exec(mut self, exec_id: ExecId) -> Self {
2923 self.context.exec_id = Some(exec_id);
2924 self
2925 }
2926
2927 pub fn build(self, data: impl Into<EventData>) -> Event {
2928 Event::new(self.session_id, self.context, data)
2929 }
2930}
2931
2932#[cfg(test)]
2937mod tests {
2938 use super::*;
2939 use crate::driver_registry::PromptCacheStrategy;
2940 use serde_json::json;
2941 use std::collections::HashMap;
2942
2943 #[test]
2944 fn test_event_creation() {
2945 let session_id = SessionId::new();
2946 let context = EventContext::empty();
2947 let data = InputMessageData::new(Message::user("test"));
2948
2949 let event = Event::new(session_id, context, data);
2950
2951 assert_eq!(event.event_type, "input.message");
2952 assert_eq!(event.session_uuid(), session_id.uuid());
2953 assert!(event.is_input_event());
2954 assert!(event.is_message_event());
2955 }
2956
2957 #[test]
2958 fn test_event_context_from_atom_context() {
2959 let session_id = SessionId::new();
2960 let turn_id = TurnId::new();
2961 let input_message_id = MessageId::new();
2962
2963 let atom_ctx = AtomContext::new(session_id, turn_id, input_message_id);
2964 let context = EventContext::from_atom_context(&atom_ctx);
2965
2966 assert_eq!(context.turn_id, Some(turn_id));
2967 assert_eq!(context.input_message_id, Some(input_message_id));
2968 assert_eq!(context.exec_id, Some(atom_ctx.exec_id));
2969 }
2970
2971 #[test]
2972 fn test_event_serialization() {
2973 let session_id = SessionId::new();
2974 let context = EventContext::empty();
2975 let event = Event::new(
2976 session_id,
2977 context,
2978 InputMessageData::new(Message::user("test")),
2979 );
2980
2981 let json = serde_json::to_string(&event).unwrap();
2982
2983 assert!(json.contains("\"type\":\"input.message\""));
2984 assert!(json.contains("\"session_id\""));
2985 assert!(json.contains("\"context\""));
2986 assert!(json.contains("\"data\""));
2987 }
2988
2989 #[test]
2990 fn transcript_repaired_is_valid_filter_event_type() {
2991 assert!(VALID_EVENT_TYPES.contains(&TRANSCRIPT_REPAIRED));
2992 }
2993
2994 #[test]
2998 fn capability_usage_is_valid_filter_event_type() {
2999 assert!(VALID_EVENT_TYPES.contains(&CAPABILITY_USAGE));
3000 }
3001
3002 #[test]
3003 fn test_event_builder() {
3004 let session_id = SessionId::new();
3005 let turn_id = TurnId::new();
3006 let input_message_id = MessageId::new();
3007 let exec_id = ExecId::new();
3008
3009 let event = EventBuilder::new(session_id)
3010 .with_turn(turn_id, input_message_id)
3011 .with_exec(exec_id)
3012 .build(ReasonStartedData {
3013 harness_id: HarnessId::from_seed(1),
3014 agent_id: Some(AgentId::new()),
3015 metadata: Some(ModelMetadata {
3016 model: "gpt-4o".to_string(),
3017 model_id: None,
3018 provider_id: None,
3019 }),
3020 });
3021
3022 assert_eq!(event.event_type, "reason.started");
3023 assert_eq!(event.session_id, session_id);
3024 assert_eq!(event.context.turn_id, Some(turn_id));
3025 assert_eq!(event.context.exec_id, Some(exec_id));
3026 }
3027
3028 #[test]
3029 fn test_reason_completed_data() {
3030 let data = ReasonCompletedData::success("Hello world", true, 2, Some(1000), None);
3031 assert!(data.success);
3032 assert_eq!(data.text_preview, Some("Hello world".to_string()));
3033 assert!(data.has_tool_calls);
3034 assert_eq!(data.tool_call_count, 2);
3035 assert_eq!(data.duration_ms, Some(1000));
3036 assert!(data.usage.is_none());
3037
3038 let data = ReasonCompletedData::failure("Network error".to_string(), Some(500));
3039 assert!(!data.success);
3040 assert_eq!(data.error, Some("Network error".to_string()));
3041 assert_eq!(data.duration_ms, Some(500));
3042 }
3043
3044 #[test]
3045 fn test_input_output_event_types() {
3046 assert_eq!(INPUT_MESSAGE, "input.message");
3047 assert_eq!(OUTPUT_MESSAGE_STARTED, "output.message.started");
3048 assert_eq!(OUTPUT_MESSAGE_DELTA, "output.message.delta");
3049 assert_eq!(OUTPUT_MESSAGE_COMPLETED, "output.message.completed");
3050 }
3051
3052 #[test]
3053 fn test_turn_event_types() {
3054 assert_eq!(TURN_STARTED, "turn.started");
3055 assert_eq!(TURN_COMPLETED, "turn.completed");
3056 assert_eq!(TURN_FAILED, "turn.failed");
3057 assert_eq!(TURN_CANCELLED, "turn.cancelled");
3058 }
3059
3060 #[test]
3061 fn test_turn_cancelled_data() {
3062 let data = TurnCancelledData {
3063 turn_id: TurnId::from_uuid(Uuid::now_v7()),
3064 reason: Some("User requested cancellation".to_string()),
3065 usage: Some(TokenUsage::new(100, 50)),
3066 };
3067
3068 let event_data: EventData = data.into();
3069 assert_eq!(event_data.event_type(), TURN_CANCELLED);
3070 }
3071
3072 #[test]
3073 fn test_tool_event_types() {
3074 assert_eq!(TOOL_STARTED, "tool.started");
3075 assert_eq!(TOOL_COMPLETED, "tool.completed");
3076 }
3077
3078 #[test]
3079 fn test_llm_generation_event_type() {
3080 assert_eq!(LLM_GENERATION, "llm.generation");
3081 }
3082
3083 #[test]
3084 fn test_llm_generation_data_success() {
3085 let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")];
3086 let tools = vec![ToolDefinitionSummary {
3087 name: "get_weather".to_string(),
3088 display_name: None,
3089 category: None,
3090 capability_id: None,
3091 capability_name: None,
3092 description: "Get weather for a city".to_string(),
3093 }];
3094 let tool_calls = vec![];
3095 let data = LlmGenerationData::success(
3096 messages.clone(),
3097 tools,
3098 Some("Hi there!".to_string()),
3099 tool_calls,
3100 "gpt-4o".to_string(),
3101 Some("openai".to_string()),
3102 Some(TokenUsage {
3103 input_tokens: 10,
3104 output_tokens: 5,
3105 cache_read_tokens: None,
3106 cache_creation_tokens: None,
3107 actual_cost_usd: None,
3108 estimated_cost_usd: None,
3109 effective_cost_usd: None,
3110 }),
3111 Some(100),
3112 Some(25), );
3114
3115 assert_eq!(data.messages.len(), 2);
3116 assert_eq!(data.tools.len(), 1);
3117 assert_eq!(data.tools[0].name, "get_weather");
3118 assert_eq!(data.output.text, Some("Hi there!".to_string()));
3119 assert!(data.output.tool_calls.is_empty());
3120 assert!(data.metadata.success);
3121 assert_eq!(data.metadata.model, "gpt-4o");
3122 assert_eq!(data.metadata.provider, Some("openai".to_string()));
3123 assert!(data.metadata.error.is_none());
3124 assert_eq!(data.metadata.finish_reasons, Some(vec!["stop".to_string()]));
3126 assert!(data.metadata.response_id.is_none());
3127 }
3128
3129 #[test]
3130 fn test_llm_generation_data_with_full_metadata() {
3131 let messages = vec![Message::user("Hello")];
3132 let data = LlmGenerationData::success_with_metadata(
3133 messages,
3134 vec![],
3135 Some("Hi!".to_string()),
3136 vec![],
3137 "claude-3-opus".to_string(),
3138 Some("anthropic".to_string()),
3139 Some(TokenUsage {
3140 input_tokens: 5,
3141 output_tokens: 3,
3142 cache_read_tokens: None,
3143 cache_creation_tokens: None,
3144 actual_cost_usd: None,
3145 estimated_cost_usd: None,
3146 effective_cost_usd: None,
3147 }),
3148 Some(50),
3149 Some(25), Some(vec!["end_turn".to_string()]),
3151 Some("msg_12345".to_string()),
3152 );
3153
3154 assert!(data.metadata.success);
3155 assert_eq!(data.metadata.model, "claude-3-opus");
3156 assert_eq!(data.metadata.provider, Some("anthropic".to_string()));
3157 assert_eq!(data.metadata.time_to_first_token_ms, Some(25));
3158 assert_eq!(
3159 data.metadata.finish_reasons,
3160 Some(vec!["end_turn".to_string()])
3161 );
3162 assert_eq!(data.metadata.response_id, Some("msg_12345".to_string()));
3163 }
3164
3165 #[test]
3166 fn test_llm_generation_data_failure() {
3167 let messages = vec![Message::user("Hello")];
3168 let data = LlmGenerationData::failure(
3169 messages,
3170 vec![],
3171 "gpt-4o".to_string(),
3172 Some("openai".to_string()),
3173 "Rate limit exceeded".to_string(),
3174 Some(50),
3175 None, );
3177
3178 assert!(!data.metadata.success);
3179 assert_eq!(data.metadata.error, Some("Rate limit exceeded".to_string()));
3180 assert!(data.output.text.is_none());
3181 assert!(data.output.tool_calls.is_empty());
3182 }
3183
3184 #[test]
3185 fn test_llm_generation_event_data() {
3186 let data = LlmGenerationData::success(
3187 vec![Message::user("test")],
3188 vec![],
3189 Some("response".to_string()),
3190 vec![],
3191 "model".to_string(),
3192 None,
3193 None,
3194 None,
3195 None, );
3197
3198 let event_data: EventData = data.into();
3199 assert_eq!(event_data.event_type(), LLM_GENERATION);
3200 }
3201
3202 #[test]
3203 fn test_llm_generation_is_durable_not_ephemeral() {
3204 let session_id = SessionId::new();
3205 let data = LlmGenerationData::success(
3206 vec![Message::user("test")],
3207 vec![],
3208 Some("response".to_string()),
3209 vec![],
3210 "model".to_string(),
3211 None,
3212 None,
3213 None,
3214 None,
3215 );
3216
3217 let request = EventRequest::new(session_id, EventContext::empty(), data);
3218 assert!(!request.is_ephemeral());
3219 }
3220
3221 #[test]
3222 fn test_delta_events_are_ephemeral() {
3223 let session_id = SessionId::new();
3224 let turn_id = TurnId::new();
3225
3226 let output_delta = EventRequest::new(
3227 session_id,
3228 EventContext::empty(),
3229 OutputMessageDeltaData {
3230 turn_id,
3231 delta: "hel".to_string(),
3232 accumulated: "hel".to_string(),
3233 },
3234 );
3235 assert!(output_delta.is_ephemeral());
3236
3237 let thinking_delta = EventRequest::new(
3238 session_id,
3239 EventContext::empty(),
3240 ReasonThinkingDeltaData {
3241 turn_id,
3242 delta: "step".to_string(),
3243 accumulated: "step".to_string(),
3244 },
3245 );
3246 assert!(thinking_delta.is_ephemeral());
3247
3248 let tool_delta = EventRequest::new(
3249 session_id,
3250 EventContext::empty(),
3251 ToolOutputDeltaData {
3252 tool_call_id: "call_123".to_string(),
3253 tool_name: "bash".to_string(),
3254 delta: "line".to_string(),
3255 stream: "stdout".to_string(),
3256 },
3257 );
3258 assert!(tool_delta.is_ephemeral());
3259 }
3260
3261 #[test]
3262 fn test_llm_generation_data_with_request_options() {
3263 let mut provider_options = HashMap::new();
3264 provider_options.insert(
3265 "openai".to_string(),
3266 json!({ "previous_response_id": true }),
3267 );
3268
3269 let data = LlmGenerationData::success(
3270 vec![Message::user("Hello")],
3271 vec![],
3272 Some("Hi".to_string()),
3273 vec![],
3274 "gpt-5.4".to_string(),
3275 Some("openai".to_string()),
3276 None,
3277 Some(42),
3278 Some(12),
3279 )
3280 .with_request_options(LlmRequestOptions {
3281 prompt_cache: Some(LlmPromptCacheInfo {
3282 enabled: true,
3283 strategy: PromptCacheStrategy::Auto,
3284 provider_mode: Some("prompt_cache_key".to_string()),
3285 }),
3286 tool_search: Some(LlmToolSearchInfo {
3287 enabled: true,
3288 threshold: 8,
3289 }),
3290 provider_options,
3291 metadata: Default::default(),
3292 });
3293
3294 let json = serde_json::to_value(&data).unwrap();
3295 assert_eq!(
3296 json["metadata"]["request_options"]["prompt_cache"]["provider_mode"],
3297 "prompt_cache_key"
3298 );
3299 assert_eq!(
3300 json["metadata"]["request_options"]["tool_search"]["threshold"],
3301 8
3302 );
3303 assert_eq!(
3304 json["metadata"]["request_options"]["provider_options"]["openai"]["previous_response_id"],
3305 true
3306 );
3307 }
3308
3309 #[test]
3310 fn test_extended_thinking_event_types() {
3311 assert_eq!(REASON_THINKING_STARTED, "reason.thinking.started");
3312 assert_eq!(REASON_THINKING_DELTA, "reason.thinking.delta");
3313 assert_eq!(REASON_THINKING_COMPLETED, "reason.thinking.completed");
3314 }
3315
3316 #[test]
3317 fn test_output_message_started_data() {
3318 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3319 let data = OutputMessageStartedData {
3320 turn_id,
3321 model: Some("claude-4-opus".to_string()),
3322 iteration: None,
3323 };
3324
3325 let event_data: EventData = data.into();
3326 assert_eq!(event_data.event_type(), OUTPUT_MESSAGE_STARTED);
3327
3328 let json = serde_json::to_string(&event_data).unwrap();
3330 assert!(json.contains("turn_id"));
3331 assert!(json.contains("claude-4-opus"));
3332 }
3333
3334 #[test]
3335 fn test_output_message_started_data_without_model() {
3336 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3337 let data = OutputMessageStartedData {
3338 turn_id,
3339 model: None,
3340 iteration: None,
3341 };
3342
3343 let json = serde_json::to_string(&data).unwrap();
3345 assert!(!json.contains("model"));
3346 }
3347
3348 #[test]
3349 fn test_reason_thinking_started_data() {
3350 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3351 let data = ReasonThinkingStartedData {
3352 turn_id,
3353 model: Some("claude-4-opus".to_string()),
3354 };
3355
3356 let event_data: EventData = data.into();
3357 assert_eq!(event_data.event_type(), REASON_THINKING_STARTED);
3358
3359 let json = serde_json::to_string(&event_data).unwrap();
3361 assert!(json.contains("turn_id"));
3362 assert!(json.contains("claude-4-opus"));
3363 }
3364
3365 #[test]
3366 fn test_reason_thinking_delta_data() {
3367 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3368 let data = ReasonThinkingDeltaData {
3369 turn_id,
3370 delta: "thinking step 1".to_string(),
3371 accumulated: "thinking step 1".to_string(),
3372 };
3373
3374 let event_data: EventData = data.into();
3375 assert_eq!(event_data.event_type(), REASON_THINKING_DELTA);
3376
3377 let json = serde_json::to_string(&event_data).unwrap();
3379 assert!(json.contains("turn_id"));
3380 assert!(json.contains("delta"));
3381 assert!(json.contains("accumulated"));
3382 }
3383
3384 #[test]
3385 fn test_reason_thinking_completed_data() {
3386 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3387 let data = ReasonThinkingCompletedData {
3388 turn_id,
3389 thinking: "Full thinking content here".to_string(),
3390 };
3391
3392 let event_data: EventData = data.into();
3393 assert_eq!(event_data.event_type(), REASON_THINKING_COMPLETED);
3394
3395 let json = serde_json::to_string(&event_data).unwrap();
3397 assert!(json.contains("turn_id"));
3398 assert!(json.contains("thinking"));
3399 }
3400
3401 #[test]
3402 fn test_output_message_delta_data() {
3403 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3404 let data = OutputMessageDeltaData {
3405 turn_id,
3406 delta: "Hello".to_string(),
3407 accumulated: "Hello".to_string(),
3408 };
3409
3410 let event_data: EventData = data.into();
3411 assert_eq!(event_data.event_type(), OUTPUT_MESSAGE_DELTA);
3412
3413 let json = serde_json::to_string(&event_data).unwrap();
3415 assert!(json.contains("turn_id"));
3416 assert!(json.contains("delta"));
3417 assert!(json.contains("accumulated"));
3418 }
3419
3420 #[test]
3421 fn test_output_message_delta_deserialization_preserves_fields() {
3422 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3425 let data = OutputMessageDeltaData {
3426 turn_id,
3427 delta: "Hello world".to_string(),
3428 accumulated: "Hello world".to_string(),
3429 };
3430
3431 let json = serde_json::to_value(EventData::OutputMessageDelta(data.clone())).unwrap();
3433
3434 let deserialized = deserialize_event_data(OUTPUT_MESSAGE_DELTA, json);
3436
3437 match deserialized {
3439 EventData::OutputMessageDelta(td) => {
3440 assert_eq!(td.turn_id, turn_id);
3441 assert_eq!(td.delta, "Hello world");
3442 assert_eq!(td.accumulated, "Hello world");
3443 }
3444 _ => panic!("Expected OutputMessageDelta, got different variant"),
3445 }
3446 }
3447
3448 #[test]
3449 fn test_output_message_started_deserialization() {
3450 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3451 let data = OutputMessageStartedData {
3452 turn_id,
3453 model: Some("claude-3".to_string()),
3454 iteration: None,
3455 };
3456
3457 let json = serde_json::to_value(EventData::OutputMessageStarted(data.clone())).unwrap();
3459
3460 let deserialized = deserialize_event_data(OUTPUT_MESSAGE_STARTED, json);
3462
3463 match deserialized {
3465 EventData::OutputMessageStarted(at) => {
3466 assert_eq!(at.turn_id, turn_id);
3467 assert_eq!(at.model, Some("claude-3".to_string()));
3468 }
3469 _ => panic!("Expected OutputMessageStarted, got different variant"),
3470 }
3471 }
3472
3473 #[test]
3474 fn test_reason_thinking_started_deserialization() {
3475 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3480 let data = ReasonThinkingStartedData {
3481 turn_id,
3482 model: Some("claude-3".to_string()),
3483 };
3484
3485 let json = serde_json::to_value(&data).unwrap();
3487
3488 let deserialized = deserialize_event_data(REASON_THINKING_STARTED, json);
3490
3491 match deserialized {
3493 EventData::ReasonThinkingStarted(at) => {
3494 assert_eq!(at.turn_id, turn_id);
3495 assert_eq!(at.model, Some("claude-3".to_string()));
3496 }
3497 other => panic!("Expected ReasonThinkingStarted, got {}", other.event_type()),
3498 }
3499 }
3500
3501 #[test]
3502 fn test_llm_generation_with_ttft() {
3503 let messages = vec![Message::user("Hello")];
3504 let data = LlmGenerationData::success_with_metadata(
3505 messages,
3506 vec![],
3507 Some("Hi!".to_string()),
3508 vec![],
3509 "gpt-4o".to_string(),
3510 Some("openai".to_string()),
3511 Some(TokenUsage {
3512 input_tokens: 10,
3513 output_tokens: 5,
3514 cache_read_tokens: None,
3515 cache_creation_tokens: None,
3516 actual_cost_usd: None,
3517 estimated_cost_usd: None,
3518 effective_cost_usd: None,
3519 }),
3520 Some(500), Some(120), Some(vec!["stop".to_string()]),
3523 None,
3524 );
3525
3526 assert!(data.metadata.success);
3527 assert_eq!(data.metadata.duration_ms, Some(500));
3528 assert_eq!(data.metadata.time_to_first_token_ms, Some(120));
3529 }
3530
3531 #[test]
3532 fn test_llm_generation_ttft_serialization() {
3533 let messages = vec![Message::user("test")];
3534 let data = LlmGenerationData::success_with_metadata(
3535 messages,
3536 vec![],
3537 Some("response".to_string()),
3538 vec![],
3539 "model".to_string(),
3540 None,
3541 None,
3542 Some(1000),
3543 Some(150), None,
3545 None,
3546 );
3547
3548 let json = serde_json::to_string(&data).unwrap();
3549 assert!(json.contains("time_to_first_token_ms"));
3550 assert!(json.contains("150"));
3551 }
3552
3553 #[test]
3554 fn test_reason_item_data_event_type_and_serialization() {
3555 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3556 let data = ReasonItemData {
3557 turn_id,
3558 provider: "openai".to_string(),
3559 model: Some("gpt-5.5".to_string()),
3560 item_id: "rs_abc".to_string(),
3561 encrypted_content: Some("OPAQUE_BLOB".to_string()),
3562 summary: vec!["safe summary".to_string()],
3563 token_count: Some(123),
3564 };
3565
3566 let event_data: EventData = data.into();
3567 assert_eq!(event_data.event_type(), REASON_ITEM);
3568
3569 let json = serde_json::to_string(&event_data).unwrap();
3570 assert!(json.contains("turn_id"));
3571 assert!(json.contains("openai"));
3572 assert!(json.contains("rs_abc"));
3573 assert!(json.contains("OPAQUE_BLOB"));
3574 assert!(json.contains("safe summary"));
3575 }
3576
3577 #[test]
3578 fn test_event_deserialize_reason_item_uses_event_type_dispatch() {
3579 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3580 let payload = serde_json::json!({
3581 "id": EventId::new().to_string(),
3582 "type": REASON_ITEM,
3583 "ts": Utc::now().to_rfc3339(),
3584 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
3585 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
3586 "data": {
3587 "turn_id": turn_id.to_string(),
3588 "provider": "openai",
3589 "model": "gpt-5",
3590 "item_id": "rs_event",
3591 "encrypted_content": "ENC",
3592 "summary": ["safe"],
3593 "token_count": 9
3594 }
3595 });
3596
3597 let event: Event = serde_json::from_value(payload).expect("event deserializes");
3598 match event.data {
3599 EventData::ReasonItem(data) => {
3600 assert_eq!(data.turn_id, turn_id);
3601 assert_eq!(data.provider, "openai");
3602 assert_eq!(data.item_id, "rs_event");
3603 assert_eq!(data.token_count, Some(9));
3604 }
3605 other => panic!("expected reason.item data, got {}", other.event_type()),
3606 }
3607 }
3608
3609 #[test]
3610 fn test_event_request_deserialize_reason_item_uses_event_type_dispatch() {
3611 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3612 let payload = serde_json::json!({
3613 "type": REASON_ITEM,
3614 "ts": Utc::now().to_rfc3339(),
3615 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
3616 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
3617 "data": {
3618 "turn_id": turn_id.to_string(),
3619 "provider": "openai",
3620 "item_id": "rs_request",
3621 "encrypted_content": "ENC",
3622 "summary": ["safe"]
3623 }
3624 });
3625
3626 let req: EventRequest = serde_json::from_value(payload).expect("request deserializes");
3627 match req.data {
3628 EventData::ReasonItem(data) => {
3629 assert_eq!(data.turn_id, turn_id);
3630 assert_eq!(data.provider, "openai");
3631 assert_eq!(data.item_id, "rs_request");
3632 }
3633 other => panic!("expected reason.item data, got {}", other.event_type()),
3634 }
3635 }
3636
3637 #[test]
3638 fn test_reason_item_data_round_trip_uses_typed_dispatch() {
3639 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3643 let data = ReasonItemData {
3644 turn_id,
3645 provider: "openai".to_string(),
3646 model: Some("gpt-5".to_string()),
3647 item_id: "rs_xyz".to_string(),
3648 encrypted_content: Some("ENC".to_string()),
3649 summary: vec![],
3650 token_count: None,
3651 };
3652
3653 let json = serde_json::to_value(&data).unwrap();
3654 let deserialized = deserialize_event_data(REASON_ITEM, json);
3655
3656 match deserialized {
3657 EventData::ReasonItem(out) => {
3658 assert_eq!(out.turn_id, turn_id);
3659 assert_eq!(out.provider, "openai");
3660 assert_eq!(out.item_id, "rs_xyz");
3661 assert_eq!(out.encrypted_content.as_deref(), Some("ENC"));
3662 }
3663 other => panic!("Expected ReasonItem, got {}", other.event_type()),
3664 }
3665 }
3666
3667 #[test]
3676 fn test_reason_item_resolves_via_type_dispatch_despite_overlap() {
3677 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3682 let json = serde_json::json!({
3683 "turn_id": turn_id.to_string(),
3684 "provider": "openai",
3685 "model": "gpt-5",
3686 "item_id": "rs_keep",
3687 "encrypted_content": "ENC",
3688 "summary": ["s"],
3689 "token_count": 7,
3690 });
3691
3692 let as_thinking: ReasonThinkingStartedData =
3697 serde_json::from_value(json.clone()).expect("thinking ignores extra fields");
3698 assert_eq!(as_thinking.turn_id, turn_id);
3699 assert_eq!(as_thinking.model.as_deref(), Some("gpt-5"));
3700
3701 let as_item: ReasonItemData =
3702 serde_json::from_value(json.clone()).expect("ReasonItem accepts payload");
3703 assert_eq!(as_item.item_id, "rs_keep");
3704 assert_eq!(as_item.provider, "openai");
3705
3706 let event_data = deserialize_event_data(REASON_ITEM, json);
3709 match event_data {
3710 EventData::ReasonItem(out) => {
3711 assert_eq!(out.item_id, "rs_keep");
3712 assert_eq!(out.provider, "openai");
3713 }
3714 other => panic!(
3715 "Typed dispatcher must select ReasonItem for {REASON_ITEM}, got {}",
3716 other.event_type()
3717 ),
3718 }
3719 }
3720
3721 #[test]
3728 fn test_reason_item_data_excludes_plaintext_reasoning() {
3729 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3730 let data = ReasonItemData {
3731 turn_id,
3732 provider: "openai".to_string(),
3733 model: Some("gpt-5".to_string()),
3734 item_id: "rs_secret".to_string(),
3735 encrypted_content: Some("opaque_blob_thinking_content_reasoning_text".to_string()),
3739 summary: vec!["safe summary mentioning content and thinking".to_string()],
3740 token_count: Some(1),
3741 };
3742
3743 let value = serde_json::to_value(&data).expect("serializable");
3744 let object = value.as_object().expect("data serializes to JSON object");
3745 for forbidden in [
3746 "content",
3747 "reasoning_text",
3748 "thinking",
3749 "reasoning_content",
3750 "raw_reasoning",
3751 ] {
3752 assert!(
3753 !object.contains_key(forbidden),
3754 "ReasonItemData JSON must not expose `{forbidden}` key, got: {object:?}",
3755 );
3756 }
3757 assert!(object.contains_key("encrypted_content"));
3759 assert!(object.contains_key("summary"));
3760 }
3761
3762 #[test]
3763 fn test_llm_generation_ttft_omitted_when_none() {
3764 let messages = vec![Message::user("test")];
3765 let data = LlmGenerationData::success(
3766 messages,
3767 vec![],
3768 Some("response".to_string()),
3769 vec![],
3770 "model".to_string(),
3771 None,
3772 None,
3773 None,
3774 None, );
3776
3777 assert!(data.metadata.time_to_first_token_ms.is_none());
3779
3780 let json = serde_json::to_string(&data).unwrap();
3782 assert!(!json.contains("time_to_first_token_ms"));
3783 }
3784}
3785
3786#[cfg(test)]
3795mod contract_tests {
3796 use super::*;
3797 use insta::{assert_json_snapshot, with_settings};
3798
3799 fn test_session_id() -> SessionId {
3801 SessionId::from_uuid(uuid::Uuid::from_u128(
3802 0x0000_0000_0000_0000_0000_0000_0000_0001,
3803 ))
3804 }
3805
3806 fn test_turn_id() -> TurnId {
3807 TurnId::from_uuid(uuid::Uuid::from_u128(
3808 0x0000_0000_0000_0000_0000_0000_0000_0002,
3809 ))
3810 }
3811
3812 fn test_message_id() -> MessageId {
3813 MessageId::from_uuid(uuid::Uuid::from_u128(
3814 0x0000_0000_0000_0000_0000_0000_0000_0003,
3815 ))
3816 }
3817
3818 fn test_agent_id() -> AgentId {
3819 AgentId::from_uuid(uuid::Uuid::from_u128(
3820 0x0000_0000_0000_0000_0000_0000_0000_0004,
3821 ))
3822 }
3823
3824 fn test_harness_id() -> HarnessId {
3825 HarnessId::from_uuid(uuid::Uuid::from_u128(
3826 0x0000_0000_0000_0000_0000_0000_0000_0005,
3827 ))
3828 }
3829
3830 #[test]
3837 fn snapshot_input_message() {
3838 let data = InputMessageData::new(Message::user("Hello, world!"));
3839 with_settings!({
3840 sort_maps => true,
3841 }, {
3842 assert_json_snapshot!("event_data_input_message", data, {
3844 ".message.id" => "[MESSAGE_ID]",
3845 ".message.created_at" => "[TIMESTAMP]"
3846 });
3847 });
3848 }
3849
3850 #[test]
3851 fn snapshot_output_message_started() {
3852 let data = OutputMessageStartedData {
3853 turn_id: test_turn_id(),
3854 model: Some("gpt-4o".to_string()),
3855 iteration: None,
3856 };
3857 with_settings!({
3858 sort_maps => true,
3859 }, {
3860 assert_json_snapshot!("event_data_output_message_started", data);
3861 });
3862 }
3863
3864 #[test]
3865 fn snapshot_output_message_delta() {
3866 let data = OutputMessageDeltaData {
3867 turn_id: test_turn_id(),
3868 delta: "Hello".to_string(),
3869 accumulated: "Hello".to_string(),
3870 };
3871 with_settings!({
3872 sort_maps => true,
3873 }, {
3874 assert_json_snapshot!("event_data_output_message_delta", data);
3875 });
3876 }
3877
3878 #[test]
3879 fn snapshot_output_message_completed() {
3880 let data = OutputMessageCompletedData::new(Message::assistant("Hello!"));
3881 with_settings!({
3882 sort_maps => true,
3883 }, {
3884 assert_json_snapshot!("event_data_output_message_completed", data, {
3886 ".message.id" => "[MESSAGE_ID]",
3887 ".message.created_at" => "[TIMESTAMP]"
3888 });
3889 });
3890 }
3891
3892 #[test]
3893 fn snapshot_turn_started() {
3894 let data = TurnStartedData {
3895 turn_id: test_turn_id(),
3896 input_message_id: test_message_id(),
3897 input_content: Some("Hello".to_string()),
3898 };
3899 with_settings!({
3900 sort_maps => true,
3901 }, {
3902 assert_json_snapshot!("event_data_turn_started", data);
3903 });
3904 }
3905
3906 #[test]
3907 fn snapshot_turn_completed() {
3908 let data = TurnCompletedData {
3909 turn_id: test_turn_id(),
3910 iterations: 3,
3911 duration_ms: Some(1500),
3912 usage: Some(TokenUsage::new(100, 50)),
3913 input_content: None,
3914 final_message_id: Some(test_message_id()),
3915 final_answer_preview: Some("Done.".to_string()),
3916 time_to_first_token_ms: Some(120),
3917 tool_call_count: Some(2),
3918 llm_call_count: Some(3),
3919 status: Some("completed".to_string()),
3920 };
3921 with_settings!({
3922 sort_maps => true,
3923 }, {
3924 assert_json_snapshot!("event_data_turn_completed", data);
3925 });
3926 }
3927
3928 #[test]
3929 fn snapshot_turn_failed() {
3930 let data = TurnFailedData {
3931 turn_id: test_turn_id(),
3932 error: "Rate limit exceeded".to_string(),
3933 error_code: Some("RATE_LIMIT".to_string()),
3934 error_fields: None,
3935 error_disclosure: None,
3936 };
3937 with_settings!({
3938 sort_maps => true,
3939 }, {
3940 assert_json_snapshot!("event_data_turn_failed", data);
3941 });
3942 }
3943
3944 #[test]
3945 fn snapshot_turn_cancelled() {
3946 let data = TurnCancelledData {
3947 turn_id: test_turn_id(),
3948 reason: Some("User requested".to_string()),
3949 usage: Some(TokenUsage::new(50, 25)),
3950 };
3951 with_settings!({
3952 sort_maps => true,
3953 }, {
3954 assert_json_snapshot!("event_data_turn_cancelled", data);
3955 });
3956 }
3957
3958 #[test]
3959 fn snapshot_reason_started() {
3960 let data = ReasonStartedData {
3961 harness_id: test_harness_id(),
3962 agent_id: Some(test_agent_id()),
3963 metadata: Some(ModelMetadata {
3964 model: "gpt-4o".to_string(),
3965 model_id: None,
3966 provider_id: None,
3967 }),
3968 };
3969 with_settings!({
3970 sort_maps => true,
3971 }, {
3972 assert_json_snapshot!("event_data_reason_started", data);
3973 });
3974 }
3975
3976 #[test]
3977 fn snapshot_reason_completed() {
3978 let data = ReasonCompletedData::success(
3979 "Hello world",
3980 true,
3981 2,
3982 Some(1000),
3983 Some(TokenUsage::new(100, 50)),
3984 );
3985 with_settings!({
3986 sort_maps => true,
3987 }, {
3988 assert_json_snapshot!("event_data_reason_completed", data);
3989 });
3990 }
3991
3992 #[test]
3993 fn snapshot_act_started() {
3994 let data = ActStartedData {
3995 tool_calls: vec![ToolCallSummary {
3996 id: "tc_1".to_string(),
3997 name: "get_weather".to_string(),
3998 display_name: None,
3999 narration: None,
4000 }],
4001 headline: None,
4002 };
4003 with_settings!({
4004 sort_maps => true,
4005 }, {
4006 assert_json_snapshot!("event_data_act_started", data);
4007 });
4008 }
4009
4010 #[test]
4011 fn snapshot_act_completed() {
4012 let data = ActCompletedData {
4013 completed: true,
4014 success_count: 2,
4015 error_count: 0,
4016 duration_ms: Some(500),
4017 headline: None,
4018 };
4019 with_settings!({
4020 sort_maps => true,
4021 }, {
4022 assert_json_snapshot!("event_data_act_completed", data);
4023 });
4024 }
4025
4026 #[test]
4027 fn snapshot_tool_started() {
4028 let data = ToolStartedData {
4029 tool_call: ToolCall {
4030 id: "tc_1".to_string(),
4031 name: "get_weather".to_string(),
4032 arguments: serde_json::json!({"city": "London"}),
4033 },
4034 tool_call_fingerprint: None,
4035 display_name: None,
4036 narration: None,
4037 };
4038 with_settings!({
4039 sort_maps => true,
4040 }, {
4041 assert_json_snapshot!("event_data_tool_started", data);
4042 });
4043 }
4044
4045 #[test]
4046 fn snapshot_tool_completed() {
4047 let data = ToolCompletedData::success(
4048 "tc_1".to_string(),
4049 "get_weather".to_string(),
4050 vec![crate::message::ContentPart::text("Sunny, 22°C")],
4051 Some(250),
4052 );
4053 with_settings!({
4054 sort_maps => true,
4055 }, {
4056 assert_json_snapshot!("event_data_tool_completed", data);
4057 });
4058 }
4059
4060 #[test]
4061 fn snapshot_llm_generation() {
4062 let data = LlmGenerationData::success(
4063 vec![Message::user("Hello")],
4064 vec![ToolDefinitionSummary {
4065 name: "tool1".to_string(),
4066 display_name: None,
4067 category: None,
4068 capability_id: None,
4069 capability_name: None,
4070 description: "A tool".to_string(),
4071 }],
4072 Some("Hi there!".to_string()),
4073 vec![],
4074 "gpt-4o".to_string(),
4075 Some("openai".to_string()),
4076 Some(TokenUsage::new(10, 5)),
4077 Some(100),
4078 Some(25),
4079 );
4080 with_settings!({
4081 sort_maps => true,
4082 }, {
4083 assert_json_snapshot!("event_data_llm_generation", data, {
4085 ".messages[].id" => "[MESSAGE_ID]",
4086 ".messages[].created_at" => "[TIMESTAMP]"
4087 });
4088 });
4089 }
4090
4091 #[test]
4092 fn snapshot_reason_thinking_started() {
4093 let data = ReasonThinkingStartedData {
4094 turn_id: test_turn_id(),
4095 model: Some("claude-4-opus".to_string()),
4096 };
4097 with_settings!({
4098 sort_maps => true,
4099 }, {
4100 assert_json_snapshot!("event_data_reason_thinking_started", data);
4101 });
4102 }
4103
4104 #[test]
4105 fn snapshot_reason_thinking_delta() {
4106 let data = ReasonThinkingDeltaData {
4107 turn_id: test_turn_id(),
4108 delta: "Let me think...".to_string(),
4109 accumulated: "Let me think...".to_string(),
4110 };
4111 with_settings!({
4112 sort_maps => true,
4113 }, {
4114 assert_json_snapshot!("event_data_reason_thinking_delta", data);
4115 });
4116 }
4117
4118 #[test]
4119 fn snapshot_reason_thinking_completed() {
4120 let data = ReasonThinkingCompletedData {
4121 turn_id: test_turn_id(),
4122 thinking: "I need to consider...".to_string(),
4123 };
4124 with_settings!({
4125 sort_maps => true,
4126 }, {
4127 assert_json_snapshot!("event_data_reason_thinking_completed", data);
4128 });
4129 }
4130
4131 #[test]
4132 fn snapshot_reason_item() {
4133 let data = ReasonItemData {
4134 turn_id: test_turn_id(),
4135 provider: "openai".to_string(),
4136 model: Some("gpt-5.5".to_string()),
4137 item_id: "rs_test".to_string(),
4138 encrypted_content: Some("OPAQUE".to_string()),
4139 summary: vec!["safe summary".to_string()],
4140 token_count: Some(42),
4141 };
4142 with_settings!({
4143 sort_maps => true,
4144 }, {
4145 assert_json_snapshot!("event_data_reason_item", data);
4146 });
4147 }
4148
4149 #[test]
4150 fn snapshot_session_started() {
4151 let data = SessionStartedData {
4152 harness_id: test_harness_id(),
4153 agent_id: Some(test_agent_id()),
4154 model_id: None,
4155 };
4156 with_settings!({
4157 sort_maps => true,
4158 }, {
4159 assert_json_snapshot!("event_data_session_started", data);
4160 });
4161 }
4162
4163 #[test]
4164 fn snapshot_session_activated() {
4165 let data = SessionActivatedData {
4166 turn_id: test_turn_id(),
4167 input_message_id: test_message_id(),
4168 };
4169 with_settings!({
4170 sort_maps => true,
4171 }, {
4172 assert_json_snapshot!("event_data_session_activated", data);
4173 });
4174 }
4175
4176 #[test]
4177 fn snapshot_session_idled() {
4178 let data = SessionIdledData {
4179 turn_id: test_turn_id(),
4180 iterations: Some(3),
4181 usage: Some(TokenUsage::new(500, 200)),
4182 };
4183 with_settings!({
4184 sort_maps => true,
4185 }, {
4186 assert_json_snapshot!("event_data_session_idled", data);
4187 });
4188 }
4189
4190 #[test]
4196 fn tool_call_summary_with_display_name() {
4197 let summary = ToolCallSummary {
4198 id: "tc_1".to_string(),
4199 name: "get_weather".to_string(),
4200 display_name: Some("Get Weather".to_string()),
4201 narration: None,
4202 };
4203 let json = serde_json::to_value(&summary).unwrap();
4204 assert_eq!(json["display_name"], "Get Weather");
4205
4206 let deserialized: ToolCallSummary = serde_json::from_value(json).unwrap();
4208 assert_eq!(deserialized.display_name.as_deref(), Some("Get Weather"));
4209 }
4210
4211 #[test]
4212 fn tool_call_summary_without_display_name_omits_field() {
4213 let summary = ToolCallSummary {
4214 id: "tc_1".to_string(),
4215 name: "get_weather".to_string(),
4216 display_name: None,
4217 narration: None,
4218 };
4219 let json = serde_json::to_string(&summary).unwrap();
4220 assert!(!json.contains("display_name"));
4221
4222 let json_without = r#"{"id":"tc_1","name":"get_weather"}"#;
4224 let deserialized: ToolCallSummary = serde_json::from_str(json_without).unwrap();
4225 assert_eq!(deserialized.display_name, None);
4226 }
4227
4228 #[test]
4229 fn act_started_with_definitions_populates_display_names() {
4230 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4231
4232 let tool_calls = vec![
4233 ToolCall {
4234 id: "tc_1".to_string(),
4235 name: "get_weather".to_string(),
4236 arguments: serde_json::json!({}),
4237 },
4238 ToolCall {
4239 id: "tc_2".to_string(),
4240 name: "unknown_tool".to_string(),
4241 arguments: serde_json::json!({}),
4242 },
4243 ];
4244 let tool_defs = vec![crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4245 name: "get_weather".to_string(),
4246 display_name: Some("Get Weather".to_string()),
4247 description: "Gets weather".to_string(),
4248 parameters: serde_json::json!({}),
4249 policy: ToolPolicy::Auto,
4250 category: None,
4251 deferrable: DeferrablePolicy::default(),
4252 hints: crate::tool_types::ToolHints::default(),
4253 full_parameters: None,
4254 })];
4255
4256 let data = ActStartedData::with_definitions(&tool_calls, &tool_defs);
4257 assert_eq!(data.tool_calls.len(), 2);
4258 assert_eq!(
4259 data.tool_calls[0].display_name.as_deref(),
4260 Some("Get Weather")
4261 );
4262 assert_eq!(data.tool_calls[1].display_name, None);
4263 }
4264
4265 #[test]
4266 fn tool_completed_with_display_name_roundtrip() {
4267 let data = ToolCompletedData::success(
4268 "tc_1".to_string(),
4269 "get_weather".to_string(),
4270 vec![crate::message::ContentPart::text("Sunny")],
4271 Some(100),
4272 )
4273 .with_display_name(Some("Get Weather".to_string()));
4274
4275 assert_eq!(data.display_name.as_deref(), Some("Get Weather"));
4276
4277 let json = serde_json::to_value(&data).unwrap();
4278 assert_eq!(json["display_name"], "Get Weather");
4279
4280 let deserialized: ToolCompletedData = serde_json::from_value(json).unwrap();
4281 assert_eq!(deserialized.display_name.as_deref(), Some("Get Weather"));
4282 }
4283
4284 #[test]
4285 fn tool_started_display_name_serialization() {
4286 let data = ToolStartedData {
4287 tool_call: ToolCall {
4288 id: "tc_1".to_string(),
4289 name: "bash".to_string(),
4290 arguments: serde_json::json!({"command": "ls"}),
4291 },
4292 tool_call_fingerprint: None,
4293 display_name: Some("Bash".to_string()),
4294 narration: None,
4295 };
4296
4297 let json = serde_json::to_value(&data).unwrap();
4298 assert_eq!(json["display_name"], "Bash");
4299 }
4300
4301 #[test]
4302 fn tool_definition_summary_display_name() {
4303 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4304
4305 let def = crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4306 name: "read_file".to_string(),
4307 display_name: Some("Read File".to_string()),
4308 description: "Reads a file".to_string(),
4309 parameters: serde_json::json!({}),
4310 policy: ToolPolicy::Auto,
4311 category: None,
4312 deferrable: DeferrablePolicy::default(),
4313 hints: crate::tool_types::ToolHints::default(),
4314 full_parameters: None,
4315 });
4316
4317 let summary = ToolDefinitionSummary::from(&def);
4318 assert_eq!(summary.display_name.as_deref(), Some("Read File"));
4319
4320 let json = serde_json::to_value(&summary).unwrap();
4321 assert_eq!(json["display_name"], "Read File");
4322 }
4323
4324 #[test]
4331 fn forward_compat_unknown_fields_ignored() {
4332 let json = r#"{
4334 "turn_id": "turn_00000000000000000000000000000002",
4335 "iterations": 3,
4336 "duration_ms": 1500,
4337 "usage": {"input_tokens": 100, "output_tokens": 50},
4338 "future_field": "should be ignored",
4339 "another_new_field": 42
4340 }"#;
4341
4342 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4343 assert_eq!(data.iterations, 3);
4344 assert_eq!(data.duration_ms, Some(1500));
4345 }
4346
4347 #[test]
4348 fn forward_compat_unknown_event_type_becomes_unsupported() {
4349 let json = serde_json::json!({"some_field": "value"});
4351 let data = deserialize_event_data("future.event.type", json);
4352
4353 assert!(data.is_unsupported());
4354 assert_eq!(data.event_type(), "unsupported");
4355 }
4356
4357 #[test]
4358 fn forward_compat_unsupported_preserves_data() {
4359 let original = serde_json::json!({"key": "value", "nested": {"a": 1}});
4361 let data = deserialize_event_data("unknown.event", original.clone());
4362
4363 match data {
4364 EventData::Unsupported { event_type, data } => {
4365 assert_eq!(event_type, "unknown.event");
4366 assert_eq!(data, original);
4367 }
4368 _ => panic!("Expected Unsupported variant"),
4369 }
4370 }
4371
4372 #[test]
4373 fn forward_compat_optional_fields_absent() {
4374 let json = r#"{
4376 "turn_id": "turn_00000000000000000000000000000002",
4377 "iterations": 3
4378 }"#;
4379
4380 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4381 assert_eq!(data.iterations, 3);
4382 assert!(data.duration_ms.is_none());
4383 assert!(data.usage.is_none());
4384 assert!(data.input_content.is_none());
4385 assert!(data.final_message_id.is_none());
4386 assert!(data.final_answer_preview.is_none());
4387 assert!(data.time_to_first_token_ms.is_none());
4388 assert!(data.tool_call_count.is_none());
4389 assert!(data.llm_call_count.is_none());
4390 assert!(data.status.is_none());
4391 }
4392
4393 #[test]
4399 fn round_trip_all_event_data_types() {
4400 let test_cases: Vec<(&str, EventData)> = vec![
4402 (
4403 INPUT_MESSAGE,
4404 InputMessageData::new(Message::user("test")).into(),
4405 ),
4406 (
4407 OUTPUT_MESSAGE_STARTED,
4408 OutputMessageStartedData {
4409 turn_id: test_turn_id(),
4410 model: None,
4411 iteration: None,
4412 }
4413 .into(),
4414 ),
4415 (
4416 OUTPUT_MESSAGE_DELTA,
4417 OutputMessageDeltaData {
4418 turn_id: test_turn_id(),
4419 delta: "x".to_string(),
4420 accumulated: "x".to_string(),
4421 }
4422 .into(),
4423 ),
4424 (
4425 OUTPUT_MESSAGE_COMPLETED,
4426 OutputMessageCompletedData::new(Message::assistant("hi")).into(),
4427 ),
4428 (
4429 TURN_STARTED,
4430 TurnStartedData {
4431 turn_id: test_turn_id(),
4432 input_message_id: test_message_id(),
4433 input_content: None,
4434 }
4435 .into(),
4436 ),
4437 (
4438 TURN_COMPLETED,
4439 TurnCompletedData {
4440 turn_id: test_turn_id(),
4441 iterations: 1,
4442 duration_ms: None,
4443 usage: None,
4444 input_content: None,
4445 final_message_id: None,
4446 final_answer_preview: None,
4447 time_to_first_token_ms: None,
4448 tool_call_count: None,
4449 llm_call_count: None,
4450 status: None,
4451 }
4452 .into(),
4453 ),
4454 (
4455 TURN_FAILED,
4456 TurnFailedData {
4457 turn_id: test_turn_id(),
4458 error: "err".to_string(),
4459 error_code: None,
4460 error_fields: None,
4461 error_disclosure: None,
4462 }
4463 .into(),
4464 ),
4465 (
4466 TURN_CANCELLED,
4467 TurnCancelledData {
4468 turn_id: test_turn_id(),
4469 reason: None,
4470 usage: None,
4471 }
4472 .into(),
4473 ),
4474 (
4475 TURN_SEALED,
4476 TurnSealedData {
4477 turn_id: test_turn_id(),
4478 reason: "no_progress".to_string(),
4479 detail: Some("sealed".to_string()),
4480 iterations: Some(3),
4481 usage: None,
4482 }
4483 .into(),
4484 ),
4485 (
4486 REASON_STARTED,
4487 ReasonStartedData {
4488 harness_id: test_harness_id(),
4489 agent_id: Some(test_agent_id()),
4490 metadata: None,
4491 }
4492 .into(),
4493 ),
4494 (
4495 REASON_COMPLETED,
4496 ReasonCompletedData::success("", false, 0, None, None).into(),
4497 ),
4498 (
4499 ACT_STARTED,
4500 ActStartedData {
4501 tool_calls: vec![],
4502 headline: None,
4503 }
4504 .into(),
4505 ),
4506 (
4507 ACT_COMPLETED,
4508 ActCompletedData {
4509 completed: true,
4510 success_count: 0,
4511 error_count: 0,
4512 duration_ms: None,
4513 headline: None,
4514 }
4515 .into(),
4516 ),
4517 (
4518 SESSION_STARTED,
4519 SessionStartedData {
4520 harness_id: test_harness_id(),
4521 agent_id: Some(test_agent_id()),
4522 model_id: None,
4523 }
4524 .into(),
4525 ),
4526 (
4527 SESSION_ACTIVATED,
4528 SessionActivatedData {
4529 turn_id: test_turn_id(),
4530 input_message_id: test_message_id(),
4531 }
4532 .into(),
4533 ),
4534 (
4535 SESSION_IDLED,
4536 SessionIdledData {
4537 turn_id: test_turn_id(),
4538 iterations: None,
4539 usage: None,
4540 }
4541 .into(),
4542 ),
4543 ];
4544
4545 for (event_type, original) in test_cases {
4546 let json = serde_json::to_value(&original).unwrap();
4548 let deserialized = deserialize_event_data(event_type, json);
4550 assert_eq!(
4552 original.event_type(),
4553 deserialized.event_type(),
4554 "Event type mismatch for {}",
4555 event_type
4556 );
4557 }
4558 }
4559
4560 #[test]
4566 fn event_structure_has_required_fields() {
4567 let session_id = test_session_id();
4568 let context = EventContext::turn(test_turn_id(), test_message_id());
4569 let event = Event::new(
4570 session_id,
4571 context,
4572 InputMessageData::new(Message::user("test")),
4573 );
4574
4575 let json = serde_json::to_value(&event).unwrap();
4577 assert!(json.get("id").is_some(), "Missing id field");
4578 assert!(json.get("type").is_some(), "Missing type field");
4579 assert!(json.get("ts").is_some(), "Missing ts field");
4580 assert!(json.get("session_id").is_some(), "Missing session_id field");
4581 assert!(json.get("context").is_some(), "Missing context field");
4582 assert!(json.get("data").is_some(), "Missing data field");
4583 }
4584
4585 #[test]
4586 fn event_context_span_fields() {
4587 let context = EventContext::empty().with_span(
4588 "trace123".to_string(),
4589 "span456".to_string(),
4590 Some("parent789".to_string()),
4591 );
4592
4593 let json = serde_json::to_value(&context).unwrap();
4594 assert_eq!(
4595 json.get("trace_id").and_then(|v| v.as_str()),
4596 Some("trace123")
4597 );
4598 assert_eq!(
4599 json.get("span_id").and_then(|v| v.as_str()),
4600 Some("span456")
4601 );
4602 assert_eq!(
4603 json.get("parent_span_id").and_then(|v| v.as_str()),
4604 Some("parent789")
4605 );
4606 }
4607
4608 #[test]
4609 fn is_unsupported_returns_false_for_known_types() {
4610 let data = InputMessageData::new(Message::user("test"));
4611 let event_data: EventData = data.into();
4612 assert!(!event_data.is_unsupported());
4613 }
4614
4615 #[test]
4616 fn is_unsupported_returns_true_for_unsupported() {
4617 let data = deserialize_event_data("unknown.type", serde_json::json!({}));
4618 assert!(data.is_unsupported());
4619 }
4620}