1use chrono::{DateTime, Utc};
22use serde::{Deserialize, Deserializer, Serialize};
23use serde_json::Value;
24use std::collections::HashMap;
25use uuid::Uuid;
26
27#[cfg(feature = "openapi")]
28use utoipa::ToSchema;
29
30use crate::localization::localized_tool_display_name;
31use crate::typed_id::{AgentId, EventId, ExecId, HarnessId, MessageId, ModelId, SessionId, TurnId};
32use crate::user_facing_error::{UserFacingError, UserFacingErrorFields};
33
34pub const INPUT_MESSAGE: &str = "input.message";
40
41pub const OUTPUT_MESSAGE_STARTED: &str = "output.message.started";
43pub const OUTPUT_MESSAGE_DELTA: &str = "output.message.delta";
44pub const OUTPUT_MESSAGE_COMPLETED: &str = "output.message.completed";
45pub const OUTPUT_MESSAGE_REPLACED: &str = "output.message.replaced";
50
51pub const TURN_STARTED: &str = "turn.started";
53pub const TURN_COMPLETED: &str = "turn.completed";
54pub const TURN_FAILED: &str = "turn.failed";
55pub const TURN_SEALED: &str = "turn.sealed";
60pub const TURN_CANCELLED: &str = "turn.cancelled";
61
62pub const REASON_STARTED: &str = "reason.started";
64pub const REASON_COMPLETED: &str = "reason.completed";
65pub const REASON_RECOVERED: &str = "reason.recovered";
66pub const CAPABILITY_USAGE: &str = "capability.usage";
67pub const ACT_STARTED: &str = "act.started";
68pub const ACT_COMPLETED: &str = "act.completed";
69pub const TOOL_STARTED: &str = "tool.started";
70pub const TOOL_COMPLETED: &str = "tool.completed";
71pub const TOOL_PROGRESS: &str = "tool.progress";
72pub const TOOL_OUTPUT_DELTA: &str = "tool.output.delta";
73pub const TOOL_CALL_REQUESTED: &str = "tool.call_requested";
74pub const TRANSCRIPT_REPAIRED: &str = "transcript.repaired";
75pub const TOOL_CALL_REPAIRED: &str = "tool.call_repaired";
79
80pub const LLM_GENERATION: &str = "llm.generation";
82
83fn is_ephemeral_event_type(event_type: &str) -> bool {
87 matches!(
88 event_type,
89 OUTPUT_MESSAGE_DELTA
90 | REASON_THINKING_DELTA
91 | TOOL_OUTPUT_DELTA
92 | VOICE_INPUT_TRANSCRIPT_DELTA
93 | VOICE_OUTPUT_TRANSCRIPT_DELTA
94 )
95}
96
97pub const REASON_THINKING_STARTED: &str = "reason.thinking.started";
99pub const REASON_THINKING_DELTA: &str = "reason.thinking.delta";
100pub const REASON_THINKING_COMPLETED: &str = "reason.thinking.completed";
101
102pub const REASON_ITEM: &str = "reason.item";
109
110pub const SESSION_STARTED: &str = "session.started";
112pub const SESSION_ACTIVATED: &str = "session.activated";
113pub const SESSION_IDLED: &str = "session.idled";
114pub const SESSION_TITLE_UPDATED: &str = "session.title.updated";
117
118pub const SCHEDULE_TRIGGERED: &str = "schedule.triggered";
120
121pub const TASK_CREATED: &str = "task.created";
128pub const TASK_UPDATED: &str = "task.updated";
129pub const TASK_MESSAGE_SENT: &str = "task.message.sent";
130pub const TASK_MESSAGE_RECEIVED: &str = "task.message.received";
131
132pub const CONTEXT_COMPACTING: &str = "context.compacting";
134pub const CONTEXT_COMPACTED: &str = "context.compacted";
135
136pub const FILE_WRITTEN: &str = "file.written";
138
139pub const BUDGET_WARNING: &str = "budget.warning";
141pub const BUDGET_PAUSED: &str = "budget.paused";
142pub const BUDGET_EXHAUSTED: &str = "budget.exhausted";
143pub const BUDGET_RESUMED: &str = "budget.resumed";
144
145pub const VOICE_SESSION_STARTED: &str = "voice.session.started";
147pub const VOICE_INPUT_TRANSCRIPT_DELTA: &str = "voice.input_transcript.delta";
148pub const VOICE_INPUT_TRANSCRIPT_COMPLETED: &str = "voice.input_transcript.completed";
149pub const VOICE_OUTPUT_TRANSCRIPT_DELTA: &str = "voice.output_transcript.delta";
150pub const VOICE_OUTPUT_TRANSCRIPT_COMPLETED: &str = "voice.output_transcript.completed";
151pub const VOICE_SESSION_ENDED: &str = "voice.session.ended";
152pub const VOICE_SESSION_FAILED: &str = "voice.session.failed";
153
154pub const VALID_EVENT_TYPES: &[&str] = &[
158 INPUT_MESSAGE,
159 OUTPUT_MESSAGE_STARTED,
160 OUTPUT_MESSAGE_DELTA,
161 OUTPUT_MESSAGE_COMPLETED,
162 OUTPUT_MESSAGE_REPLACED,
163 TURN_STARTED,
164 TURN_COMPLETED,
165 TURN_FAILED,
166 TURN_SEALED,
167 TURN_CANCELLED,
168 REASON_STARTED,
169 REASON_COMPLETED,
170 REASON_RECOVERED,
171 ACT_STARTED,
172 ACT_COMPLETED,
173 TOOL_STARTED,
174 TOOL_COMPLETED,
175 TOOL_PROGRESS,
176 TOOL_OUTPUT_DELTA,
177 TOOL_CALL_REQUESTED,
178 TRANSCRIPT_REPAIRED,
179 TOOL_CALL_REPAIRED,
180 LLM_GENERATION,
181 REASON_THINKING_STARTED,
182 REASON_THINKING_DELTA,
183 REASON_THINKING_COMPLETED,
184 REASON_ITEM,
185 SESSION_STARTED,
186 SESSION_ACTIVATED,
187 SESSION_IDLED,
188 SESSION_TITLE_UPDATED,
189 SCHEDULE_TRIGGERED,
190 CONTEXT_COMPACTING,
191 CONTEXT_COMPACTED,
192 BUDGET_WARNING,
193 BUDGET_PAUSED,
194 BUDGET_EXHAUSTED,
195 BUDGET_RESUMED,
196 VOICE_SESSION_STARTED,
197 VOICE_INPUT_TRANSCRIPT_DELTA,
198 VOICE_INPUT_TRANSCRIPT_COMPLETED,
199 VOICE_OUTPUT_TRANSCRIPT_DELTA,
200 VOICE_OUTPUT_TRANSCRIPT_COMPLETED,
201 VOICE_SESSION_ENDED,
202 VOICE_SESSION_FAILED,
203 FILE_WRITTEN,
204 CAPABILITY_USAGE,
205];
206
207use crate::atoms::AtomContext;
212
213#[derive(Debug, Clone, Serialize, Deserialize, Default)]
220#[cfg_attr(feature = "openapi", derive(ToSchema))]
221pub struct EventContext {
222 #[serde(skip_serializing_if = "Option::is_none")]
224 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "turn_01933b5a00007000800000000000001"))]
225 pub turn_id: Option<TurnId>,
226
227 #[serde(skip_serializing_if = "Option::is_none")]
229 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "message_01933b5a00007000800000000000001"))]
230 pub input_message_id: Option<MessageId>,
231
232 #[serde(skip_serializing_if = "Option::is_none")]
234 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "exec_01933b5a00007000800000000000001"))]
235 pub exec_id: Option<ExecId>,
236
237 #[serde(skip_serializing_if = "Option::is_none")]
240 pub trace_id: Option<String>,
241
242 #[serde(skip_serializing_if = "Option::is_none")]
245 pub span_id: Option<String>,
246
247 #[serde(skip_serializing_if = "Option::is_none")]
250 pub parent_span_id: Option<String>,
251}
252
253impl EventContext {
254 pub fn empty() -> Self {
256 Self::default()
257 }
258
259 pub fn from_atom_context(ctx: &AtomContext) -> Self {
261 Self {
262 turn_id: Some(ctx.turn_id),
263 input_message_id: Some(ctx.input_message_id),
264 exec_id: Some(ctx.exec_id),
265 trace_id: None,
266 span_id: None,
267 parent_span_id: None,
268 }
269 }
270
271 pub fn turn(turn_id: TurnId, input_message_id: MessageId) -> Self {
273 Self {
274 turn_id: Some(turn_id),
275 input_message_id: Some(input_message_id),
276 exec_id: None,
277 trace_id: None,
278 span_id: None,
279 parent_span_id: None,
280 }
281 }
282
283 pub fn with_span(
285 mut self,
286 trace_id: String,
287 span_id: String,
288 parent_span_id: Option<String>,
289 ) -> Self {
290 self.trace_id = Some(trace_id);
291 self.span_id = Some(span_id);
292 self.parent_span_id = parent_span_id;
293 self
294 }
295}
296
297#[derive(Debug, Clone, Serialize)]
313#[cfg_attr(feature = "openapi", derive(ToSchema))]
314pub struct Event {
315 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "event_01933b5a00007000800000000000001"))]
317 pub id: EventId,
318
319 #[serde(rename = "type")]
321 pub event_type: String,
322
323 pub ts: DateTime<Utc>,
325
326 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "session_01933b5a00007000800000000000001"))]
328 pub session_id: SessionId,
329
330 pub context: EventContext,
332
333 pub data: EventData,
336
337 #[serde(skip_serializing_if = "Option::is_none")]
339 pub metadata: Option<serde_json::Value>,
340
341 #[serde(skip_serializing_if = "Option::is_none")]
343 pub tags: Option<Vec<String>>,
344
345 #[serde(skip_serializing_if = "Option::is_none")]
347 pub sequence: Option<i32>,
348}
349
350#[derive(Debug, Deserialize)]
351struct RawEvent {
352 id: EventId,
353 #[serde(rename = "type")]
354 event_type: String,
355 ts: DateTime<Utc>,
356 session_id: SessionId,
357 context: EventContext,
358 data: serde_json::Value,
359 metadata: Option<serde_json::Value>,
360 tags: Option<Vec<String>>,
361 sequence: Option<i32>,
362}
363
364impl<'de> Deserialize<'de> for Event {
365 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
366 where
367 D: Deserializer<'de>,
368 {
369 let raw = RawEvent::deserialize(deserializer)?;
370 let data = deserialize_event_data(&raw.event_type, raw.data);
371 Ok(Self {
372 id: raw.id,
373 event_type: raw.event_type,
374 ts: raw.ts,
375 session_id: raw.session_id,
376 context: raw.context,
377 data,
378 metadata: raw.metadata,
379 tags: raw.tags,
380 sequence: raw.sequence,
381 })
382 }
383}
384
385impl Event {
386 pub fn new(session_id: SessionId, context: EventContext, data: impl Into<EventData>) -> Self {
390 let data = data.into();
391 let event_type = data.event_type().to_string();
392 Self {
393 id: EventId::new(),
394 event_type,
395 ts: Utc::now(),
396 session_id,
397 context,
398 data,
399 metadata: None,
400 tags: None,
401 sequence: None,
402 }
403 }
404
405 pub fn with_id(
407 id: EventId,
408 session_id: SessionId,
409 context: EventContext,
410 data: impl Into<EventData>,
411 ) -> Self {
412 let data = data.into();
413 let event_type = data.event_type().to_string();
414 Self {
415 id,
416 event_type,
417 ts: Utc::now(),
418 session_id,
419 context,
420 data,
421 metadata: None,
422 tags: None,
423 sequence: None,
424 }
425 }
426
427 pub fn with_sequence(mut self, sequence: i32) -> Self {
429 self.sequence = Some(sequence);
430 self
431 }
432
433 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
435 self.metadata = Some(metadata);
436 self
437 }
438
439 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
441 self.tags = Some(tags);
442 self
443 }
444
445 pub fn session_uuid(&self) -> Uuid {
447 self.session_id.uuid()
448 }
449
450 pub fn is_message_event(&self) -> bool {
452 self.event_type == INPUT_MESSAGE || self.event_type == OUTPUT_MESSAGE_COMPLETED
453 }
454
455 pub fn is_ephemeral(&self) -> bool {
463 is_ephemeral_event_type(&self.event_type)
464 }
465
466 pub fn is_input_event(&self) -> bool {
468 self.event_type.starts_with("input.")
469 }
470
471 pub fn is_output_event(&self) -> bool {
473 self.event_type.starts_with("output.")
474 }
475
476 pub fn is_atom_event(&self) -> bool {
478 matches!(
479 self.event_type.as_str(),
480 REASON_STARTED
481 | REASON_COMPLETED
482 | REASON_RECOVERED
483 | ACT_STARTED
484 | ACT_COMPLETED
485 | TOOL_STARTED
486 | TOOL_COMPLETED
487 | TOOL_PROGRESS
488 | TOOL_CALL_REQUESTED
489 | TRANSCRIPT_REPAIRED
490 )
491 }
492
493 pub fn is_turn_event(&self) -> bool {
495 self.event_type.starts_with("turn.")
496 }
497
498 pub fn is_session_event(&self) -> bool {
500 self.event_type.starts_with("session.")
501 }
502
503 pub fn is_unsupported(&self) -> bool {
506 self.data.is_unsupported()
507 }
508}
509
510use crate::message::{ContentPart, ExecutionPhase, Message};
515use crate::tool_narration::{
516 ToolNarrationPhase, render_group_headline_with_locale, render_tool_narration_with_locale,
517};
518use crate::tool_types::ToolCall;
519
520#[derive(Debug, Clone, Serialize, Deserialize)]
522#[cfg_attr(feature = "openapi", derive(ToSchema))]
523pub struct ModelMetadata {
524 pub model: String,
526
527 #[serde(skip_serializing_if = "Option::is_none")]
529 pub model_id: Option<Uuid>,
530
531 #[serde(skip_serializing_if = "Option::is_none")]
533 pub provider_id: Option<Uuid>,
534}
535
536#[derive(Debug, Clone, Serialize, Deserialize, Default)]
563#[cfg_attr(feature = "openapi", derive(ToSchema))]
564pub struct TokenUsage {
565 pub input_tokens: u32,
568 pub output_tokens: u32,
570 #[serde(skip_serializing_if = "Option::is_none")]
572 pub cache_read_tokens: Option<u32>,
573 #[serde(skip_serializing_if = "Option::is_none")]
575 pub cache_creation_tokens: Option<u32>,
576
577 #[serde(skip_serializing_if = "Option::is_none")]
581 pub actual_cost_usd: Option<f64>,
582
583 #[serde(skip_serializing_if = "Option::is_none")]
588 pub estimated_cost_usd: Option<f64>,
589
590 #[serde(skip_serializing_if = "Option::is_none")]
593 pub effective_cost_usd: Option<f64>,
594}
595
596impl TokenUsage {
597 pub fn new(input_tokens: u32, output_tokens: u32) -> Self {
599 Self {
600 input_tokens,
601 output_tokens,
602 cache_read_tokens: None,
603 cache_creation_tokens: None,
604 actual_cost_usd: None,
605 estimated_cost_usd: None,
606 effective_cost_usd: None,
607 }
608 }
609
610 pub fn with_cache(
612 input_tokens: u32,
613 output_tokens: u32,
614 cache_read_tokens: Option<u32>,
615 cache_creation_tokens: Option<u32>,
616 ) -> Self {
617 Self {
618 input_tokens,
619 output_tokens,
620 cache_read_tokens,
621 cache_creation_tokens,
622 actual_cost_usd: None,
623 estimated_cost_usd: None,
624 effective_cost_usd: None,
625 }
626 }
627
628 pub fn with_cost(
632 mut self,
633 actual_cost_usd: Option<f64>,
634 estimated_cost_usd: Option<f64>,
635 ) -> Self {
636 self.actual_cost_usd = actual_cost_usd;
637 self.estimated_cost_usd = estimated_cost_usd;
638 self
639 }
640
641 pub fn with_effective_cost(mut self, effective_cost_usd: Option<f64>) -> Self {
645 self.effective_cost_usd = effective_cost_usd;
646 self
647 }
648
649 pub fn effective_cost_usd(&self) -> Option<f64> {
653 self.effective_cost_usd
654 .or(self.actual_cost_usd.or(self.estimated_cost_usd))
655 }
656
657 pub fn total_tokens(&self) -> u32 {
659 self.input_tokens + self.output_tokens
660 }
661
662 pub fn add(&mut self, other: &TokenUsage) {
664 let current_cost = self.effective_cost_usd();
669 self.input_tokens += other.input_tokens;
670 self.output_tokens += other.output_tokens;
671 if let Some(cache) = other.cache_read_tokens {
672 *self.cache_read_tokens.get_or_insert(0) += cache;
673 }
674 if let Some(cache) = other.cache_creation_tokens {
675 *self.cache_creation_tokens.get_or_insert(0) += cache;
676 }
677 if let Some(cost) = other.actual_cost_usd {
678 *self.actual_cost_usd.get_or_insert(0.0) += cost;
679 }
680 if let Some(cost) = other.estimated_cost_usd {
681 *self.estimated_cost_usd.get_or_insert(0.0) += cost;
682 }
683 if let Some(cost) = other.effective_cost_usd() {
684 *self
685 .effective_cost_usd
686 .get_or_insert(current_cost.unwrap_or(0.0)) += cost;
687 }
688 }
689}
690
691#[cfg(test)]
692mod token_usage_tests {
693 use super::TokenUsage;
694
695 #[test]
696 fn aggregate_effective_cost_preserves_mixed_actual_and_estimated_total() {
697 let aggregate = TokenUsage::with_cache(30, 15, None, None)
698 .with_cost(Some(1.0), Some(2.0))
699 .with_effective_cost(Some(3.0));
700
701 assert_eq!(aggregate.actual_cost_usd, Some(1.0));
702 assert_eq!(aggregate.estimated_cost_usd, Some(2.0));
703 assert_eq!(aggregate.effective_cost_usd(), Some(3.0));
704 }
705
706 #[test]
707 fn generation_effective_cost_still_prefers_actual_over_estimated() {
708 let generation = TokenUsage::new(10, 5).with_cost(Some(1.0), Some(2.0));
709
710 assert_eq!(generation.effective_cost_usd(), Some(1.0));
711 }
712
713 #[test]
714 fn add_seeds_effective_cost_from_accumulator_implicit_cost() {
715 let mut aggregate = TokenUsage::new(10, 5).with_cost(Some(1.0), Some(10.0));
716 let generation = TokenUsage::new(20, 10).with_cost(None, Some(2.0));
717
718 aggregate.add(&generation);
719
720 assert_eq!(aggregate.actual_cost_usd, Some(1.0));
721 assert_eq!(aggregate.estimated_cost_usd, Some(12.0));
722 assert_eq!(aggregate.effective_cost_usd(), Some(3.0));
723 }
724
725 #[test]
726 fn add_effective_cost_does_not_double_count_when_actual_is_mutated() {
727 let mut aggregate = TokenUsage::new(10, 5).with_cost(Some(1.0), None);
732 let generation = TokenUsage::new(20, 10).with_cost(Some(3.0), None);
733
734 aggregate.add(&generation);
735
736 assert_eq!(aggregate.actual_cost_usd, Some(4.0));
737 assert_eq!(aggregate.effective_cost_usd(), Some(4.0));
738 }
739}
740
741#[derive(Debug, Clone, Serialize, Deserialize)]
743#[cfg_attr(feature = "openapi", derive(ToSchema))]
744pub struct InputMessageData {
745 pub message: Message,
747}
748
749impl InputMessageData {
750 pub fn new(message: Message) -> Self {
751 Self { message }
752 }
753}
754
755#[derive(Debug, Clone, Serialize, Deserialize)]
764#[cfg_attr(feature = "openapi", derive(ToSchema))]
765pub struct OutputMessageStartedData {
766 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
768 pub turn_id: TurnId,
769
770 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_550e8400e29b41d4a716446655440000"))]
773 pub message_id: MessageId,
774
775 #[serde(skip_serializing_if = "Option::is_none")]
777 pub model: Option<String>,
778
779 #[serde(skip_serializing_if = "Option::is_none")]
782 pub iteration: Option<u32>,
783
784 #[serde(default, skip_serializing_if = "Option::is_none")]
794 pub phase: Option<ExecutionPhase>,
795}
796
797#[derive(Debug, Clone, Serialize, Deserialize)]
802#[cfg_attr(feature = "openapi", derive(ToSchema))]
803pub struct OutputMessageDeltaData {
804 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
806 pub turn_id: TurnId,
807
808 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_550e8400e29b41d4a716446655440000"))]
811 pub message_id: MessageId,
812
813 pub delta: String,
815
816 pub accumulated: String,
818
819 #[serde(default, skip_serializing_if = "Option::is_none")]
830 pub phase: Option<ExecutionPhase>,
831}
832
833#[derive(Debug, Clone, Serialize, Deserialize)]
835#[cfg_attr(feature = "openapi", derive(ToSchema))]
836pub struct OutputMessageCompletedData {
837 pub message: Message,
839
840 #[serde(skip_serializing_if = "Option::is_none")]
842 pub metadata: Option<ModelMetadata>,
843
844 #[serde(skip_serializing_if = "Option::is_none")]
846 pub usage: Option<TokenUsage>,
847
848 #[serde(default, skip_serializing_if = "Option::is_none")]
850 pub error_code: Option<String>,
851
852 #[serde(default, skip_serializing_if = "Option::is_none")]
854 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
855 pub error_fields: Option<UserFacingErrorFields>,
856
857 #[serde(default, skip_serializing_if = "Option::is_none")]
861 pub error_disclosure: Option<String>,
862}
863
864impl OutputMessageCompletedData {
865 pub fn new(message: Message) -> Self {
866 Self {
867 message,
868 metadata: None,
869 usage: None,
870 error_code: None,
871 error_fields: None,
872 error_disclosure: None,
873 }
874 }
875
876 pub fn with_metadata(mut self, metadata: ModelMetadata) -> Self {
877 self.metadata = Some(metadata);
878 self
879 }
880
881 pub fn with_usage(mut self, usage: TokenUsage) -> Self {
882 self.usage = Some(usage);
883 self
884 }
885
886 pub fn with_user_facing_error(mut self, error: &UserFacingError) -> Self {
887 error.apply_to_event_fields(&mut self.error_code, &mut self.error_fields);
888 self
889 }
890
891 pub fn with_error_disclosure(mut self, mode: crate::ErrorDisclosure) -> Self {
892 self.error_disclosure = Some(mode.as_str().to_string());
893 self
894 }
895}
896
897#[derive(Debug, Clone, Serialize, Deserialize)]
904#[cfg_attr(feature = "openapi", derive(ToSchema))]
905pub struct OutputMessageReplacedData {
906 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
908 pub turn_id: TurnId,
909
910 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_550e8400e29b41d4a716446655440000"))]
913 pub message_id: MessageId,
914
915 pub guardrail_capability_id: String,
918
919 pub guardrail_id: String,
921
922 pub reason_code: String,
925
926 pub replacement: String,
928}
929
930#[derive(Debug, Clone, Serialize, Deserialize)]
936#[cfg_attr(feature = "openapi", derive(ToSchema))]
937pub struct ReasonStartedData {
938 pub harness_id: HarnessId,
940
941 #[serde(skip_serializing_if = "Option::is_none")]
943 pub agent_id: Option<AgentId>,
944
945 #[serde(skip_serializing_if = "Option::is_none")]
947 pub metadata: Option<ModelMetadata>,
948}
949
950#[derive(Debug, Clone, Serialize, Deserialize)]
952#[cfg_attr(feature = "openapi", derive(ToSchema))]
953pub struct ReasonCompletedData {
954 pub success: bool,
956
957 #[serde(skip_serializing_if = "Option::is_none")]
959 pub text_preview: Option<String>,
960
961 pub has_tool_calls: bool,
963
964 pub tool_call_count: u32,
966
967 #[serde(skip_serializing_if = "Option::is_none")]
969 pub error: Option<String>,
970
971 #[serde(skip_serializing_if = "Option::is_none")]
973 pub duration_ms: Option<u64>,
974
975 #[serde(skip_serializing_if = "Option::is_none")]
977 pub usage: Option<TokenUsage>,
978}
979
980impl ReasonCompletedData {
981 pub fn success(
982 text: &str,
983 has_tool_calls: bool,
984 tool_call_count: u32,
985 duration_ms: Option<u64>,
986 usage: Option<TokenUsage>,
987 ) -> Self {
988 let text_preview = if text.is_empty() {
989 None
990 } else {
991 Some(text.chars().take(200).collect())
992 };
993
994 Self {
995 success: true,
996 text_preview,
997 has_tool_calls,
998 tool_call_count,
999 error: None,
1000 duration_ms,
1001 usage,
1002 }
1003 }
1004
1005 pub fn failure(error: String, duration_ms: Option<u64>) -> Self {
1006 Self {
1007 success: false,
1008 text_preview: None,
1009 has_tool_calls: false,
1010 tool_call_count: 0,
1011 error: Some(error),
1012 duration_ms,
1013 usage: None,
1014 }
1015 }
1016}
1017
1018#[derive(Debug, Clone, Serialize, Deserialize)]
1020#[cfg_attr(feature = "openapi", derive(ToSchema))]
1021#[serde(rename_all = "snake_case")]
1022pub enum RecoveryMode {
1023 Finalize,
1026 Restart,
1028}
1029
1030#[derive(Debug, Clone, Serialize, Deserialize)]
1036#[cfg_attr(feature = "openapi", derive(ToSchema))]
1037pub struct ReasonRecoveredData {
1038 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1040 pub turn_id: TurnId,
1041
1042 pub mode: RecoveryMode,
1044
1045 pub accumulated_len: usize,
1047}
1048
1049#[derive(Debug, Clone, Serialize, Deserialize)]
1051#[serde(rename_all = "snake_case")]
1052#[cfg_attr(feature = "openapi", derive(ToSchema))]
1053pub enum CapabilityUsageKind {
1054 Configured,
1055 Resolved,
1056 Exposed,
1057 Invoked,
1058 EffectRan,
1059}
1060
1061#[derive(Debug, Clone, Serialize, Deserialize)]
1065#[cfg_attr(feature = "openapi", derive(ToSchema))]
1066pub struct CapabilityUsageRecord {
1067 pub capability_id: String,
1069 #[serde(default, skip_serializing_if = "Option::is_none")]
1071 pub capability_name: Option<String>,
1072 pub usage_kind: CapabilityUsageKind,
1074 #[serde(default, skip_serializing_if = "Option::is_none")]
1076 pub tool_name: Option<String>,
1077 #[serde(default, skip_serializing_if = "Option::is_none")]
1079 pub usage_count: Option<u64>,
1080 #[serde(default, skip_serializing_if = "Option::is_none")]
1082 pub duration_ms: Option<u64>,
1083}
1084
1085#[derive(Debug, Clone, Serialize, Deserialize)]
1087#[cfg_attr(feature = "openapi", derive(ToSchema))]
1088pub struct CapabilityUsageData {
1089 pub records: Vec<CapabilityUsageRecord>,
1090}
1091
1092#[derive(Debug, Clone, Serialize, Deserialize)]
1094#[cfg_attr(feature = "openapi", derive(ToSchema))]
1095pub struct ToolCallSummary {
1096 pub id: String,
1097 pub name: String,
1098 #[serde(default, skip_serializing_if = "Option::is_none")]
1100 pub display_name: Option<String>,
1101 #[serde(default, skip_serializing_if = "Option::is_none")]
1103 pub narration: Option<String>,
1104}
1105
1106impl From<&ToolCall> for ToolCallSummary {
1107 fn from(tc: &ToolCall) -> Self {
1108 Self {
1109 id: tc.id.clone(),
1110 name: tc.name.clone(),
1111 display_name: None,
1112 narration: None,
1113 }
1114 }
1115}
1116
1117#[derive(Debug, Clone, Serialize, Deserialize)]
1119#[cfg_attr(feature = "openapi", derive(ToSchema))]
1120pub struct ToolDefinitionSummary {
1121 pub name: String,
1123 #[serde(default, skip_serializing_if = "Option::is_none")]
1125 pub display_name: Option<String>,
1126 #[serde(default, skip_serializing_if = "Option::is_none")]
1128 pub category: Option<String>,
1129 #[serde(default, skip_serializing_if = "Option::is_none")]
1131 pub capability_id: Option<String>,
1132 #[serde(default, skip_serializing_if = "Option::is_none")]
1134 pub capability_name: Option<String>,
1135 pub description: String,
1137}
1138
1139impl From<&crate::tool_types::ToolDefinition> for ToolDefinitionSummary {
1140 fn from(tool: &crate::tool_types::ToolDefinition) -> Self {
1141 let capability_attribution = tool.capability_attribution();
1142 Self {
1143 name: tool.name().to_string(),
1144 display_name: tool.display_name().map(|s| s.to_string()),
1145 category: tool.category().map(|s| s.to_string()),
1146 capability_id: capability_attribution.map(|(id, _)| id.to_string()),
1147 capability_name: capability_attribution.and_then(|(_, name)| name.map(str::to_string)),
1148 description: tool.description().to_string(),
1149 }
1150 }
1151}
1152
1153#[derive(Debug, Clone, Serialize, Deserialize)]
1155#[cfg_attr(feature = "openapi", derive(ToSchema))]
1156pub struct ActStartedData {
1157 pub tool_calls: Vec<ToolCallSummary>,
1159 #[serde(default, skip_serializing_if = "Option::is_none")]
1161 pub headline: Option<String>,
1162}
1163
1164impl ActStartedData {
1165 pub fn new(tool_calls: &[ToolCall]) -> Self {
1166 Self::new_with_locale(tool_calls, None)
1167 }
1168
1169 pub fn new_with_locale(tool_calls: &[ToolCall], locale: Option<&str>) -> Self {
1170 Self {
1171 tool_calls: tool_calls.iter().map(ToolCallSummary::from).collect(),
1172 headline: render_group_headline_with_locale(
1173 tool_calls,
1174 &[],
1175 ToolNarrationPhase::Started,
1176 locale,
1177 ),
1178 }
1179 }
1180
1181 pub fn with_definitions(
1183 tool_calls: &[ToolCall],
1184 tool_defs: &[crate::tool_types::ToolDefinition],
1185 ) -> Self {
1186 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
1187 }
1188
1189 pub fn with_definitions_and_locale(
1190 tool_calls: &[ToolCall],
1191 tool_defs: &[crate::tool_types::ToolDefinition],
1192 locale: Option<&str>,
1193 ) -> Self {
1194 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
1195 tool_defs.iter().map(|d| (d.name(), d)).collect();
1196 Self {
1197 tool_calls: tool_calls
1198 .iter()
1199 .map(|tc| {
1200 let tool_def = def_map.get(tc.name.as_str()).copied();
1201 let display_name = localized_tool_display_name(
1202 &tc.name,
1203 tool_def.and_then(|d| d.display_name()),
1204 locale,
1205 );
1206 ToolCallSummary {
1207 id: tc.id.clone(),
1208 name: tc.name.clone(),
1209 display_name,
1210 narration: Some(render_tool_narration_with_locale(
1211 tool_def,
1212 tc,
1213 ToolNarrationPhase::Started,
1214 locale,
1215 )),
1216 }
1217 })
1218 .collect(),
1219 headline: render_group_headline_with_locale(
1220 tool_calls,
1221 tool_defs,
1222 ToolNarrationPhase::Started,
1223 locale,
1224 ),
1225 }
1226 }
1227}
1228
1229#[derive(Debug, Clone, Serialize, Deserialize)]
1231#[cfg_attr(feature = "openapi", derive(ToSchema))]
1232pub struct ActCompletedData {
1233 pub completed: bool,
1235
1236 pub success_count: u32,
1238
1239 pub error_count: u32,
1241
1242 #[serde(skip_serializing_if = "Option::is_none")]
1244 pub duration_ms: Option<u64>,
1245 #[serde(default, skip_serializing_if = "Option::is_none")]
1247 pub headline: Option<String>,
1248}
1249
1250#[derive(Debug, Clone, Serialize, Deserialize)]
1252#[cfg_attr(feature = "openapi", derive(ToSchema))]
1253pub struct ToolStartedData {
1254 pub tool_call: ToolCall,
1256 #[serde(default, skip_serializing_if = "Option::is_none")]
1258 pub tool_call_fingerprint: Option<String>,
1259 #[serde(default, skip_serializing_if = "Option::is_none")]
1261 pub display_name: Option<String>,
1262 #[serde(default, skip_serializing_if = "Option::is_none")]
1264 pub narration: Option<String>,
1265}
1266
1267#[derive(Debug, Clone, Serialize, Deserialize)]
1269#[cfg_attr(feature = "openapi", derive(ToSchema))]
1270pub struct ToolCompletedData {
1271 pub tool_call_id: String,
1273
1274 pub tool_name: String,
1276
1277 #[serde(default, skip_serializing_if = "Option::is_none")]
1279 pub tool_call_fingerprint: Option<String>,
1280
1281 #[serde(default, skip_serializing_if = "Option::is_none")]
1283 pub tool_result_fingerprint: Option<String>,
1284
1285 #[serde(default, skip_serializing_if = "Option::is_none")]
1287 pub display_name: Option<String>,
1288
1289 pub success: bool,
1291
1292 pub status: String,
1294
1295 #[serde(skip_serializing_if = "Option::is_none")]
1297 pub result: Option<Vec<ContentPart>>,
1298
1299 #[serde(skip_serializing_if = "Option::is_none")]
1301 pub error: Option<String>,
1302
1303 #[serde(skip_serializing_if = "Option::is_none")]
1305 pub duration_ms: Option<u64>,
1306
1307 #[serde(default, skip_serializing_if = "Option::is_none")]
1309 pub capability_id: Option<String>,
1310
1311 #[serde(default, skip_serializing_if = "Option::is_none")]
1313 pub capability_name: Option<String>,
1314
1315 #[serde(default, skip_serializing_if = "Option::is_none")]
1317 pub narration: Option<String>,
1318}
1319
1320impl ToolCompletedData {
1321 pub fn success(
1322 tool_call_id: String,
1323 tool_name: String,
1324 result: Vec<ContentPart>,
1325 duration_ms: Option<u64>,
1326 ) -> Self {
1327 Self {
1328 tool_call_id,
1329 tool_name,
1330 tool_call_fingerprint: None,
1331 tool_result_fingerprint: None,
1332 display_name: None,
1333 success: true,
1334 status: "success".to_string(),
1335 result: Some(result),
1336 error: None,
1337 duration_ms,
1338 capability_id: None,
1339 capability_name: None,
1340 narration: None,
1341 }
1342 }
1343
1344 pub fn failure(
1345 tool_call_id: String,
1346 tool_name: String,
1347 status: String,
1348 error: String,
1349 duration_ms: Option<u64>,
1350 ) -> Self {
1351 Self {
1352 tool_call_id,
1353 tool_name,
1354 tool_call_fingerprint: None,
1355 tool_result_fingerprint: None,
1356 display_name: None,
1357 success: false,
1358 status,
1359 result: None,
1360 error: Some(error),
1361 duration_ms,
1362 capability_id: None,
1363 capability_name: None,
1364 narration: None,
1365 }
1366 }
1367
1368 pub fn with_display_name(mut self, display_name: Option<String>) -> Self {
1370 self.display_name = display_name;
1371 self
1372 }
1373
1374 pub fn with_fingerprints(
1375 mut self,
1376 tool_call_fingerprint: String,
1377 tool_result_fingerprint: String,
1378 ) -> Self {
1379 self.tool_call_fingerprint = Some(tool_call_fingerprint);
1380 self.tool_result_fingerprint = Some(tool_result_fingerprint);
1381 self
1382 }
1383
1384 pub fn with_narration(mut self, narration: Option<String>) -> Self {
1386 self.narration = narration;
1387 self
1388 }
1389
1390 pub fn with_capability_attribution(
1392 mut self,
1393 capability_id: Option<String>,
1394 capability_name: Option<String>,
1395 ) -> Self {
1396 self.capability_id = capability_id;
1397 self.capability_name = capability_name;
1398 self
1399 }
1400}
1401
1402#[derive(Debug, Clone, Serialize, Deserialize)]
1408#[cfg_attr(feature = "openapi", derive(ToSchema))]
1409pub struct ToolProgressData {
1410 pub tool_call_id: String,
1412
1413 pub tool_name: String,
1415
1416 pub message: String,
1418
1419 #[serde(default, skip_serializing_if = "Option::is_none")]
1421 pub display_name: Option<String>,
1422}
1423
1424#[derive(Debug, Clone, Serialize, Deserialize)]
1434#[cfg_attr(feature = "openapi", derive(ToSchema))]
1435pub struct ToolOutputDeltaData {
1436 pub tool_call_id: String,
1438
1439 pub tool_name: String,
1441
1442 pub delta: String,
1444
1445 pub stream: String,
1447}
1448
1449#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1451#[cfg_attr(feature = "openapi", derive(ToSchema))]
1452#[serde(rename_all = "snake_case")]
1453pub enum TranscriptRepairAction {
1454 Replay,
1456 Synthesize,
1458}
1459
1460#[derive(Debug, Clone, Serialize, Deserialize)]
1466#[cfg_attr(feature = "openapi", derive(ToSchema))]
1467pub struct TranscriptRepairedData {
1468 pub tool_call_id: String,
1470
1471 #[serde(default, skip_serializing_if = "Option::is_none")]
1473 pub tool_name: Option<String>,
1474
1475 pub action: TranscriptRepairAction,
1477}
1478
1479#[derive(Debug, Clone, Serialize, Deserialize)]
1485#[cfg_attr(feature = "openapi", derive(ToSchema))]
1486pub struct ToolCallRepairedData {
1487 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1489 pub turn_id: TurnId,
1490
1491 pub tool_call_id: String,
1493
1494 pub tool_name: String,
1496
1497 pub outcome: String,
1499}
1500
1501#[derive(Debug, Clone, Serialize, Deserialize)]
1506#[cfg_attr(feature = "openapi", derive(ToSchema))]
1507pub struct ToolCallRequestedData {
1508 pub tool_calls: Vec<ToolCall>,
1510 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1512 pub tool_summaries: Vec<ToolCallSummary>,
1513 #[serde(default, skip_serializing_if = "Option::is_none")]
1515 pub headline: Option<String>,
1516}
1517
1518impl ToolCallRequestedData {
1519 pub fn with_definitions(
1520 tool_calls: &[ToolCall],
1521 tool_defs: &[crate::tool_types::ToolDefinition],
1522 ) -> Self {
1523 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
1524 }
1525
1526 pub fn with_definitions_and_locale(
1527 tool_calls: &[ToolCall],
1528 tool_defs: &[crate::tool_types::ToolDefinition],
1529 locale: Option<&str>,
1530 ) -> Self {
1531 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
1532 tool_defs.iter().map(|d| (d.name(), d)).collect();
1533
1534 let tool_summaries = tool_calls
1535 .iter()
1536 .map(|tool_call| {
1537 let tool_def = def_map.get(tool_call.name.as_str()).copied();
1538 ToolCallSummary {
1539 id: tool_call.id.clone(),
1540 name: tool_call.name.clone(),
1541 display_name: localized_tool_display_name(
1542 &tool_call.name,
1543 tool_def.and_then(|def| def.display_name()),
1544 locale,
1545 ),
1546 narration: Some(render_tool_narration_with_locale(
1547 tool_def,
1548 tool_call,
1549 ToolNarrationPhase::Waiting,
1550 locale,
1551 )),
1552 }
1553 })
1554 .collect();
1555
1556 Self {
1557 tool_calls: tool_calls.to_vec(),
1558 tool_summaries,
1559 headline: render_group_headline_with_locale(
1560 tool_calls,
1561 tool_defs,
1562 ToolNarrationPhase::Waiting,
1563 locale,
1564 ),
1565 }
1566 }
1567}
1568
1569#[derive(Debug, Clone, Serialize, Deserialize)]
1575#[cfg_attr(feature = "openapi", derive(ToSchema))]
1576pub struct LlmGenerationOutput {
1577 #[serde(skip_serializing_if = "Option::is_none")]
1579 pub text: Option<String>,
1580
1581 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1583 pub tool_calls: Vec<ToolCall>,
1584}
1585
1586#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1591#[cfg_attr(feature = "openapi", derive(ToSchema))]
1592pub struct LlmRequestOptions {
1593 #[serde(skip_serializing_if = "Option::is_none")]
1595 pub prompt_cache: Option<LlmPromptCacheInfo>,
1596 #[serde(skip_serializing_if = "Option::is_none")]
1598 pub tool_search: Option<LlmToolSearchInfo>,
1599 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1601 pub provider_options: HashMap<String, Value>,
1602 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1605 pub metadata: HashMap<String, String>,
1606}
1607
1608impl LlmRequestOptions {
1609 pub fn is_empty(&self) -> bool {
1610 self.prompt_cache.is_none()
1611 && self.tool_search.is_none()
1612 && self.provider_options.is_empty()
1613 && self.metadata.is_empty()
1614 }
1615}
1616
1617#[derive(Debug, Clone, Serialize, Deserialize)]
1619#[cfg_attr(feature = "openapi", derive(ToSchema))]
1620pub struct LlmPromptCacheInfo {
1621 pub enabled: bool,
1623 pub strategy: crate::driver_registry::PromptCacheStrategy,
1625 #[serde(skip_serializing_if = "Option::is_none")]
1627 pub provider_mode: Option<String>,
1628}
1629
1630#[derive(Debug, Clone, Serialize, Deserialize)]
1632#[cfg_attr(feature = "openapi", derive(ToSchema))]
1633pub struct LlmToolSearchInfo {
1634 pub enabled: bool,
1636 pub threshold: usize,
1638}
1639
1640#[derive(Debug, Clone, Serialize, Deserialize)]
1642#[cfg_attr(feature = "openapi", derive(ToSchema))]
1643pub struct LlmGenerationMetadata {
1644 #[cfg_attr(feature = "openapi", schema(example = "claude-sonnet-4-5"))]
1646 pub model: String,
1647
1648 #[serde(skip_serializing_if = "Option::is_none")]
1650 #[cfg_attr(feature = "openapi", schema(example = "anthropic"))]
1651 pub provider: Option<String>,
1652
1653 #[serde(skip_serializing_if = "Option::is_none")]
1655 pub usage: Option<TokenUsage>,
1656
1657 #[serde(skip_serializing_if = "Option::is_none")]
1659 #[cfg_attr(feature = "openapi", schema(example = 1_842u64))]
1660 pub duration_ms: Option<u64>,
1661
1662 #[serde(skip_serializing_if = "Option::is_none")]
1664 #[cfg_attr(feature = "openapi", schema(example = 312u64))]
1665 pub time_to_first_token_ms: Option<u64>,
1666
1667 #[cfg_attr(feature = "openapi", schema(example = true))]
1669 pub success: bool,
1670
1671 #[serde(skip_serializing_if = "Option::is_none")]
1673 #[cfg_attr(feature = "openapi", schema(example = "provider returned 503"))]
1674 pub error: Option<String>,
1675
1676 #[serde(skip_serializing_if = "Option::is_none")]
1679 #[cfg_attr(feature = "openapi", schema(example = json!(["tool_calls"])))]
1680 pub finish_reasons: Option<Vec<String>>,
1681
1682 #[serde(skip_serializing_if = "Option::is_none")]
1685 #[cfg_attr(feature = "openapi", schema(example = "msg_01ABCDef0123456789"))]
1686 pub response_id: Option<String>,
1687
1688 #[serde(skip_serializing_if = "Option::is_none")]
1691 pub retry: Option<LlmRetryInfo>,
1692
1693 #[serde(skip_serializing_if = "Option::is_none")]
1696 pub compaction: Option<LlmCompactionInfo>,
1697
1698 #[serde(skip_serializing_if = "Option::is_none")]
1700 pub request_options: Option<LlmRequestOptions>,
1701}
1702
1703#[derive(Debug, Clone, Serialize, Deserialize)]
1705#[cfg_attr(feature = "openapi", derive(ToSchema))]
1706pub struct LlmRetryInfo {
1707 pub attempts: u32,
1709
1710 pub total_wait_ms: u64,
1712}
1713
1714#[derive(Debug, Clone, Serialize, Deserialize)]
1719#[cfg_attr(feature = "openapi", derive(ToSchema))]
1720pub struct LlmCompactionInfo {
1721 pub compacted: bool,
1723
1724 #[serde(skip_serializing_if = "Option::is_none")]
1726 pub input_tokens_before: Option<u32>,
1727
1728 #[serde(skip_serializing_if = "Option::is_none")]
1730 pub input_tokens_after: Option<u32>,
1731
1732 #[serde(skip_serializing_if = "Option::is_none")]
1734 pub duration_ms: Option<u64>,
1735}
1736
1737impl LlmCompactionInfo {
1738 pub fn new(
1740 input_tokens_before: Option<u32>,
1741 input_tokens_after: Option<u32>,
1742 duration_ms: Option<u64>,
1743 ) -> Self {
1744 Self {
1745 compacted: true,
1746 input_tokens_before,
1747 input_tokens_after,
1748 duration_ms,
1749 }
1750 }
1751}
1752
1753#[derive(Debug, Clone, Serialize, Deserialize)]
1758#[cfg_attr(feature = "openapi", derive(ToSchema))]
1759pub struct LlmGenerationData {
1760 pub messages: Vec<Message>,
1762
1763 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1765 pub tools: Vec<ToolDefinitionSummary>,
1766
1767 pub output: LlmGenerationOutput,
1769
1770 pub metadata: LlmGenerationMetadata,
1772}
1773
1774impl LlmGenerationData {
1775 #[allow(clippy::too_many_arguments)]
1777 pub fn success(
1778 messages: Vec<Message>,
1779 tools: Vec<ToolDefinitionSummary>,
1780 text: Option<String>,
1781 tool_calls: Vec<ToolCall>,
1782 model: String,
1783 provider: Option<String>,
1784 usage: Option<TokenUsage>,
1785 duration_ms: Option<u64>,
1786 time_to_first_token_ms: Option<u64>,
1787 ) -> Self {
1788 let finish_reasons = if !tool_calls.is_empty() {
1790 Some(vec!["tool_calls".to_string()])
1791 } else {
1792 Some(vec!["stop".to_string()])
1793 };
1794
1795 Self {
1796 messages,
1797 tools,
1798 output: LlmGenerationOutput { text, tool_calls },
1799 metadata: LlmGenerationMetadata {
1800 model,
1801 provider,
1802 usage,
1803 duration_ms,
1804 time_to_first_token_ms,
1805 success: true,
1806 error: None,
1807 finish_reasons,
1808 response_id: None,
1809 retry: None,
1810 compaction: None,
1811 request_options: None,
1812 },
1813 }
1814 }
1815
1816 #[allow(clippy::too_many_arguments)]
1818 pub fn success_with_metadata(
1819 messages: Vec<Message>,
1820 tools: Vec<ToolDefinitionSummary>,
1821 text: Option<String>,
1822 tool_calls: Vec<ToolCall>,
1823 model: String,
1824 provider: Option<String>,
1825 usage: Option<TokenUsage>,
1826 duration_ms: Option<u64>,
1827 time_to_first_token_ms: Option<u64>,
1828 finish_reasons: Option<Vec<String>>,
1829 response_id: Option<String>,
1830 ) -> Self {
1831 Self {
1832 messages,
1833 tools,
1834 output: LlmGenerationOutput { text, tool_calls },
1835 metadata: LlmGenerationMetadata {
1836 model,
1837 provider,
1838 usage,
1839 duration_ms,
1840 time_to_first_token_ms,
1841 success: true,
1842 error: None,
1843 finish_reasons,
1844 response_id,
1845 retry: None,
1846 compaction: None,
1847 request_options: None,
1848 },
1849 }
1850 }
1851
1852 #[allow(clippy::too_many_arguments)]
1854 pub fn success_with_retry(
1855 messages: Vec<Message>,
1856 tools: Vec<ToolDefinitionSummary>,
1857 text: Option<String>,
1858 tool_calls: Vec<ToolCall>,
1859 model: String,
1860 provider: Option<String>,
1861 usage: Option<TokenUsage>,
1862 duration_ms: Option<u64>,
1863 time_to_first_token_ms: Option<u64>,
1864 finish_reasons: Option<Vec<String>>,
1865 response_id: Option<String>,
1866 retry: Option<LlmRetryInfo>,
1867 ) -> Self {
1868 Self {
1869 messages,
1870 tools,
1871 output: LlmGenerationOutput { text, tool_calls },
1872 metadata: LlmGenerationMetadata {
1873 model,
1874 provider,
1875 usage,
1876 duration_ms,
1877 time_to_first_token_ms,
1878 success: true,
1879 error: None,
1880 finish_reasons,
1881 response_id,
1882 retry,
1883 compaction: None,
1884 request_options: None,
1885 },
1886 }
1887 }
1888
1889 pub fn failure(
1891 messages: Vec<Message>,
1892 tools: Vec<ToolDefinitionSummary>,
1893 model: String,
1894 provider: Option<String>,
1895 error: String,
1896 duration_ms: Option<u64>,
1897 time_to_first_token_ms: Option<u64>,
1898 ) -> Self {
1899 Self {
1900 messages,
1901 tools,
1902 output: LlmGenerationOutput {
1903 text: None,
1904 tool_calls: vec![],
1905 },
1906 metadata: LlmGenerationMetadata {
1907 model,
1908 provider,
1909 usage: None,
1910 duration_ms,
1911 time_to_first_token_ms,
1912 success: false,
1913 error: Some(error),
1914 finish_reasons: Some(vec!["error".to_string()]),
1915 response_id: None,
1916 retry: None,
1917 compaction: None,
1918 request_options: None,
1919 },
1920 }
1921 }
1922
1923 pub fn with_compaction(mut self, compaction: LlmCompactionInfo) -> Self {
1927 self.metadata.compaction = Some(compaction);
1928 self
1929 }
1930
1931 pub fn with_retry(mut self, retry: LlmRetryInfo) -> Self {
1933 self.metadata.retry = Some(retry);
1934 self
1935 }
1936
1937 pub fn with_request_options(mut self, request_options: LlmRequestOptions) -> Self {
1939 if !request_options.is_empty() {
1940 self.metadata.request_options = Some(request_options);
1941 }
1942 self
1943 }
1944}
1945
1946#[derive(Debug, Clone, Serialize, Deserialize)]
1956#[cfg_attr(feature = "openapi", derive(ToSchema))]
1957pub struct ReasonThinkingStartedData {
1958 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1960 pub turn_id: TurnId,
1961
1962 #[serde(skip_serializing_if = "Option::is_none")]
1964 pub model: Option<String>,
1965}
1966
1967#[derive(Debug, Clone, Serialize, Deserialize)]
1973#[cfg_attr(feature = "openapi", derive(ToSchema))]
1974pub struct ReasonThinkingDeltaData {
1975 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1977 pub turn_id: TurnId,
1978
1979 pub delta: String,
1981
1982 pub accumulated: String,
1984}
1985
1986#[derive(Debug, Clone, Serialize, Deserialize)]
1991#[cfg_attr(feature = "openapi", derive(ToSchema))]
1992pub struct ReasonThinkingCompletedData {
1993 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1995 pub turn_id: TurnId,
1996
1997 pub thinking: String,
1999}
2000
2001#[derive(Debug, Clone, Serialize, Deserialize)]
2009#[cfg_attr(feature = "openapi", derive(ToSchema))]
2010pub struct ReasonItemData {
2011 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2013 pub turn_id: TurnId,
2014
2015 pub provider: String,
2017
2018 #[serde(skip_serializing_if = "Option::is_none")]
2020 pub model: Option<String>,
2021
2022 pub item_id: String,
2024
2025 #[serde(skip_serializing_if = "Option::is_none")]
2027 pub encrypted_content: Option<String>,
2028
2029 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2032 pub summary: Vec<String>,
2033
2034 #[serde(skip_serializing_if = "Option::is_none")]
2036 pub token_count: Option<u32>,
2037}
2038
2039#[derive(Debug, Clone, Serialize, Deserialize)]
2045#[cfg_attr(feature = "openapi", derive(ToSchema))]
2046pub struct TurnStartedData {
2047 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2049 pub turn_id: TurnId,
2050
2051 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
2053 pub input_message_id: MessageId,
2054
2055 #[serde(skip_serializing_if = "Option::is_none")]
2057 pub input_content: Option<String>,
2058}
2059
2060#[derive(Debug, Clone, Serialize, Deserialize)]
2062#[cfg_attr(feature = "openapi", derive(ToSchema))]
2063pub struct TurnCompletedData {
2064 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2066 pub turn_id: TurnId,
2067
2068 pub iterations: u32,
2070
2071 #[serde(skip_serializing_if = "Option::is_none")]
2073 pub duration_ms: Option<u64>,
2074
2075 #[serde(skip_serializing_if = "Option::is_none")]
2077 pub usage: Option<TokenUsage>,
2078
2079 #[serde(skip_serializing_if = "Option::is_none")]
2081 pub input_content: Option<String>,
2082
2083 #[serde(skip_serializing_if = "Option::is_none")]
2085 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "message_01933b5a00007000800000000000001"))]
2086 pub final_message_id: Option<MessageId>,
2087
2088 #[serde(skip_serializing_if = "Option::is_none")]
2090 pub final_answer_preview: Option<String>,
2091
2092 #[serde(skip_serializing_if = "Option::is_none")]
2094 pub time_to_first_token_ms: Option<u64>,
2095
2096 #[serde(skip_serializing_if = "Option::is_none")]
2098 pub tool_call_count: Option<u32>,
2099
2100 #[serde(skip_serializing_if = "Option::is_none")]
2102 pub llm_call_count: Option<u32>,
2103
2104 #[serde(skip_serializing_if = "Option::is_none")]
2106 pub status: Option<String>,
2107}
2108
2109#[derive(Debug, Clone, Serialize, Deserialize)]
2111#[cfg_attr(feature = "openapi", derive(ToSchema))]
2112pub struct TurnFailedData {
2113 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2115 pub turn_id: TurnId,
2116
2117 pub error: String,
2119
2120 #[serde(default, skip_serializing_if = "Option::is_none")]
2122 pub error_code: Option<String>,
2123
2124 #[serde(default, skip_serializing_if = "Option::is_none")]
2126 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
2127 pub error_fields: Option<UserFacingErrorFields>,
2128
2129 #[serde(default, skip_serializing_if = "Option::is_none")]
2133 pub error_disclosure: Option<String>,
2134}
2135
2136#[derive(Debug, Clone, Serialize, Deserialize)]
2143#[cfg_attr(feature = "openapi", derive(ToSchema))]
2144pub struct TurnSealedData {
2145 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2147 pub turn_id: TurnId,
2148
2149 pub reason: String,
2152
2153 #[serde(default, skip_serializing_if = "Option::is_none")]
2155 pub detail: Option<String>,
2156
2157 #[serde(default, skip_serializing_if = "Option::is_none")]
2159 pub iterations: Option<u32>,
2160
2161 #[serde(default, skip_serializing_if = "Option::is_none")]
2163 pub usage: Option<TokenUsage>,
2164}
2165
2166#[derive(Debug, Clone, Serialize, Deserialize)]
2168#[cfg_attr(feature = "openapi", derive(ToSchema))]
2169pub struct TurnCancelledData {
2170 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2172 pub turn_id: TurnId,
2173
2174 #[serde(skip_serializing_if = "Option::is_none")]
2176 pub reason: Option<String>,
2177
2178 #[serde(skip_serializing_if = "Option::is_none")]
2180 pub usage: Option<TokenUsage>,
2181}
2182
2183#[derive(Debug, Clone, Serialize, Deserialize)]
2189#[cfg_attr(feature = "openapi", derive(ToSchema))]
2190pub struct SessionStartedData {
2191 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "harness_01933b5a00007000800000000000001"))]
2193 pub harness_id: HarnessId,
2194
2195 #[serde(skip_serializing_if = "Option::is_none")]
2197 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agent_01933b5a00007000800000000000001"))]
2198 pub agent_id: Option<AgentId>,
2199
2200 #[serde(skip_serializing_if = "Option::is_none")]
2202 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
2203 pub model_id: Option<ModelId>,
2204}
2205
2206#[derive(Debug, Clone, Serialize, Deserialize)]
2208#[cfg_attr(feature = "openapi", derive(ToSchema))]
2209pub struct SessionActivatedData {
2210 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2212 pub turn_id: TurnId,
2213
2214 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
2216 pub input_message_id: MessageId,
2217}
2218
2219#[derive(Debug, Clone, Serialize, Deserialize)]
2221#[cfg_attr(feature = "openapi", derive(ToSchema))]
2222pub struct SessionIdledData {
2223 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2225 pub turn_id: TurnId,
2226
2227 #[serde(skip_serializing_if = "Option::is_none")]
2229 pub iterations: Option<u32>,
2230
2231 #[serde(skip_serializing_if = "Option::is_none")]
2233 pub usage: Option<TokenUsage>,
2234}
2235
2236#[derive(Debug, Clone, Serialize, Deserialize)]
2238#[cfg_attr(feature = "openapi", derive(ToSchema))]
2239pub struct SessionTitleUpdatedData {
2240 pub previous_title: Option<String>,
2242
2243 pub title: String,
2245}
2246
2247#[derive(Debug, Clone, Serialize, Deserialize)]
2256#[cfg_attr(feature = "openapi", derive(ToSchema))]
2257pub struct SessionTaskEventData {
2258 pub task: crate::session_task::SessionTask,
2259}
2260
2261#[derive(Debug, Clone, Serialize, Deserialize)]
2263#[cfg_attr(feature = "openapi", derive(ToSchema))]
2264pub struct TaskMessageEventData {
2265 pub task_id: String,
2266 pub message: crate::session_task::TaskMessage,
2267}
2268
2269#[derive(Debug, Clone, Serialize, Deserialize)]
2275#[cfg_attr(feature = "openapi", derive(ToSchema))]
2276#[serde(rename_all = "snake_case")]
2277pub enum CompactionReason {
2278 ProactiveBudget,
2280 RequestTooLarge,
2282 Manual,
2284}
2285
2286impl std::fmt::Display for CompactionReason {
2287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2288 match self {
2289 Self::ProactiveBudget => write!(f, "proactive_budget"),
2290 Self::RequestTooLarge => write!(f, "request_too_large"),
2291 Self::Manual => write!(f, "manual"),
2292 }
2293 }
2294}
2295
2296#[derive(Debug, Clone, Serialize, Deserialize)]
2298#[cfg_attr(feature = "openapi", derive(ToSchema))]
2299pub struct ContextCompactingData {
2300 pub reason: CompactionReason,
2302 pub strategy: String,
2304 pub messages_before: usize,
2306 #[serde(default, skip_serializing_if = "Option::is_none")]
2308 pub tokens_before: Option<u64>,
2309 #[serde(default, skip_serializing_if = "Option::is_none")]
2311 pub bytes_before: Option<u64>,
2312}
2313
2314#[derive(Debug, Clone, Serialize, Deserialize)]
2316#[cfg_attr(feature = "openapi", derive(ToSchema))]
2317pub struct CompactionStepData {
2318 pub strategy: String,
2320 pub messages_after: usize,
2322 pub duration_ms: u64,
2324}
2325
2326#[derive(Debug, Clone, Serialize, Deserialize)]
2328#[cfg_attr(feature = "openapi", derive(ToSchema))]
2329pub struct ContextCompactedData {
2330 #[serde(default, skip_serializing_if = "Option::is_none")]
2332 pub checkpoint_id: Option<String>,
2333 pub strategy_used: String,
2335 pub messages_before: usize,
2337 pub messages_after: usize,
2339 #[serde(default, skip_serializing_if = "Option::is_none")]
2341 pub tokens_before: Option<u64>,
2342 #[serde(default, skip_serializing_if = "Option::is_none")]
2344 pub tokens_after: Option<u64>,
2345 #[serde(default, skip_serializing_if = "Option::is_none")]
2347 pub bytes_before: Option<u64>,
2348 #[serde(default, skip_serializing_if = "Option::is_none")]
2350 pub bytes_after: Option<u64>,
2351 pub duration_ms: u64,
2353 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2355 pub steps: Vec<CompactionStepData>,
2356}
2357
2358#[derive(Debug, Clone, Serialize, Deserialize)]
2364#[cfg_attr(feature = "openapi", derive(ToSchema))]
2365pub struct FileWrittenData {
2366 pub path: String,
2368 pub operation: String,
2370 pub size_bytes: i64,
2372 pub created: bool,
2374}
2375
2376pub const FILE_OP_CREATE: &str = "create";
2378pub const FILE_OP_UPDATE: &str = "update";
2379
2380#[derive(Debug, Clone, Serialize, Deserialize)]
2386#[cfg_attr(feature = "openapi", derive(ToSchema))]
2387pub struct BudgetEventData {
2388 pub budget_id: String,
2390 pub balance: f64,
2392 pub limit: f64,
2394 pub currency: String,
2396 #[serde(skip_serializing_if = "Option::is_none")]
2398 pub message: Option<String>,
2399 #[serde(skip_serializing_if = "Option::is_none")]
2401 pub soft_limit: Option<f64>,
2402}
2403
2404#[derive(Debug, Clone, Serialize, Deserialize)]
2410#[cfg_attr(feature = "openapi", derive(ToSchema))]
2411pub struct VoiceSessionStartedData {
2412 #[cfg_attr(
2414 feature = "openapi",
2415 schema(example = "voice_01933b5a00007000800000000000001")
2416 )]
2417 pub voice_connection_id: String,
2418 #[cfg_attr(feature = "openapi", schema(example = "gpt-realtime"))]
2420 pub model: String,
2421 #[cfg_attr(feature = "openapi", schema(example = "alloy"))]
2423 pub voice: String,
2424 #[cfg_attr(feature = "openapi", schema(example = "medium"))]
2426 pub reasoning_effort: String,
2427 #[cfg_attr(feature = "openapi", schema(example = "webrtc"))]
2429 pub transport: String,
2430}
2431
2432#[derive(Debug, Clone, Serialize, Deserialize)]
2434#[cfg_attr(feature = "openapi", derive(ToSchema))]
2435pub struct VoiceTranscriptData {
2436 pub voice_connection_id: String,
2438 #[serde(default, skip_serializing_if = "Option::is_none")]
2440 pub item_id: Option<String>,
2441 #[serde(default, skip_serializing_if = "Option::is_none")]
2443 pub response_id: Option<String>,
2444 #[serde(default, skip_serializing_if = "Option::is_none")]
2446 pub phase: Option<String>,
2447 #[serde(default, skip_serializing_if = "String::is_empty")]
2449 pub delta: String,
2450 pub accumulated: String,
2452}
2453
2454#[derive(Debug, Clone, Serialize, Deserialize)]
2456#[cfg_attr(feature = "openapi", derive(ToSchema))]
2457pub struct VoiceSessionEndedData {
2458 #[cfg_attr(
2460 feature = "openapi",
2461 schema(example = "voice_01933b5a00007000800000000000001")
2462 )]
2463 pub voice_connection_id: String,
2464 #[serde(default, skip_serializing_if = "Option::is_none")]
2466 #[cfg_attr(
2467 feature = "openapi",
2468 schema(example = "User hung up after refund confirmed.")
2469 )]
2470 pub reason: Option<String>,
2471 #[serde(default, skip_serializing_if = "Option::is_none")]
2474 #[cfg_attr(feature = "openapi", schema(example = 184_500_u64))]
2475 pub duration_ms: Option<u64>,
2476}
2477
2478#[derive(Debug, Clone, Serialize, Deserialize)]
2480#[cfg_attr(feature = "openapi", derive(ToSchema))]
2481pub struct VoiceSessionFailedData {
2482 #[cfg_attr(
2484 feature = "openapi",
2485 schema(example = "voice_01933b5a00007000800000000000001")
2486 )]
2487 pub voice_connection_id: String,
2488 #[cfg_attr(
2490 feature = "openapi",
2491 schema(example = "realtime provider closed stream: 1011 internal_error")
2492 )]
2493 pub error: String,
2494}
2495
2496#[derive(Debug, Clone, Serialize)]
2544#[serde(untagged)]
2545#[cfg_attr(feature = "openapi", derive(ToSchema))]
2546#[cfg_attr(feature = "openapi", schema(
2547 title = "EventData",
2548 description = "Event-specific payload. The schema depends on the event type field.",
2549 example = json!({"message": {"id": "...", "role": "user", "content": []}})
2550))]
2551pub enum EventData {
2552 InputMessage(InputMessageData),
2554
2555 OutputMessageDelta(OutputMessageDeltaData),
2557 OutputMessageStarted(OutputMessageStartedData),
2558 OutputMessageReplaced(OutputMessageReplacedData),
2559 OutputMessageCompleted(OutputMessageCompletedData),
2560
2561 TurnStarted(TurnStartedData),
2563 TurnCompleted(TurnCompletedData),
2564 TurnFailed(TurnFailedData),
2565
2566 ReasonStarted(ReasonStartedData),
2568 ReasonCompleted(ReasonCompletedData),
2569 ReasonRecovered(ReasonRecoveredData),
2570 CapabilityUsage(CapabilityUsageData),
2571 ActStarted(ActStartedData),
2572 ActCompleted(ActCompletedData),
2573 ToolStarted(ToolStartedData),
2574 ToolCompleted(ToolCompletedData),
2575 ToolProgress(ToolProgressData),
2576 ToolOutputDelta(ToolOutputDeltaData),
2577 ToolCallRequested(ToolCallRequestedData),
2578
2579 TranscriptRepaired(TranscriptRepairedData),
2581 ToolCallRepaired(ToolCallRepairedData),
2582
2583 LlmGeneration(LlmGenerationData),
2585
2586 ReasonThinkingDelta(ReasonThinkingDeltaData),
2588 ReasonItem(ReasonItemData),
2589 ReasonThinkingStarted(ReasonThinkingStartedData),
2590 ReasonThinkingCompleted(ReasonThinkingCompletedData),
2591
2592 TurnSealed(TurnSealedData),
2593 TurnCancelled(TurnCancelledData),
2594
2595 SessionStarted(SessionStartedData),
2597 SessionActivated(SessionActivatedData),
2598 SessionIdled(SessionIdledData),
2599 SessionTitleUpdated(SessionTitleUpdatedData),
2600
2601 TaskCreated(SessionTaskEventData),
2603 TaskUpdated(SessionTaskEventData),
2604 TaskMessageSent(TaskMessageEventData),
2605 TaskMessageReceived(TaskMessageEventData),
2606
2607 ContextCompacting(ContextCompactingData),
2609 ContextCompacted(ContextCompactedData),
2610
2611 FileWritten(FileWrittenData),
2613
2614 BudgetWarning(BudgetEventData),
2616 BudgetPaused(BudgetEventData),
2617 BudgetExhausted(BudgetEventData),
2618 BudgetResumed(BudgetEventData),
2619
2620 VoiceSessionStarted(VoiceSessionStartedData),
2622 VoiceInputTranscriptDelta(VoiceTranscriptData),
2623 VoiceInputTranscriptCompleted(VoiceTranscriptData),
2624 VoiceOutputTranscriptDelta(VoiceTranscriptData),
2625 VoiceOutputTranscriptCompleted(VoiceTranscriptData),
2626 VoiceSessionEnded(VoiceSessionEndedData),
2627 VoiceSessionFailed(VoiceSessionFailedData),
2628
2629 #[serde(skip)]
2633 Unsupported {
2634 event_type: String,
2636 data: serde_json::Value,
2638 },
2639}
2640
2641impl EventData {
2642 pub fn is_unsupported(&self) -> bool {
2645 matches!(self, EventData::Unsupported { .. })
2646 }
2647
2648 pub fn unsupported(event_type: String, data: serde_json::Value) -> Self {
2651 tracing::warn!(
2652 event_type = %event_type,
2653 "Encountered unsupported event type - will be filtered from API responses"
2654 );
2655 EventData::Unsupported { event_type, data }
2656 }
2657}
2658
2659macro_rules! event_data_kinds {
2673 ($( $variant:ident($data:ty) = $type_const:path ),+ $(,)?) => {
2674 impl EventData {
2675 pub fn event_type(&self) -> &'static str {
2678 match self {
2679 $( EventData::$variant(_) => $type_const, )+
2680 EventData::Unsupported { .. } => "unsupported",
2681 }
2682 }
2683 }
2684
2685 pub fn deserialize_event_data(event_type: &str, data: serde_json::Value) -> EventData {
2697 let result = match event_type {
2698 $(
2699 $type_const => serde_json::from_value::<$data>(data.clone())
2700 .map(EventData::$variant),
2701 )+
2702 _ => return EventData::unsupported(event_type.to_string(), data),
2703 };
2704
2705 result.unwrap_or_else(|e| {
2706 tracing::warn!(
2707 event_type = %event_type,
2708 error = %e,
2709 "Failed to deserialize known event type - treating as unsupported"
2710 );
2711 EventData::Unsupported {
2712 event_type: event_type.to_string(),
2713 data,
2714 }
2715 })
2716 }
2717 };
2718}
2719
2720event_data_kinds! {
2721 InputMessage(InputMessageData) = INPUT_MESSAGE,
2723
2724 OutputMessageStarted(OutputMessageStartedData) = OUTPUT_MESSAGE_STARTED,
2726 OutputMessageDelta(OutputMessageDeltaData) = OUTPUT_MESSAGE_DELTA,
2727 OutputMessageReplaced(OutputMessageReplacedData) = OUTPUT_MESSAGE_REPLACED,
2728 OutputMessageCompleted(OutputMessageCompletedData) = OUTPUT_MESSAGE_COMPLETED,
2729
2730 TurnStarted(TurnStartedData) = TURN_STARTED,
2732 TurnCompleted(TurnCompletedData) = TURN_COMPLETED,
2733 TurnFailed(TurnFailedData) = TURN_FAILED,
2734 TurnSealed(TurnSealedData) = TURN_SEALED,
2735 TurnCancelled(TurnCancelledData) = TURN_CANCELLED,
2736
2737 ReasonStarted(ReasonStartedData) = REASON_STARTED,
2739 ReasonCompleted(ReasonCompletedData) = REASON_COMPLETED,
2740 ReasonRecovered(ReasonRecoveredData) = REASON_RECOVERED,
2741 CapabilityUsage(CapabilityUsageData) = CAPABILITY_USAGE,
2742 ActStarted(ActStartedData) = ACT_STARTED,
2743 ActCompleted(ActCompletedData) = ACT_COMPLETED,
2744 ToolStarted(ToolStartedData) = TOOL_STARTED,
2745 ToolCompleted(ToolCompletedData) = TOOL_COMPLETED,
2746 ToolProgress(ToolProgressData) = TOOL_PROGRESS,
2747 ToolOutputDelta(ToolOutputDeltaData) = TOOL_OUTPUT_DELTA,
2748 ToolCallRequested(ToolCallRequestedData) = TOOL_CALL_REQUESTED,
2749
2750 TranscriptRepaired(TranscriptRepairedData) = TRANSCRIPT_REPAIRED,
2752 ToolCallRepaired(ToolCallRepairedData) = TOOL_CALL_REPAIRED,
2753
2754 LlmGeneration(LlmGenerationData) = LLM_GENERATION,
2756
2757 ReasonThinkingStarted(ReasonThinkingStartedData) = REASON_THINKING_STARTED,
2759 ReasonThinkingDelta(ReasonThinkingDeltaData) = REASON_THINKING_DELTA,
2760 ReasonThinkingCompleted(ReasonThinkingCompletedData) = REASON_THINKING_COMPLETED,
2761 ReasonItem(ReasonItemData) = REASON_ITEM,
2762
2763 SessionStarted(SessionStartedData) = SESSION_STARTED,
2765 SessionActivated(SessionActivatedData) = SESSION_ACTIVATED,
2766 SessionIdled(SessionIdledData) = SESSION_IDLED,
2767 SessionTitleUpdated(SessionTitleUpdatedData) = SESSION_TITLE_UPDATED,
2768
2769 ContextCompacting(ContextCompactingData) = CONTEXT_COMPACTING,
2771 ContextCompacted(ContextCompactedData) = CONTEXT_COMPACTED,
2772
2773 FileWritten(FileWrittenData) = FILE_WRITTEN,
2775
2776 BudgetWarning(BudgetEventData) = BUDGET_WARNING,
2778 BudgetPaused(BudgetEventData) = BUDGET_PAUSED,
2779 BudgetExhausted(BudgetEventData) = BUDGET_EXHAUSTED,
2780 BudgetResumed(BudgetEventData) = BUDGET_RESUMED,
2781
2782 VoiceSessionStarted(VoiceSessionStartedData) = VOICE_SESSION_STARTED,
2784 VoiceInputTranscriptDelta(VoiceTranscriptData) = VOICE_INPUT_TRANSCRIPT_DELTA,
2785 VoiceInputTranscriptCompleted(VoiceTranscriptData) = VOICE_INPUT_TRANSCRIPT_COMPLETED,
2786 VoiceOutputTranscriptDelta(VoiceTranscriptData) = VOICE_OUTPUT_TRANSCRIPT_DELTA,
2787 VoiceOutputTranscriptCompleted(VoiceTranscriptData) = VOICE_OUTPUT_TRANSCRIPT_COMPLETED,
2788 VoiceSessionEnded(VoiceSessionEndedData) = VOICE_SESSION_ENDED,
2789 VoiceSessionFailed(VoiceSessionFailedData) = VOICE_SESSION_FAILED,
2790
2791 TaskCreated(SessionTaskEventData) = TASK_CREATED,
2793 TaskUpdated(SessionTaskEventData) = TASK_UPDATED,
2794 TaskMessageSent(TaskMessageEventData) = TASK_MESSAGE_SENT,
2795 TaskMessageReceived(TaskMessageEventData) = TASK_MESSAGE_RECEIVED,
2796}
2797
2798macro_rules! impl_from_event_data {
2802 ($($data_type:ty => $variant:ident),* $(,)?) => {
2803 $(
2804 impl From<$data_type> for EventData {
2805 fn from(data: $data_type) -> Self {
2806 EventData::$variant(data)
2807 }
2808 }
2809 )*
2810 };
2811}
2812
2813impl_from_event_data! {
2815 InputMessageData => InputMessage,
2816 OutputMessageStartedData => OutputMessageStarted,
2817 OutputMessageDeltaData => OutputMessageDelta,
2818 OutputMessageReplacedData => OutputMessageReplaced,
2819 OutputMessageCompletedData => OutputMessageCompleted,
2820 TurnStartedData => TurnStarted,
2821 TurnCompletedData => TurnCompleted,
2822 TurnFailedData => TurnFailed,
2823 TurnSealedData => TurnSealed,
2824 TurnCancelledData => TurnCancelled,
2825 ReasonStartedData => ReasonStarted,
2826 ReasonCompletedData => ReasonCompleted,
2827 ReasonRecoveredData => ReasonRecovered,
2828 CapabilityUsageData => CapabilityUsage,
2829 ActStartedData => ActStarted,
2830 ActCompletedData => ActCompleted,
2831 ToolStartedData => ToolStarted,
2832 ToolCompletedData => ToolCompleted,
2833 ToolProgressData => ToolProgress,
2834 ToolOutputDeltaData => ToolOutputDelta,
2835 ToolCallRequestedData => ToolCallRequested,
2836 TranscriptRepairedData => TranscriptRepaired,
2837 ToolCallRepairedData => ToolCallRepaired,
2838 LlmGenerationData => LlmGeneration,
2839 ReasonThinkingStartedData => ReasonThinkingStarted,
2840 ReasonThinkingDeltaData => ReasonThinkingDelta,
2841 ReasonThinkingCompletedData => ReasonThinkingCompleted,
2842 ReasonItemData => ReasonItem,
2843 SessionStartedData => SessionStarted,
2844 SessionActivatedData => SessionActivated,
2845 SessionIdledData => SessionIdled,
2846 SessionTitleUpdatedData => SessionTitleUpdated,
2847 ContextCompactingData => ContextCompacting,
2848 ContextCompactedData => ContextCompacted,
2849 FileWrittenData => FileWritten,
2850 VoiceSessionStartedData => VoiceSessionStarted,
2851 VoiceSessionEndedData => VoiceSessionEnded,
2852 VoiceSessionFailedData => VoiceSessionFailed,
2853}
2854
2855impl EventData {
2856 pub fn voice_transcript_event(data: VoiceTranscriptData, event_type: &str) -> Self {
2857 match event_type {
2858 VOICE_INPUT_TRANSCRIPT_DELTA => EventData::VoiceInputTranscriptDelta(data),
2859 VOICE_INPUT_TRANSCRIPT_COMPLETED => EventData::VoiceInputTranscriptCompleted(data),
2860 VOICE_OUTPUT_TRANSCRIPT_DELTA => EventData::VoiceOutputTranscriptDelta(data),
2861 VOICE_OUTPUT_TRANSCRIPT_COMPLETED => EventData::VoiceOutputTranscriptCompleted(data),
2862 _ => EventData::unsupported(
2863 event_type.to_string(),
2864 serde_json::to_value(&data).unwrap_or(serde_json::Value::Null),
2865 ),
2866 }
2867 }
2868}
2869
2870impl EventData {
2873 pub fn budget_event(data: BudgetEventData, event_type: &str) -> Self {
2874 match event_type {
2875 BUDGET_WARNING => EventData::BudgetWarning(data),
2876 BUDGET_PAUSED => EventData::BudgetPaused(data),
2877 BUDGET_EXHAUSTED => EventData::BudgetExhausted(data),
2878 BUDGET_RESUMED => EventData::BudgetResumed(data),
2879 _ => EventData::unsupported(
2880 event_type.to_string(),
2881 serde_json::to_value(&data).unwrap_or(serde_json::Value::Null),
2882 ),
2883 }
2884 }
2885}
2886
2887#[derive(Debug, Clone, Serialize)]
2897#[cfg_attr(feature = "openapi", derive(ToSchema))]
2898pub struct EventRequest {
2899 #[serde(rename = "type")]
2901 pub event_type: String,
2902
2903 pub ts: DateTime<Utc>,
2905
2906 pub session_id: SessionId,
2908
2909 pub context: EventContext,
2911
2912 pub data: EventData,
2914
2915 #[serde(skip_serializing_if = "Option::is_none")]
2917 pub metadata: Option<serde_json::Value>,
2918
2919 #[serde(skip_serializing_if = "Option::is_none")]
2921 pub tags: Option<Vec<String>>,
2922}
2923
2924#[derive(Debug, Deserialize)]
2925struct RawEventRequest {
2926 #[serde(rename = "type")]
2927 event_type: String,
2928 ts: DateTime<Utc>,
2929 session_id: SessionId,
2930 context: EventContext,
2931 data: serde_json::Value,
2932 metadata: Option<serde_json::Value>,
2933 tags: Option<Vec<String>>,
2934}
2935
2936impl<'de> Deserialize<'de> for EventRequest {
2937 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2938 where
2939 D: Deserializer<'de>,
2940 {
2941 let raw = RawEventRequest::deserialize(deserializer)?;
2942 let data = deserialize_event_data(&raw.event_type, raw.data);
2943 Ok(Self {
2944 event_type: raw.event_type,
2945 ts: raw.ts,
2946 session_id: raw.session_id,
2947 context: raw.context,
2948 data,
2949 metadata: raw.metadata,
2950 tags: raw.tags,
2951 })
2952 }
2953}
2954
2955impl EventRequest {
2956 pub fn new(session_id: SessionId, context: EventContext, data: impl Into<EventData>) -> Self {
2960 let data = data.into();
2961 let event_type = data.event_type().to_string();
2962 Self {
2963 event_type,
2964 ts: Utc::now(),
2965 session_id,
2966 context,
2967 data,
2968 metadata: None,
2969 tags: None,
2970 }
2971 }
2972
2973 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
2975 self.metadata = Some(metadata);
2976 self
2977 }
2978
2979 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
2981 self.tags = Some(tags);
2982 self
2983 }
2984
2985 pub fn is_ephemeral(&self) -> bool {
2993 is_ephemeral_event_type(&self.event_type)
2994 }
2995
2996 pub fn into_event(self, id: EventId, sequence: i32) -> Event {
2998 Event {
2999 id,
3000 event_type: self.event_type,
3001 ts: self.ts,
3002 session_id: self.session_id,
3003 context: self.context,
3004 data: self.data,
3005 metadata: self.metadata,
3006 tags: self.tags,
3007 sequence: Some(sequence),
3008 }
3009 }
3010}
3011
3012pub struct EventBuilder {
3018 session_id: SessionId,
3019 context: EventContext,
3020}
3021
3022impl EventBuilder {
3023 pub fn new(session_id: SessionId) -> Self {
3024 Self {
3025 session_id,
3026 context: EventContext::empty(),
3027 }
3028 }
3029
3030 pub fn with_turn(mut self, turn_id: TurnId, input_message_id: MessageId) -> Self {
3031 self.context.turn_id = Some(turn_id);
3032 self.context.input_message_id = Some(input_message_id);
3033 self
3034 }
3035
3036 pub fn with_exec(mut self, exec_id: ExecId) -> Self {
3037 self.context.exec_id = Some(exec_id);
3038 self
3039 }
3040
3041 pub fn build(self, data: impl Into<EventData>) -> Event {
3042 Event::new(self.session_id, self.context, data)
3043 }
3044}
3045
3046#[cfg(test)]
3051mod tests {
3052 use super::*;
3053 use crate::driver_registry::PromptCacheStrategy;
3054 use serde_json::json;
3055 use std::collections::HashMap;
3056
3057 #[test]
3058 fn test_event_creation() {
3059 let session_id = SessionId::new();
3060 let context = EventContext::empty();
3061 let data = InputMessageData::new(Message::user("test"));
3062
3063 let event = Event::new(session_id, context, data);
3064
3065 assert_eq!(event.event_type, "input.message");
3066 assert_eq!(event.session_uuid(), session_id.uuid());
3067 assert!(event.is_input_event());
3068 assert!(event.is_message_event());
3069 }
3070
3071 #[test]
3072 fn test_event_context_from_atom_context() {
3073 let session_id = SessionId::new();
3074 let turn_id = TurnId::new();
3075 let input_message_id = MessageId::new();
3076
3077 let atom_ctx = AtomContext::new(session_id, turn_id, input_message_id);
3078 let context = EventContext::from_atom_context(&atom_ctx);
3079
3080 assert_eq!(context.turn_id, Some(turn_id));
3081 assert_eq!(context.input_message_id, Some(input_message_id));
3082 assert_eq!(context.exec_id, Some(atom_ctx.exec_id));
3083 }
3084
3085 #[test]
3086 fn test_event_serialization() {
3087 let session_id = SessionId::new();
3088 let context = EventContext::empty();
3089 let event = Event::new(
3090 session_id,
3091 context,
3092 InputMessageData::new(Message::user("test")),
3093 );
3094
3095 let json = serde_json::to_string(&event).unwrap();
3096
3097 assert!(json.contains("\"type\":\"input.message\""));
3098 assert!(json.contains("\"session_id\""));
3099 assert!(json.contains("\"context\""));
3100 assert!(json.contains("\"data\""));
3101 }
3102
3103 #[test]
3104 fn transcript_repaired_is_valid_filter_event_type() {
3105 assert!(VALID_EVENT_TYPES.contains(&TRANSCRIPT_REPAIRED));
3106 }
3107
3108 #[test]
3112 fn capability_usage_is_valid_filter_event_type() {
3113 assert!(VALID_EVENT_TYPES.contains(&CAPABILITY_USAGE));
3114 }
3115
3116 #[test]
3117 fn test_event_builder() {
3118 let session_id = SessionId::new();
3119 let turn_id = TurnId::new();
3120 let input_message_id = MessageId::new();
3121 let exec_id = ExecId::new();
3122
3123 let event = EventBuilder::new(session_id)
3124 .with_turn(turn_id, input_message_id)
3125 .with_exec(exec_id)
3126 .build(ReasonStartedData {
3127 harness_id: HarnessId::from_seed(1),
3128 agent_id: Some(AgentId::new()),
3129 metadata: Some(ModelMetadata {
3130 model: "gpt-4o".to_string(),
3131 model_id: None,
3132 provider_id: None,
3133 }),
3134 });
3135
3136 assert_eq!(event.event_type, "reason.started");
3137 assert_eq!(event.session_id, session_id);
3138 assert_eq!(event.context.turn_id, Some(turn_id));
3139 assert_eq!(event.context.exec_id, Some(exec_id));
3140 }
3141
3142 #[test]
3143 fn test_reason_completed_data() {
3144 let data = ReasonCompletedData::success("Hello world", true, 2, Some(1000), None);
3145 assert!(data.success);
3146 assert_eq!(data.text_preview, Some("Hello world".to_string()));
3147 assert!(data.has_tool_calls);
3148 assert_eq!(data.tool_call_count, 2);
3149 assert_eq!(data.duration_ms, Some(1000));
3150 assert!(data.usage.is_none());
3151
3152 let data = ReasonCompletedData::failure("Network error".to_string(), Some(500));
3153 assert!(!data.success);
3154 assert_eq!(data.error, Some("Network error".to_string()));
3155 assert_eq!(data.duration_ms, Some(500));
3156 }
3157
3158 #[test]
3159 fn test_input_output_event_types() {
3160 assert_eq!(INPUT_MESSAGE, "input.message");
3161 assert_eq!(OUTPUT_MESSAGE_STARTED, "output.message.started");
3162 assert_eq!(OUTPUT_MESSAGE_DELTA, "output.message.delta");
3163 assert_eq!(OUTPUT_MESSAGE_COMPLETED, "output.message.completed");
3164 }
3165
3166 #[test]
3167 fn test_turn_event_types() {
3168 assert_eq!(TURN_STARTED, "turn.started");
3169 assert_eq!(TURN_COMPLETED, "turn.completed");
3170 assert_eq!(TURN_FAILED, "turn.failed");
3171 assert_eq!(TURN_CANCELLED, "turn.cancelled");
3172 }
3173
3174 #[test]
3175 fn test_turn_cancelled_data() {
3176 let data = TurnCancelledData {
3177 turn_id: TurnId::from_uuid(Uuid::now_v7()),
3178 reason: Some("User requested cancellation".to_string()),
3179 usage: Some(TokenUsage::new(100, 50)),
3180 };
3181
3182 let event_data: EventData = data.into();
3183 assert_eq!(event_data.event_type(), TURN_CANCELLED);
3184 }
3185
3186 #[test]
3187 fn test_tool_event_types() {
3188 assert_eq!(TOOL_STARTED, "tool.started");
3189 assert_eq!(TOOL_COMPLETED, "tool.completed");
3190 }
3191
3192 #[test]
3193 fn test_llm_generation_event_type() {
3194 assert_eq!(LLM_GENERATION, "llm.generation");
3195 }
3196
3197 #[test]
3198 fn test_llm_generation_data_success() {
3199 let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")];
3200 let tools = vec![ToolDefinitionSummary {
3201 name: "get_weather".to_string(),
3202 display_name: None,
3203 category: None,
3204 capability_id: None,
3205 capability_name: None,
3206 description: "Get weather for a city".to_string(),
3207 }];
3208 let tool_calls = vec![];
3209 let data = LlmGenerationData::success(
3210 messages.clone(),
3211 tools,
3212 Some("Hi there!".to_string()),
3213 tool_calls,
3214 "gpt-4o".to_string(),
3215 Some("openai".to_string()),
3216 Some(TokenUsage {
3217 input_tokens: 10,
3218 output_tokens: 5,
3219 cache_read_tokens: None,
3220 cache_creation_tokens: None,
3221 actual_cost_usd: None,
3222 estimated_cost_usd: None,
3223 effective_cost_usd: None,
3224 }),
3225 Some(100),
3226 Some(25), );
3228
3229 assert_eq!(data.messages.len(), 2);
3230 assert_eq!(data.tools.len(), 1);
3231 assert_eq!(data.tools[0].name, "get_weather");
3232 assert_eq!(data.output.text, Some("Hi there!".to_string()));
3233 assert!(data.output.tool_calls.is_empty());
3234 assert!(data.metadata.success);
3235 assert_eq!(data.metadata.model, "gpt-4o");
3236 assert_eq!(data.metadata.provider, Some("openai".to_string()));
3237 assert!(data.metadata.error.is_none());
3238 assert_eq!(data.metadata.finish_reasons, Some(vec!["stop".to_string()]));
3240 assert!(data.metadata.response_id.is_none());
3241 }
3242
3243 #[test]
3244 fn test_llm_generation_data_with_full_metadata() {
3245 let messages = vec![Message::user("Hello")];
3246 let data = LlmGenerationData::success_with_metadata(
3247 messages,
3248 vec![],
3249 Some("Hi!".to_string()),
3250 vec![],
3251 "claude-3-opus".to_string(),
3252 Some("anthropic".to_string()),
3253 Some(TokenUsage {
3254 input_tokens: 5,
3255 output_tokens: 3,
3256 cache_read_tokens: None,
3257 cache_creation_tokens: None,
3258 actual_cost_usd: None,
3259 estimated_cost_usd: None,
3260 effective_cost_usd: None,
3261 }),
3262 Some(50),
3263 Some(25), Some(vec!["end_turn".to_string()]),
3265 Some("msg_12345".to_string()),
3266 );
3267
3268 assert!(data.metadata.success);
3269 assert_eq!(data.metadata.model, "claude-3-opus");
3270 assert_eq!(data.metadata.provider, Some("anthropic".to_string()));
3271 assert_eq!(data.metadata.time_to_first_token_ms, Some(25));
3272 assert_eq!(
3273 data.metadata.finish_reasons,
3274 Some(vec!["end_turn".to_string()])
3275 );
3276 assert_eq!(data.metadata.response_id, Some("msg_12345".to_string()));
3277 }
3278
3279 #[test]
3280 fn test_llm_generation_data_failure() {
3281 let messages = vec![Message::user("Hello")];
3282 let data = LlmGenerationData::failure(
3283 messages,
3284 vec![],
3285 "gpt-4o".to_string(),
3286 Some("openai".to_string()),
3287 "Rate limit exceeded".to_string(),
3288 Some(50),
3289 None, );
3291
3292 assert!(!data.metadata.success);
3293 assert_eq!(data.metadata.error, Some("Rate limit exceeded".to_string()));
3294 assert!(data.output.text.is_none());
3295 assert!(data.output.tool_calls.is_empty());
3296 }
3297
3298 #[test]
3299 fn test_llm_generation_event_data() {
3300 let data = LlmGenerationData::success(
3301 vec![Message::user("test")],
3302 vec![],
3303 Some("response".to_string()),
3304 vec![],
3305 "model".to_string(),
3306 None,
3307 None,
3308 None,
3309 None, );
3311
3312 let event_data: EventData = data.into();
3313 assert_eq!(event_data.event_type(), LLM_GENERATION);
3314 }
3315
3316 #[test]
3317 fn test_llm_generation_is_durable_not_ephemeral() {
3318 let session_id = SessionId::new();
3319 let data = LlmGenerationData::success(
3320 vec![Message::user("test")],
3321 vec![],
3322 Some("response".to_string()),
3323 vec![],
3324 "model".to_string(),
3325 None,
3326 None,
3327 None,
3328 None,
3329 );
3330
3331 let request = EventRequest::new(session_id, EventContext::empty(), data);
3332 assert!(!request.is_ephemeral());
3333 }
3334
3335 #[test]
3336 fn test_delta_events_are_ephemeral() {
3337 let session_id = SessionId::new();
3338 let turn_id = TurnId::new();
3339
3340 let output_delta = EventRequest::new(
3341 session_id,
3342 EventContext::empty(),
3343 OutputMessageDeltaData {
3344 turn_id,
3345 message_id: MessageId::new(),
3346 delta: "hel".to_string(),
3347 accumulated: "hel".to_string(),
3348 phase: None,
3349 },
3350 );
3351 assert!(output_delta.is_ephemeral());
3352
3353 let thinking_delta = EventRequest::new(
3354 session_id,
3355 EventContext::empty(),
3356 ReasonThinkingDeltaData {
3357 turn_id,
3358 delta: "step".to_string(),
3359 accumulated: "step".to_string(),
3360 },
3361 );
3362 assert!(thinking_delta.is_ephemeral());
3363
3364 let tool_delta = EventRequest::new(
3365 session_id,
3366 EventContext::empty(),
3367 ToolOutputDeltaData {
3368 tool_call_id: "call_123".to_string(),
3369 tool_name: "bash".to_string(),
3370 delta: "line".to_string(),
3371 stream: "stdout".to_string(),
3372 },
3373 );
3374 assert!(tool_delta.is_ephemeral());
3375 }
3376
3377 #[test]
3378 fn test_llm_generation_data_with_request_options() {
3379 let mut provider_options = HashMap::new();
3380 provider_options.insert(
3381 "openai".to_string(),
3382 json!({ "previous_response_id": true }),
3383 );
3384
3385 let data = LlmGenerationData::success(
3386 vec![Message::user("Hello")],
3387 vec![],
3388 Some("Hi".to_string()),
3389 vec![],
3390 "gpt-5.4".to_string(),
3391 Some("openai".to_string()),
3392 None,
3393 Some(42),
3394 Some(12),
3395 )
3396 .with_request_options(LlmRequestOptions {
3397 prompt_cache: Some(LlmPromptCacheInfo {
3398 enabled: true,
3399 strategy: PromptCacheStrategy::Auto,
3400 provider_mode: Some("prompt_cache_key".to_string()),
3401 }),
3402 tool_search: Some(LlmToolSearchInfo {
3403 enabled: true,
3404 threshold: 8,
3405 }),
3406 provider_options,
3407 metadata: Default::default(),
3408 });
3409
3410 let json = serde_json::to_value(&data).unwrap();
3411 assert_eq!(
3412 json["metadata"]["request_options"]["prompt_cache"]["provider_mode"],
3413 "prompt_cache_key"
3414 );
3415 assert_eq!(
3416 json["metadata"]["request_options"]["tool_search"]["threshold"],
3417 8
3418 );
3419 assert_eq!(
3420 json["metadata"]["request_options"]["provider_options"]["openai"]["previous_response_id"],
3421 true
3422 );
3423 }
3424
3425 #[test]
3426 fn test_extended_thinking_event_types() {
3427 assert_eq!(REASON_THINKING_STARTED, "reason.thinking.started");
3428 assert_eq!(REASON_THINKING_DELTA, "reason.thinking.delta");
3429 assert_eq!(REASON_THINKING_COMPLETED, "reason.thinking.completed");
3430 }
3431
3432 #[test]
3433 fn test_output_message_started_data() {
3434 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3435 let data = OutputMessageStartedData {
3436 turn_id,
3437 message_id: MessageId::new(),
3438 model: Some("claude-4-opus".to_string()),
3439 iteration: None,
3440 phase: None,
3441 };
3442
3443 let event_data: EventData = data.into();
3444 assert_eq!(event_data.event_type(), OUTPUT_MESSAGE_STARTED);
3445
3446 let json = serde_json::to_string(&event_data).unwrap();
3448 assert!(json.contains("turn_id"));
3449 assert!(json.contains("claude-4-opus"));
3450 }
3451
3452 #[test]
3453 fn test_output_message_started_data_without_model() {
3454 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3455 let data = OutputMessageStartedData {
3456 turn_id,
3457 message_id: MessageId::new(),
3458 model: None,
3459 iteration: None,
3460 phase: None,
3461 };
3462
3463 let json = serde_json::to_string(&data).unwrap();
3465 assert!(!json.contains("model"));
3466 }
3467
3468 #[test]
3469 fn test_reason_thinking_started_data() {
3470 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3471 let data = ReasonThinkingStartedData {
3472 turn_id,
3473 model: Some("claude-4-opus".to_string()),
3474 };
3475
3476 let event_data: EventData = data.into();
3477 assert_eq!(event_data.event_type(), REASON_THINKING_STARTED);
3478
3479 let json = serde_json::to_string(&event_data).unwrap();
3481 assert!(json.contains("turn_id"));
3482 assert!(json.contains("claude-4-opus"));
3483 }
3484
3485 #[test]
3486 fn test_reason_thinking_delta_data() {
3487 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3488 let data = ReasonThinkingDeltaData {
3489 turn_id,
3490 delta: "thinking step 1".to_string(),
3491 accumulated: "thinking step 1".to_string(),
3492 };
3493
3494 let event_data: EventData = data.into();
3495 assert_eq!(event_data.event_type(), REASON_THINKING_DELTA);
3496
3497 let json = serde_json::to_string(&event_data).unwrap();
3499 assert!(json.contains("turn_id"));
3500 assert!(json.contains("delta"));
3501 assert!(json.contains("accumulated"));
3502 }
3503
3504 #[test]
3505 fn test_reason_thinking_completed_data() {
3506 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3507 let data = ReasonThinkingCompletedData {
3508 turn_id,
3509 thinking: "Full thinking content here".to_string(),
3510 };
3511
3512 let event_data: EventData = data.into();
3513 assert_eq!(event_data.event_type(), REASON_THINKING_COMPLETED);
3514
3515 let json = serde_json::to_string(&event_data).unwrap();
3517 assert!(json.contains("turn_id"));
3518 assert!(json.contains("thinking"));
3519 }
3520
3521 #[test]
3522 fn test_output_message_delta_data() {
3523 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3524 let data = OutputMessageDeltaData {
3525 turn_id,
3526 message_id: MessageId::new(),
3527 delta: "Hello".to_string(),
3528 accumulated: "Hello".to_string(),
3529 phase: None,
3530 };
3531
3532 let event_data: EventData = data.into();
3533 assert_eq!(event_data.event_type(), OUTPUT_MESSAGE_DELTA);
3534
3535 let json = serde_json::to_string(&event_data).unwrap();
3537 assert!(json.contains("turn_id"));
3538 assert!(json.contains("delta"));
3539 assert!(json.contains("accumulated"));
3540 }
3541
3542 #[test]
3543 fn test_output_message_lifecycle_shares_message_id() {
3544 let turn_id = TurnId::new();
3545 let message_id = MessageId::new();
3546 let next_message_id = MessageId::new();
3547
3548 let started = OutputMessageStartedData {
3549 turn_id,
3550 message_id,
3551 model: None,
3552 iteration: Some(1),
3553 phase: None,
3554 };
3555 let next_started = OutputMessageStartedData {
3556 turn_id,
3557 message_id: next_message_id,
3558 model: None,
3559 iteration: Some(2),
3560 phase: None,
3561 };
3562 let delta = OutputMessageDeltaData {
3563 turn_id,
3564 message_id,
3565 delta: "Hello".to_string(),
3566 accumulated: "Hello".to_string(),
3567 phase: None,
3568 };
3569 let replaced = OutputMessageReplacedData {
3570 turn_id,
3571 message_id,
3572 guardrail_capability_id: "guardrails".to_string(),
3573 guardrail_id: "example".to_string(),
3574 reason_code: "blocked".to_string(),
3575 replacement: "Safe response".to_string(),
3576 };
3577 let completed = OutputMessageCompletedData::new(
3578 Message::assistant("Safe response").with_id(message_id),
3579 );
3580
3581 assert_eq!(started.message_id, message_id);
3582 assert_eq!(delta.message_id, message_id);
3583 assert_eq!(replaced.message_id, message_id);
3584 assert_eq!(completed.message.id, message_id);
3585 assert_ne!(started.message_id, next_started.message_id);
3586
3587 for value in [
3588 serde_json::to_value(started).unwrap(),
3589 serde_json::to_value(delta).unwrap(),
3590 serde_json::to_value(replaced).unwrap(),
3591 ] {
3592 assert_eq!(value["message_id"], message_id.to_string());
3593 }
3594 }
3595
3596 #[test]
3597 fn test_output_message_phase_hint_serde() {
3598 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3602
3603 let started_none = OutputMessageStartedData {
3604 turn_id,
3605 message_id: MessageId::new(),
3606 model: None,
3607 iteration: None,
3608 phase: None,
3609 };
3610 assert!(
3611 !serde_json::to_string(&started_none)
3612 .unwrap()
3613 .contains("phase")
3614 );
3615
3616 let delta_commentary = OutputMessageDeltaData {
3617 turn_id,
3618 message_id: MessageId::new(),
3619 delta: "one moment".to_string(),
3620 accumulated: "one moment".to_string(),
3621 phase: Some(crate::message::ExecutionPhase::Commentary),
3622 };
3623 let json = serde_json::to_value(&delta_commentary).unwrap();
3624 assert_eq!(json["phase"], "commentary");
3625
3626 let back: OutputMessageDeltaData = serde_json::from_value(json).unwrap();
3628 assert_eq!(back.phase, Some(crate::message::ExecutionPhase::Commentary));
3629
3630 let delta_final = OutputMessageDeltaData {
3631 turn_id,
3632 message_id: MessageId::new(),
3633 delta: "done".to_string(),
3634 accumulated: "done".to_string(),
3635 phase: Some(crate::message::ExecutionPhase::FinalAnswer),
3636 };
3637 assert_eq!(
3638 serde_json::to_value(&delta_final).unwrap()["phase"],
3639 "final_answer"
3640 );
3641 }
3642
3643 #[test]
3644 fn test_output_message_delta_deserialization_preserves_fields() {
3645 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3648 let data = OutputMessageDeltaData {
3649 turn_id,
3650 message_id: MessageId::new(),
3651 delta: "Hello world".to_string(),
3652 accumulated: "Hello world".to_string(),
3653 phase: None,
3654 };
3655
3656 let json = serde_json::to_value(EventData::OutputMessageDelta(data.clone())).unwrap();
3658
3659 let deserialized = deserialize_event_data(OUTPUT_MESSAGE_DELTA, json);
3661
3662 match deserialized {
3664 EventData::OutputMessageDelta(td) => {
3665 assert_eq!(td.turn_id, turn_id);
3666 assert_eq!(td.delta, "Hello world");
3667 assert_eq!(td.accumulated, "Hello world");
3668 }
3669 _ => panic!("Expected OutputMessageDelta, got different variant"),
3670 }
3671 }
3672
3673 #[test]
3674 fn test_output_message_started_deserialization() {
3675 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3676 let data = OutputMessageStartedData {
3677 turn_id,
3678 message_id: MessageId::new(),
3679 model: Some("claude-3".to_string()),
3680 iteration: None,
3681 phase: None,
3682 };
3683
3684 let json = serde_json::to_value(EventData::OutputMessageStarted(data.clone())).unwrap();
3686
3687 let deserialized = deserialize_event_data(OUTPUT_MESSAGE_STARTED, json);
3689
3690 match deserialized {
3692 EventData::OutputMessageStarted(at) => {
3693 assert_eq!(at.turn_id, turn_id);
3694 assert_eq!(at.model, Some("claude-3".to_string()));
3695 }
3696 _ => panic!("Expected OutputMessageStarted, got different variant"),
3697 }
3698 }
3699
3700 #[test]
3701 fn test_reason_thinking_started_deserialization() {
3702 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3707 let data = ReasonThinkingStartedData {
3708 turn_id,
3709 model: Some("claude-3".to_string()),
3710 };
3711
3712 let json = serde_json::to_value(&data).unwrap();
3714
3715 let deserialized = deserialize_event_data(REASON_THINKING_STARTED, json);
3717
3718 match deserialized {
3720 EventData::ReasonThinkingStarted(at) => {
3721 assert_eq!(at.turn_id, turn_id);
3722 assert_eq!(at.model, Some("claude-3".to_string()));
3723 }
3724 other => panic!("Expected ReasonThinkingStarted, got {}", other.event_type()),
3725 }
3726 }
3727
3728 #[test]
3729 fn test_llm_generation_with_ttft() {
3730 let messages = vec![Message::user("Hello")];
3731 let data = LlmGenerationData::success_with_metadata(
3732 messages,
3733 vec![],
3734 Some("Hi!".to_string()),
3735 vec![],
3736 "gpt-4o".to_string(),
3737 Some("openai".to_string()),
3738 Some(TokenUsage {
3739 input_tokens: 10,
3740 output_tokens: 5,
3741 cache_read_tokens: None,
3742 cache_creation_tokens: None,
3743 actual_cost_usd: None,
3744 estimated_cost_usd: None,
3745 effective_cost_usd: None,
3746 }),
3747 Some(500), Some(120), Some(vec!["stop".to_string()]),
3750 None,
3751 );
3752
3753 assert!(data.metadata.success);
3754 assert_eq!(data.metadata.duration_ms, Some(500));
3755 assert_eq!(data.metadata.time_to_first_token_ms, Some(120));
3756 }
3757
3758 #[test]
3759 fn test_llm_generation_ttft_serialization() {
3760 let messages = vec![Message::user("test")];
3761 let data = LlmGenerationData::success_with_metadata(
3762 messages,
3763 vec![],
3764 Some("response".to_string()),
3765 vec![],
3766 "model".to_string(),
3767 None,
3768 None,
3769 Some(1000),
3770 Some(150), None,
3772 None,
3773 );
3774
3775 let json = serde_json::to_string(&data).unwrap();
3776 assert!(json.contains("time_to_first_token_ms"));
3777 assert!(json.contains("150"));
3778 }
3779
3780 #[test]
3781 fn test_reason_item_data_event_type_and_serialization() {
3782 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3783 let data = ReasonItemData {
3784 turn_id,
3785 provider: "openai".to_string(),
3786 model: Some("gpt-5.5".to_string()),
3787 item_id: "rs_abc".to_string(),
3788 encrypted_content: Some("OPAQUE_BLOB".to_string()),
3789 summary: vec!["safe summary".to_string()],
3790 token_count: Some(123),
3791 };
3792
3793 let event_data: EventData = data.into();
3794 assert_eq!(event_data.event_type(), REASON_ITEM);
3795
3796 let json = serde_json::to_string(&event_data).unwrap();
3797 assert!(json.contains("turn_id"));
3798 assert!(json.contains("openai"));
3799 assert!(json.contains("rs_abc"));
3800 assert!(json.contains("OPAQUE_BLOB"));
3801 assert!(json.contains("safe summary"));
3802 }
3803
3804 #[test]
3805 fn test_event_deserialize_reason_item_uses_event_type_dispatch() {
3806 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3807 let payload = serde_json::json!({
3808 "id": EventId::new().to_string(),
3809 "type": REASON_ITEM,
3810 "ts": Utc::now().to_rfc3339(),
3811 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
3812 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
3813 "data": {
3814 "turn_id": turn_id.to_string(),
3815 "provider": "openai",
3816 "model": "gpt-5",
3817 "item_id": "rs_event",
3818 "encrypted_content": "ENC",
3819 "summary": ["safe"],
3820 "token_count": 9
3821 }
3822 });
3823
3824 let event: Event = serde_json::from_value(payload).expect("event deserializes");
3825 match event.data {
3826 EventData::ReasonItem(data) => {
3827 assert_eq!(data.turn_id, turn_id);
3828 assert_eq!(data.provider, "openai");
3829 assert_eq!(data.item_id, "rs_event");
3830 assert_eq!(data.token_count, Some(9));
3831 }
3832 other => panic!("expected reason.item data, got {}", other.event_type()),
3833 }
3834 }
3835
3836 #[test]
3837 fn test_event_request_deserialize_reason_item_uses_event_type_dispatch() {
3838 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3839 let payload = serde_json::json!({
3840 "type": REASON_ITEM,
3841 "ts": Utc::now().to_rfc3339(),
3842 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
3843 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
3844 "data": {
3845 "turn_id": turn_id.to_string(),
3846 "provider": "openai",
3847 "item_id": "rs_request",
3848 "encrypted_content": "ENC",
3849 "summary": ["safe"]
3850 }
3851 });
3852
3853 let req: EventRequest = serde_json::from_value(payload).expect("request deserializes");
3854 match req.data {
3855 EventData::ReasonItem(data) => {
3856 assert_eq!(data.turn_id, turn_id);
3857 assert_eq!(data.provider, "openai");
3858 assert_eq!(data.item_id, "rs_request");
3859 }
3860 other => panic!("expected reason.item data, got {}", other.event_type()),
3861 }
3862 }
3863
3864 #[test]
3865 fn test_reason_item_data_round_trip_uses_typed_dispatch() {
3866 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3870 let data = ReasonItemData {
3871 turn_id,
3872 provider: "openai".to_string(),
3873 model: Some("gpt-5".to_string()),
3874 item_id: "rs_xyz".to_string(),
3875 encrypted_content: Some("ENC".to_string()),
3876 summary: vec![],
3877 token_count: None,
3878 };
3879
3880 let json = serde_json::to_value(&data).unwrap();
3881 let deserialized = deserialize_event_data(REASON_ITEM, json);
3882
3883 match deserialized {
3884 EventData::ReasonItem(out) => {
3885 assert_eq!(out.turn_id, turn_id);
3886 assert_eq!(out.provider, "openai");
3887 assert_eq!(out.item_id, "rs_xyz");
3888 assert_eq!(out.encrypted_content.as_deref(), Some("ENC"));
3889 }
3890 other => panic!("Expected ReasonItem, got {}", other.event_type()),
3891 }
3892 }
3893
3894 #[test]
3903 fn test_reason_item_resolves_via_type_dispatch_despite_overlap() {
3904 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3909 let json = serde_json::json!({
3910 "turn_id": turn_id.to_string(),
3911 "provider": "openai",
3912 "model": "gpt-5",
3913 "item_id": "rs_keep",
3914 "encrypted_content": "ENC",
3915 "summary": ["s"],
3916 "token_count": 7,
3917 });
3918
3919 let as_thinking: ReasonThinkingStartedData =
3924 serde_json::from_value(json.clone()).expect("thinking ignores extra fields");
3925 assert_eq!(as_thinking.turn_id, turn_id);
3926 assert_eq!(as_thinking.model.as_deref(), Some("gpt-5"));
3927
3928 let as_item: ReasonItemData =
3929 serde_json::from_value(json.clone()).expect("ReasonItem accepts payload");
3930 assert_eq!(as_item.item_id, "rs_keep");
3931 assert_eq!(as_item.provider, "openai");
3932
3933 let event_data = deserialize_event_data(REASON_ITEM, json);
3936 match event_data {
3937 EventData::ReasonItem(out) => {
3938 assert_eq!(out.item_id, "rs_keep");
3939 assert_eq!(out.provider, "openai");
3940 }
3941 other => panic!(
3942 "Typed dispatcher must select ReasonItem for {REASON_ITEM}, got {}",
3943 other.event_type()
3944 ),
3945 }
3946 }
3947
3948 #[test]
3955 fn test_reason_item_data_excludes_plaintext_reasoning() {
3956 let turn_id = TurnId::from_uuid(Uuid::now_v7());
3957 let data = ReasonItemData {
3958 turn_id,
3959 provider: "openai".to_string(),
3960 model: Some("gpt-5".to_string()),
3961 item_id: "rs_secret".to_string(),
3962 encrypted_content: Some("opaque_blob_thinking_content_reasoning_text".to_string()),
3966 summary: vec!["safe summary mentioning content and thinking".to_string()],
3967 token_count: Some(1),
3968 };
3969
3970 let value = serde_json::to_value(&data).expect("serializable");
3971 let object = value.as_object().expect("data serializes to JSON object");
3972 for forbidden in [
3973 "content",
3974 "reasoning_text",
3975 "thinking",
3976 "reasoning_content",
3977 "raw_reasoning",
3978 ] {
3979 assert!(
3980 !object.contains_key(forbidden),
3981 "ReasonItemData JSON must not expose `{forbidden}` key, got: {object:?}",
3982 );
3983 }
3984 assert!(object.contains_key("encrypted_content"));
3986 assert!(object.contains_key("summary"));
3987 }
3988
3989 #[test]
3990 fn test_llm_generation_ttft_omitted_when_none() {
3991 let messages = vec![Message::user("test")];
3992 let data = LlmGenerationData::success(
3993 messages,
3994 vec![],
3995 Some("response".to_string()),
3996 vec![],
3997 "model".to_string(),
3998 None,
3999 None,
4000 None,
4001 None, );
4003
4004 assert!(data.metadata.time_to_first_token_ms.is_none());
4006
4007 let json = serde_json::to_string(&data).unwrap();
4009 assert!(!json.contains("time_to_first_token_ms"));
4010 }
4011}
4012
4013#[cfg(test)]
4022mod contract_tests {
4023 use super::*;
4024 use insta::{assert_json_snapshot, with_settings};
4025
4026 fn test_session_id() -> SessionId {
4028 SessionId::from_uuid(uuid::Uuid::from_u128(
4029 0x0000_0000_0000_0000_0000_0000_0000_0001,
4030 ))
4031 }
4032
4033 fn test_turn_id() -> TurnId {
4034 TurnId::from_uuid(uuid::Uuid::from_u128(
4035 0x0000_0000_0000_0000_0000_0000_0000_0002,
4036 ))
4037 }
4038
4039 fn test_message_id() -> MessageId {
4040 MessageId::from_uuid(uuid::Uuid::from_u128(
4041 0x0000_0000_0000_0000_0000_0000_0000_0003,
4042 ))
4043 }
4044
4045 fn test_agent_id() -> AgentId {
4046 AgentId::from_uuid(uuid::Uuid::from_u128(
4047 0x0000_0000_0000_0000_0000_0000_0000_0004,
4048 ))
4049 }
4050
4051 fn test_harness_id() -> HarnessId {
4052 HarnessId::from_uuid(uuid::Uuid::from_u128(
4053 0x0000_0000_0000_0000_0000_0000_0000_0005,
4054 ))
4055 }
4056
4057 #[test]
4064 fn snapshot_input_message() {
4065 let data = InputMessageData::new(Message::user("Hello, world!"));
4066 with_settings!({
4067 sort_maps => true,
4068 }, {
4069 assert_json_snapshot!("event_data_input_message", data, {
4071 ".message.id" => "[MESSAGE_ID]",
4072 ".message.created_at" => "[TIMESTAMP]"
4073 });
4074 });
4075 }
4076
4077 #[test]
4078 fn snapshot_output_message_started() {
4079 let data = OutputMessageStartedData {
4080 turn_id: test_turn_id(),
4081 message_id: test_message_id(),
4082 model: Some("gpt-4o".to_string()),
4083 iteration: None,
4084 phase: None,
4085 };
4086 with_settings!({
4087 sort_maps => true,
4088 }, {
4089 assert_json_snapshot!("event_data_output_message_started", data);
4090 });
4091 }
4092
4093 #[test]
4094 fn snapshot_output_message_delta() {
4095 let data = OutputMessageDeltaData {
4096 turn_id: test_turn_id(),
4097 message_id: test_message_id(),
4098 delta: "Hello".to_string(),
4099 accumulated: "Hello".to_string(),
4100 phase: None,
4101 };
4102 with_settings!({
4103 sort_maps => true,
4104 }, {
4105 assert_json_snapshot!("event_data_output_message_delta", data);
4106 });
4107 }
4108
4109 #[test]
4110 fn snapshot_output_message_completed() {
4111 let data = OutputMessageCompletedData::new(Message::assistant("Hello!"));
4112 with_settings!({
4113 sort_maps => true,
4114 }, {
4115 assert_json_snapshot!("event_data_output_message_completed", data, {
4117 ".message.id" => "[MESSAGE_ID]",
4118 ".message.created_at" => "[TIMESTAMP]"
4119 });
4120 });
4121 }
4122
4123 #[test]
4124 fn snapshot_turn_started() {
4125 let data = TurnStartedData {
4126 turn_id: test_turn_id(),
4127 input_message_id: test_message_id(),
4128 input_content: Some("Hello".to_string()),
4129 };
4130 with_settings!({
4131 sort_maps => true,
4132 }, {
4133 assert_json_snapshot!("event_data_turn_started", data);
4134 });
4135 }
4136
4137 #[test]
4138 fn snapshot_turn_completed() {
4139 let data = TurnCompletedData {
4140 turn_id: test_turn_id(),
4141 iterations: 3,
4142 duration_ms: Some(1500),
4143 usage: Some(TokenUsage::new(100, 50)),
4144 input_content: None,
4145 final_message_id: Some(test_message_id()),
4146 final_answer_preview: Some("Done.".to_string()),
4147 time_to_first_token_ms: Some(120),
4148 tool_call_count: Some(2),
4149 llm_call_count: Some(3),
4150 status: Some("completed".to_string()),
4151 };
4152 with_settings!({
4153 sort_maps => true,
4154 }, {
4155 assert_json_snapshot!("event_data_turn_completed", data);
4156 });
4157 }
4158
4159 #[test]
4160 fn snapshot_turn_failed() {
4161 let data = TurnFailedData {
4162 turn_id: test_turn_id(),
4163 error: "Rate limit exceeded".to_string(),
4164 error_code: Some("RATE_LIMIT".to_string()),
4165 error_fields: None,
4166 error_disclosure: None,
4167 };
4168 with_settings!({
4169 sort_maps => true,
4170 }, {
4171 assert_json_snapshot!("event_data_turn_failed", data);
4172 });
4173 }
4174
4175 #[test]
4176 fn snapshot_turn_cancelled() {
4177 let data = TurnCancelledData {
4178 turn_id: test_turn_id(),
4179 reason: Some("User requested".to_string()),
4180 usage: Some(TokenUsage::new(50, 25)),
4181 };
4182 with_settings!({
4183 sort_maps => true,
4184 }, {
4185 assert_json_snapshot!("event_data_turn_cancelled", data);
4186 });
4187 }
4188
4189 #[test]
4190 fn snapshot_reason_started() {
4191 let data = ReasonStartedData {
4192 harness_id: test_harness_id(),
4193 agent_id: Some(test_agent_id()),
4194 metadata: Some(ModelMetadata {
4195 model: "gpt-4o".to_string(),
4196 model_id: None,
4197 provider_id: None,
4198 }),
4199 };
4200 with_settings!({
4201 sort_maps => true,
4202 }, {
4203 assert_json_snapshot!("event_data_reason_started", data);
4204 });
4205 }
4206
4207 #[test]
4208 fn snapshot_reason_completed() {
4209 let data = ReasonCompletedData::success(
4210 "Hello world",
4211 true,
4212 2,
4213 Some(1000),
4214 Some(TokenUsage::new(100, 50)),
4215 );
4216 with_settings!({
4217 sort_maps => true,
4218 }, {
4219 assert_json_snapshot!("event_data_reason_completed", data);
4220 });
4221 }
4222
4223 #[test]
4224 fn snapshot_act_started() {
4225 let data = ActStartedData {
4226 tool_calls: vec![ToolCallSummary {
4227 id: "tc_1".to_string(),
4228 name: "get_weather".to_string(),
4229 display_name: None,
4230 narration: None,
4231 }],
4232 headline: None,
4233 };
4234 with_settings!({
4235 sort_maps => true,
4236 }, {
4237 assert_json_snapshot!("event_data_act_started", data);
4238 });
4239 }
4240
4241 #[test]
4242 fn snapshot_act_completed() {
4243 let data = ActCompletedData {
4244 completed: true,
4245 success_count: 2,
4246 error_count: 0,
4247 duration_ms: Some(500),
4248 headline: None,
4249 };
4250 with_settings!({
4251 sort_maps => true,
4252 }, {
4253 assert_json_snapshot!("event_data_act_completed", data);
4254 });
4255 }
4256
4257 #[test]
4258 fn snapshot_tool_started() {
4259 let data = ToolStartedData {
4260 tool_call: ToolCall {
4261 id: "tc_1".to_string(),
4262 name: "get_weather".to_string(),
4263 arguments: serde_json::json!({"city": "London"}),
4264 },
4265 tool_call_fingerprint: None,
4266 display_name: None,
4267 narration: None,
4268 };
4269 with_settings!({
4270 sort_maps => true,
4271 }, {
4272 assert_json_snapshot!("event_data_tool_started", data);
4273 });
4274 }
4275
4276 #[test]
4277 fn snapshot_tool_completed() {
4278 let data = ToolCompletedData::success(
4279 "tc_1".to_string(),
4280 "get_weather".to_string(),
4281 vec![crate::message::ContentPart::text("Sunny, 22°C")],
4282 Some(250),
4283 );
4284 with_settings!({
4285 sort_maps => true,
4286 }, {
4287 assert_json_snapshot!("event_data_tool_completed", data);
4288 });
4289 }
4290
4291 #[test]
4292 fn snapshot_llm_generation() {
4293 let data = LlmGenerationData::success(
4294 vec![Message::user("Hello")],
4295 vec![ToolDefinitionSummary {
4296 name: "tool1".to_string(),
4297 display_name: None,
4298 category: None,
4299 capability_id: None,
4300 capability_name: None,
4301 description: "A tool".to_string(),
4302 }],
4303 Some("Hi there!".to_string()),
4304 vec![],
4305 "gpt-4o".to_string(),
4306 Some("openai".to_string()),
4307 Some(TokenUsage::new(10, 5)),
4308 Some(100),
4309 Some(25),
4310 );
4311 with_settings!({
4312 sort_maps => true,
4313 }, {
4314 assert_json_snapshot!("event_data_llm_generation", data, {
4316 ".messages[].id" => "[MESSAGE_ID]",
4317 ".messages[].created_at" => "[TIMESTAMP]"
4318 });
4319 });
4320 }
4321
4322 #[test]
4323 fn snapshot_reason_thinking_started() {
4324 let data = ReasonThinkingStartedData {
4325 turn_id: test_turn_id(),
4326 model: Some("claude-4-opus".to_string()),
4327 };
4328 with_settings!({
4329 sort_maps => true,
4330 }, {
4331 assert_json_snapshot!("event_data_reason_thinking_started", data);
4332 });
4333 }
4334
4335 #[test]
4336 fn snapshot_reason_thinking_delta() {
4337 let data = ReasonThinkingDeltaData {
4338 turn_id: test_turn_id(),
4339 delta: "Let me think...".to_string(),
4340 accumulated: "Let me think...".to_string(),
4341 };
4342 with_settings!({
4343 sort_maps => true,
4344 }, {
4345 assert_json_snapshot!("event_data_reason_thinking_delta", data);
4346 });
4347 }
4348
4349 #[test]
4350 fn snapshot_reason_thinking_completed() {
4351 let data = ReasonThinkingCompletedData {
4352 turn_id: test_turn_id(),
4353 thinking: "I need to consider...".to_string(),
4354 };
4355 with_settings!({
4356 sort_maps => true,
4357 }, {
4358 assert_json_snapshot!("event_data_reason_thinking_completed", data);
4359 });
4360 }
4361
4362 #[test]
4363 fn snapshot_reason_item() {
4364 let data = ReasonItemData {
4365 turn_id: test_turn_id(),
4366 provider: "openai".to_string(),
4367 model: Some("gpt-5.5".to_string()),
4368 item_id: "rs_test".to_string(),
4369 encrypted_content: Some("OPAQUE".to_string()),
4370 summary: vec!["safe summary".to_string()],
4371 token_count: Some(42),
4372 };
4373 with_settings!({
4374 sort_maps => true,
4375 }, {
4376 assert_json_snapshot!("event_data_reason_item", data);
4377 });
4378 }
4379
4380 #[test]
4381 fn snapshot_session_started() {
4382 let data = SessionStartedData {
4383 harness_id: test_harness_id(),
4384 agent_id: Some(test_agent_id()),
4385 model_id: None,
4386 };
4387 with_settings!({
4388 sort_maps => true,
4389 }, {
4390 assert_json_snapshot!("event_data_session_started", data);
4391 });
4392 }
4393
4394 #[test]
4395 fn snapshot_session_activated() {
4396 let data = SessionActivatedData {
4397 turn_id: test_turn_id(),
4398 input_message_id: test_message_id(),
4399 };
4400 with_settings!({
4401 sort_maps => true,
4402 }, {
4403 assert_json_snapshot!("event_data_session_activated", data);
4404 });
4405 }
4406
4407 #[test]
4408 fn snapshot_session_idled() {
4409 let data = SessionIdledData {
4410 turn_id: test_turn_id(),
4411 iterations: Some(3),
4412 usage: Some(TokenUsage::new(500, 200)),
4413 };
4414 with_settings!({
4415 sort_maps => true,
4416 }, {
4417 assert_json_snapshot!("event_data_session_idled", data);
4418 });
4419 }
4420
4421 #[test]
4422 fn snapshot_session_title_updated() {
4423 let data = SessionTitleUpdatedData {
4424 previous_title: Some("Old title".to_string()),
4425 title: "Automatic Session Titles".to_string(),
4426 };
4427 with_settings!({
4428 sort_maps => true,
4429 }, {
4430 assert_json_snapshot!("event_data_session_title_updated", data);
4431 });
4432
4433 let untitled = serde_json::to_value(SessionTitleUpdatedData {
4434 previous_title: None,
4435 title: "First title".to_string(),
4436 })
4437 .expect("serialize title event");
4438 assert!(untitled["previous_title"].is_null());
4439 }
4440
4441 #[test]
4447 fn tool_call_summary_with_display_name() {
4448 let summary = ToolCallSummary {
4449 id: "tc_1".to_string(),
4450 name: "get_weather".to_string(),
4451 display_name: Some("Get Weather".to_string()),
4452 narration: None,
4453 };
4454 let json = serde_json::to_value(&summary).unwrap();
4455 assert_eq!(json["display_name"], "Get Weather");
4456
4457 let deserialized: ToolCallSummary = serde_json::from_value(json).unwrap();
4459 assert_eq!(deserialized.display_name.as_deref(), Some("Get Weather"));
4460 }
4461
4462 #[test]
4463 fn tool_call_summary_without_display_name_omits_field() {
4464 let summary = ToolCallSummary {
4465 id: "tc_1".to_string(),
4466 name: "get_weather".to_string(),
4467 display_name: None,
4468 narration: None,
4469 };
4470 let json = serde_json::to_string(&summary).unwrap();
4471 assert!(!json.contains("display_name"));
4472
4473 let json_without = r#"{"id":"tc_1","name":"get_weather"}"#;
4475 let deserialized: ToolCallSummary = serde_json::from_str(json_without).unwrap();
4476 assert_eq!(deserialized.display_name, None);
4477 }
4478
4479 #[test]
4480 fn act_started_with_definitions_populates_display_names() {
4481 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4482
4483 let tool_calls = vec![
4484 ToolCall {
4485 id: "tc_1".to_string(),
4486 name: "get_weather".to_string(),
4487 arguments: serde_json::json!({}),
4488 },
4489 ToolCall {
4490 id: "tc_2".to_string(),
4491 name: "unknown_tool".to_string(),
4492 arguments: serde_json::json!({}),
4493 },
4494 ];
4495 let tool_defs = vec![crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4496 name: "get_weather".to_string(),
4497 display_name: Some("Get Weather".to_string()),
4498 description: "Gets weather".to_string(),
4499 parameters: serde_json::json!({}),
4500 policy: ToolPolicy::Auto,
4501 category: None,
4502 deferrable: DeferrablePolicy::default(),
4503 hints: crate::tool_types::ToolHints::default(),
4504 full_parameters: None,
4505 })];
4506
4507 let data = ActStartedData::with_definitions(&tool_calls, &tool_defs);
4508 assert_eq!(data.tool_calls.len(), 2);
4509 assert_eq!(
4510 data.tool_calls[0].display_name.as_deref(),
4511 Some("Get Weather")
4512 );
4513 assert_eq!(data.tool_calls[1].display_name, None);
4514 }
4515
4516 #[test]
4517 fn tool_completed_with_display_name_roundtrip() {
4518 let data = ToolCompletedData::success(
4519 "tc_1".to_string(),
4520 "get_weather".to_string(),
4521 vec![crate::message::ContentPart::text("Sunny")],
4522 Some(100),
4523 )
4524 .with_display_name(Some("Get Weather".to_string()));
4525
4526 assert_eq!(data.display_name.as_deref(), Some("Get Weather"));
4527
4528 let json = serde_json::to_value(&data).unwrap();
4529 assert_eq!(json["display_name"], "Get Weather");
4530
4531 let deserialized: ToolCompletedData = serde_json::from_value(json).unwrap();
4532 assert_eq!(deserialized.display_name.as_deref(), Some("Get Weather"));
4533 }
4534
4535 #[test]
4536 fn tool_started_display_name_serialization() {
4537 let data = ToolStartedData {
4538 tool_call: ToolCall {
4539 id: "tc_1".to_string(),
4540 name: "bash".to_string(),
4541 arguments: serde_json::json!({"command": "ls"}),
4542 },
4543 tool_call_fingerprint: None,
4544 display_name: Some("Bash".to_string()),
4545 narration: None,
4546 };
4547
4548 let json = serde_json::to_value(&data).unwrap();
4549 assert_eq!(json["display_name"], "Bash");
4550 }
4551
4552 #[test]
4553 fn tool_definition_summary_display_name() {
4554 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4555
4556 let def = crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4557 name: "read_file".to_string(),
4558 display_name: Some("Read File".to_string()),
4559 description: "Reads a file".to_string(),
4560 parameters: serde_json::json!({}),
4561 policy: ToolPolicy::Auto,
4562 category: None,
4563 deferrable: DeferrablePolicy::default(),
4564 hints: crate::tool_types::ToolHints::default(),
4565 full_parameters: None,
4566 });
4567
4568 let summary = ToolDefinitionSummary::from(&def);
4569 assert_eq!(summary.display_name.as_deref(), Some("Read File"));
4570
4571 let json = serde_json::to_value(&summary).unwrap();
4572 assert_eq!(json["display_name"], "Read File");
4573 }
4574
4575 #[test]
4582 fn forward_compat_unknown_fields_ignored() {
4583 let json = r#"{
4585 "turn_id": "turn_00000000000000000000000000000002",
4586 "iterations": 3,
4587 "duration_ms": 1500,
4588 "usage": {"input_tokens": 100, "output_tokens": 50},
4589 "future_field": "should be ignored",
4590 "another_new_field": 42
4591 }"#;
4592
4593 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4594 assert_eq!(data.iterations, 3);
4595 assert_eq!(data.duration_ms, Some(1500));
4596 }
4597
4598 #[test]
4599 fn forward_compat_unknown_event_type_becomes_unsupported() {
4600 let json = serde_json::json!({"some_field": "value"});
4602 let data = deserialize_event_data("future.event.type", json);
4603
4604 assert!(data.is_unsupported());
4605 assert_eq!(data.event_type(), "unsupported");
4606 }
4607
4608 #[test]
4609 fn forward_compat_unsupported_preserves_data() {
4610 let original = serde_json::json!({"key": "value", "nested": {"a": 1}});
4612 let data = deserialize_event_data("unknown.event", original.clone());
4613
4614 match data {
4615 EventData::Unsupported { event_type, data } => {
4616 assert_eq!(event_type, "unknown.event");
4617 assert_eq!(data, original);
4618 }
4619 _ => panic!("Expected Unsupported variant"),
4620 }
4621 }
4622
4623 #[test]
4624 fn forward_compat_optional_fields_absent() {
4625 let json = r#"{
4627 "turn_id": "turn_00000000000000000000000000000002",
4628 "iterations": 3
4629 }"#;
4630
4631 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4632 assert_eq!(data.iterations, 3);
4633 assert!(data.duration_ms.is_none());
4634 assert!(data.usage.is_none());
4635 assert!(data.input_content.is_none());
4636 assert!(data.final_message_id.is_none());
4637 assert!(data.final_answer_preview.is_none());
4638 assert!(data.time_to_first_token_ms.is_none());
4639 assert!(data.tool_call_count.is_none());
4640 assert!(data.llm_call_count.is_none());
4641 assert!(data.status.is_none());
4642 }
4643
4644 #[test]
4650 fn round_trip_all_event_data_types() {
4651 let test_cases: Vec<(&str, EventData)> = vec![
4653 (
4654 INPUT_MESSAGE,
4655 InputMessageData::new(Message::user("test")).into(),
4656 ),
4657 (
4658 OUTPUT_MESSAGE_STARTED,
4659 OutputMessageStartedData {
4660 turn_id: test_turn_id(),
4661 message_id: test_message_id(),
4662 model: None,
4663 iteration: None,
4664 phase: None,
4665 }
4666 .into(),
4667 ),
4668 (
4669 OUTPUT_MESSAGE_DELTA,
4670 OutputMessageDeltaData {
4671 turn_id: test_turn_id(),
4672 message_id: test_message_id(),
4673 delta: "x".to_string(),
4674 accumulated: "x".to_string(),
4675 phase: None,
4676 }
4677 .into(),
4678 ),
4679 (
4680 OUTPUT_MESSAGE_COMPLETED,
4681 OutputMessageCompletedData::new(Message::assistant("hi")).into(),
4682 ),
4683 (
4684 TURN_STARTED,
4685 TurnStartedData {
4686 turn_id: test_turn_id(),
4687 input_message_id: test_message_id(),
4688 input_content: None,
4689 }
4690 .into(),
4691 ),
4692 (
4693 TURN_COMPLETED,
4694 TurnCompletedData {
4695 turn_id: test_turn_id(),
4696 iterations: 1,
4697 duration_ms: None,
4698 usage: None,
4699 input_content: None,
4700 final_message_id: None,
4701 final_answer_preview: None,
4702 time_to_first_token_ms: None,
4703 tool_call_count: None,
4704 llm_call_count: None,
4705 status: None,
4706 }
4707 .into(),
4708 ),
4709 (
4710 TURN_FAILED,
4711 TurnFailedData {
4712 turn_id: test_turn_id(),
4713 error: "err".to_string(),
4714 error_code: None,
4715 error_fields: None,
4716 error_disclosure: None,
4717 }
4718 .into(),
4719 ),
4720 (
4721 TURN_CANCELLED,
4722 TurnCancelledData {
4723 turn_id: test_turn_id(),
4724 reason: None,
4725 usage: None,
4726 }
4727 .into(),
4728 ),
4729 (
4730 TURN_SEALED,
4731 TurnSealedData {
4732 turn_id: test_turn_id(),
4733 reason: "no_progress".to_string(),
4734 detail: Some("sealed".to_string()),
4735 iterations: Some(3),
4736 usage: None,
4737 }
4738 .into(),
4739 ),
4740 (
4741 REASON_STARTED,
4742 ReasonStartedData {
4743 harness_id: test_harness_id(),
4744 agent_id: Some(test_agent_id()),
4745 metadata: None,
4746 }
4747 .into(),
4748 ),
4749 (
4750 REASON_COMPLETED,
4751 ReasonCompletedData::success("", false, 0, None, None).into(),
4752 ),
4753 (
4754 ACT_STARTED,
4755 ActStartedData {
4756 tool_calls: vec![],
4757 headline: None,
4758 }
4759 .into(),
4760 ),
4761 (
4762 ACT_COMPLETED,
4763 ActCompletedData {
4764 completed: true,
4765 success_count: 0,
4766 error_count: 0,
4767 duration_ms: None,
4768 headline: None,
4769 }
4770 .into(),
4771 ),
4772 (
4773 SESSION_STARTED,
4774 SessionStartedData {
4775 harness_id: test_harness_id(),
4776 agent_id: Some(test_agent_id()),
4777 model_id: None,
4778 }
4779 .into(),
4780 ),
4781 (
4782 SESSION_ACTIVATED,
4783 SessionActivatedData {
4784 turn_id: test_turn_id(),
4785 input_message_id: test_message_id(),
4786 }
4787 .into(),
4788 ),
4789 (
4790 SESSION_IDLED,
4791 SessionIdledData {
4792 turn_id: test_turn_id(),
4793 iterations: None,
4794 usage: None,
4795 }
4796 .into(),
4797 ),
4798 (
4799 SESSION_TITLE_UPDATED,
4800 SessionTitleUpdatedData {
4801 previous_title: Some("Old title".to_string()),
4802 title: "New title".to_string(),
4803 }
4804 .into(),
4805 ),
4806 ];
4807
4808 for (event_type, original) in test_cases {
4809 let json = serde_json::to_value(&original).unwrap();
4811 let deserialized = deserialize_event_data(event_type, json);
4813 assert_eq!(
4815 original.event_type(),
4816 deserialized.event_type(),
4817 "Event type mismatch for {}",
4818 event_type
4819 );
4820 }
4821 }
4822
4823 #[test]
4829 fn event_structure_has_required_fields() {
4830 let session_id = test_session_id();
4831 let context = EventContext::turn(test_turn_id(), test_message_id());
4832 let event = Event::new(
4833 session_id,
4834 context,
4835 InputMessageData::new(Message::user("test")),
4836 );
4837
4838 let json = serde_json::to_value(&event).unwrap();
4840 assert!(json.get("id").is_some(), "Missing id field");
4841 assert!(json.get("type").is_some(), "Missing type field");
4842 assert!(json.get("ts").is_some(), "Missing ts field");
4843 assert!(json.get("session_id").is_some(), "Missing session_id field");
4844 assert!(json.get("context").is_some(), "Missing context field");
4845 assert!(json.get("data").is_some(), "Missing data field");
4846 }
4847
4848 #[test]
4849 fn event_context_span_fields() {
4850 let context = EventContext::empty().with_span(
4851 "trace123".to_string(),
4852 "span456".to_string(),
4853 Some("parent789".to_string()),
4854 );
4855
4856 let json = serde_json::to_value(&context).unwrap();
4857 assert_eq!(
4858 json.get("trace_id").and_then(|v| v.as_str()),
4859 Some("trace123")
4860 );
4861 assert_eq!(
4862 json.get("span_id").and_then(|v| v.as_str()),
4863 Some("span456")
4864 );
4865 assert_eq!(
4866 json.get("parent_span_id").and_then(|v| v.as_str()),
4867 Some("parent789")
4868 );
4869 }
4870
4871 #[test]
4872 fn is_unsupported_returns_false_for_known_types() {
4873 let data = InputMessageData::new(Message::user("test"));
4874 let event_data: EventData = data.into();
4875 assert!(!event_data.is_unsupported());
4876 }
4877
4878 #[test]
4879 fn is_unsupported_returns_true_for_unsupported() {
4880 let data = deserialize_event_data("unknown.type", serde_json::json!({}));
4881 assert!(data.is_unsupported());
4882 }
4883}