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";
117pub const SESSION_MODEL_CHANGED: &str = "session.model.changed";
122
123pub const SCHEDULE_TRIGGERED: &str = "schedule.triggered";
125
126pub const TASK_CREATED: &str = "task.created";
133pub const TASK_UPDATED: &str = "task.updated";
134pub const TASK_MESSAGE_SENT: &str = "task.message.sent";
135pub const TASK_MESSAGE_RECEIVED: &str = "task.message.received";
136
137pub const CONTEXT_COMPACTING: &str = "context.compacting";
139pub const CONTEXT_COMPACTED: &str = "context.compacted";
140pub const CONTEXT_COMPACTION_SKIPPED: &str = "context.compaction.skipped";
142pub const CONTEXT_COMPACTION_FAILED: &str = "context.compaction.failed";
144
145pub const FILE_WRITTEN: &str = "file.written";
147
148pub const BUDGET_WARNING: &str = "budget.warning";
150pub const BUDGET_PAUSED: &str = "budget.paused";
151pub const BUDGET_EXHAUSTED: &str = "budget.exhausted";
152pub const BUDGET_RESUMED: &str = "budget.resumed";
153
154pub const VOICE_SESSION_STARTED: &str = "voice.session.started";
156pub const VOICE_INPUT_TRANSCRIPT_DELTA: &str = "voice.input_transcript.delta";
157pub const VOICE_INPUT_TRANSCRIPT_COMPLETED: &str = "voice.input_transcript.completed";
158pub const VOICE_OUTPUT_TRANSCRIPT_DELTA: &str = "voice.output_transcript.delta";
159pub const VOICE_OUTPUT_TRANSCRIPT_COMPLETED: &str = "voice.output_transcript.completed";
160pub const VOICE_SESSION_ENDED: &str = "voice.session.ended";
161pub const VOICE_SESSION_FAILED: &str = "voice.session.failed";
162
163pub const VALID_EVENT_TYPES: &[&str] = &[
167 INPUT_MESSAGE,
168 OUTPUT_MESSAGE_STARTED,
169 OUTPUT_MESSAGE_DELTA,
170 OUTPUT_MESSAGE_COMPLETED,
171 OUTPUT_MESSAGE_REPLACED,
172 TURN_STARTED,
173 TURN_COMPLETED,
174 TURN_FAILED,
175 TURN_SEALED,
176 TURN_CANCELLED,
177 REASON_STARTED,
178 REASON_COMPLETED,
179 REASON_RECOVERED,
180 ACT_STARTED,
181 ACT_COMPLETED,
182 TOOL_STARTED,
183 TOOL_COMPLETED,
184 TOOL_PROGRESS,
185 TOOL_OUTPUT_DELTA,
186 TOOL_CALL_REQUESTED,
187 TRANSCRIPT_REPAIRED,
188 TOOL_CALL_REPAIRED,
189 LLM_GENERATION,
190 REASON_THINKING_STARTED,
191 REASON_THINKING_DELTA,
192 REASON_THINKING_COMPLETED,
193 REASON_ITEM,
194 SESSION_STARTED,
195 SESSION_ACTIVATED,
196 SESSION_IDLED,
197 SESSION_TITLE_UPDATED,
198 SESSION_MODEL_CHANGED,
199 SCHEDULE_TRIGGERED,
200 CONTEXT_COMPACTING,
201 CONTEXT_COMPACTED,
202 CONTEXT_COMPACTION_SKIPPED,
203 CONTEXT_COMPACTION_FAILED,
204 BUDGET_WARNING,
205 BUDGET_PAUSED,
206 BUDGET_EXHAUSTED,
207 BUDGET_RESUMED,
208 VOICE_SESSION_STARTED,
209 VOICE_INPUT_TRANSCRIPT_DELTA,
210 VOICE_INPUT_TRANSCRIPT_COMPLETED,
211 VOICE_OUTPUT_TRANSCRIPT_DELTA,
212 VOICE_OUTPUT_TRANSCRIPT_COMPLETED,
213 VOICE_SESSION_ENDED,
214 VOICE_SESSION_FAILED,
215 FILE_WRITTEN,
216 CAPABILITY_USAGE,
217];
218
219use crate::execution_context::ExecutionContext;
224
225#[derive(Debug, Clone, Serialize, Deserialize, Default)]
232#[cfg_attr(feature = "openapi", derive(ToSchema))]
233pub struct EventContext {
234 #[serde(skip_serializing_if = "Option::is_none")]
236 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "turn_01933b5a00007000800000000000001"))]
237 pub turn_id: Option<TurnId>,
238
239 #[serde(skip_serializing_if = "Option::is_none")]
241 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "message_01933b5a00007000800000000000001"))]
242 pub input_message_id: Option<MessageId>,
243
244 #[serde(skip_serializing_if = "Option::is_none")]
246 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "exec_01933b5a00007000800000000000001"))]
247 pub exec_id: Option<ExecId>,
248
249 #[serde(skip_serializing_if = "Option::is_none")]
252 pub trace_id: Option<String>,
253
254 #[serde(skip_serializing_if = "Option::is_none")]
257 pub span_id: Option<String>,
258
259 #[serde(skip_serializing_if = "Option::is_none")]
262 pub parent_span_id: Option<String>,
263}
264
265impl EventContext {
266 pub fn empty() -> Self {
268 Self::default()
269 }
270
271 pub fn from_execution_context(ctx: &ExecutionContext) -> Self {
273 Self {
274 turn_id: Some(ctx.turn_id),
275 input_message_id: Some(ctx.input_message_id),
276 exec_id: Some(ctx.exec_id),
277 trace_id: None,
278 span_id: None,
279 parent_span_id: None,
280 }
281 }
282
283 pub fn turn(turn_id: TurnId, input_message_id: MessageId) -> Self {
285 Self {
286 turn_id: Some(turn_id),
287 input_message_id: Some(input_message_id),
288 exec_id: None,
289 trace_id: None,
290 span_id: None,
291 parent_span_id: None,
292 }
293 }
294
295 pub fn with_span(
297 mut self,
298 trace_id: String,
299 span_id: String,
300 parent_span_id: Option<String>,
301 ) -> Self {
302 self.trace_id = Some(trace_id);
303 self.span_id = Some(span_id);
304 self.parent_span_id = parent_span_id;
305 self
306 }
307}
308
309#[derive(Debug, Clone, Serialize)]
325#[cfg_attr(feature = "openapi", derive(ToSchema))]
326pub struct Event {
327 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "event_01933b5a00007000800000000000001"))]
329 pub id: EventId,
330
331 #[serde(rename = "type")]
333 pub event_type: String,
334
335 pub ts: DateTime<Utc>,
337
338 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "session_01933b5a00007000800000000000001"))]
340 pub session_id: SessionId,
341
342 pub context: EventContext,
344
345 pub data: EventData,
348
349 #[serde(skip_serializing_if = "Option::is_none")]
351 pub metadata: Option<serde_json::Value>,
352
353 #[serde(skip_serializing_if = "Option::is_none")]
355 pub tags: Option<Vec<String>>,
356
357 #[serde(skip_serializing_if = "Option::is_none")]
359 pub sequence: Option<i32>,
360}
361
362#[derive(Debug, Deserialize)]
363struct RawEvent {
364 id: EventId,
365 #[serde(rename = "type")]
366 event_type: String,
367 ts: DateTime<Utc>,
368 session_id: SessionId,
369 context: EventContext,
370 data: serde_json::Value,
371 metadata: Option<serde_json::Value>,
372 tags: Option<Vec<String>>,
373 sequence: Option<i32>,
374}
375
376impl<'de> Deserialize<'de> for Event {
377 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
378 where
379 D: Deserializer<'de>,
380 {
381 let raw = RawEvent::deserialize(deserializer)?;
382 let data = deserialize_event_data(&raw.event_type, raw.data);
383 Ok(Self {
384 id: raw.id,
385 event_type: raw.event_type,
386 ts: raw.ts,
387 session_id: raw.session_id,
388 context: raw.context,
389 data,
390 metadata: raw.metadata,
391 tags: raw.tags,
392 sequence: raw.sequence,
393 })
394 }
395}
396
397impl Event {
398 pub fn into_public(mut self) -> Self {
401 self.data = self.data.into_public();
402 self
403 }
404
405 pub fn new(session_id: SessionId, context: EventContext, data: impl Into<EventData>) -> Self {
409 let data = data.into();
410 let event_type = data.event_type().to_string();
411 Self {
412 id: EventId::new(),
413 event_type,
414 ts: Utc::now(),
415 session_id,
416 context,
417 data,
418 metadata: None,
419 tags: None,
420 sequence: None,
421 }
422 }
423
424 pub fn with_id(
426 id: EventId,
427 session_id: SessionId,
428 context: EventContext,
429 data: impl Into<EventData>,
430 ) -> Self {
431 let data = data.into();
432 let event_type = data.event_type().to_string();
433 Self {
434 id,
435 event_type,
436 ts: Utc::now(),
437 session_id,
438 context,
439 data,
440 metadata: None,
441 tags: None,
442 sequence: None,
443 }
444 }
445
446 pub fn with_sequence(mut self, sequence: i32) -> Self {
448 self.sequence = Some(sequence);
449 self
450 }
451
452 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
454 self.metadata = Some(metadata);
455 self
456 }
457
458 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
460 self.tags = Some(tags);
461 self
462 }
463
464 pub fn session_uuid(&self) -> Uuid {
466 self.session_id.uuid()
467 }
468
469 pub fn is_message_event(&self) -> bool {
471 self.event_type == INPUT_MESSAGE || self.event_type == OUTPUT_MESSAGE_COMPLETED
472 }
473
474 pub fn is_ephemeral(&self) -> bool {
482 is_ephemeral_event_type(&self.event_type)
483 }
484
485 pub fn is_input_event(&self) -> bool {
487 self.event_type.starts_with("input.")
488 }
489
490 pub fn is_output_event(&self) -> bool {
492 self.event_type.starts_with("output.")
493 }
494
495 pub fn is_atom_event(&self) -> bool {
497 matches!(
498 self.event_type.as_str(),
499 REASON_STARTED
500 | REASON_COMPLETED
501 | REASON_RECOVERED
502 | ACT_STARTED
503 | ACT_COMPLETED
504 | TOOL_STARTED
505 | TOOL_COMPLETED
506 | TOOL_PROGRESS
507 | TOOL_CALL_REQUESTED
508 | TRANSCRIPT_REPAIRED
509 )
510 }
511
512 pub fn is_turn_event(&self) -> bool {
514 self.event_type.starts_with("turn.")
515 }
516
517 pub fn is_session_event(&self) -> bool {
519 self.event_type.starts_with("session.")
520 }
521
522 pub fn is_unsupported(&self) -> bool {
525 self.data.is_unsupported()
526 }
527}
528
529use crate::message::{ContentPart, Message};
534use crate::tool_narration::{
535 ToolNarrationPhase, render_group_headline_with_locale, render_tool_narration_with_locale,
536};
537use crate::tool_types::ToolCall;
538use everruns_provider::execution_phase::ExecutionPhase;
539
540#[derive(Debug, Clone, Serialize, Deserialize)]
542#[cfg_attr(feature = "openapi", derive(ToSchema))]
543pub struct ModelMetadata {
544 pub model: String,
546
547 #[serde(skip_serializing_if = "Option::is_none")]
549 pub model_id: Option<Uuid>,
550
551 #[serde(skip_serializing_if = "Option::is_none")]
553 pub provider_id: Option<Uuid>,
554}
555
556#[derive(Debug, Clone, Serialize, Deserialize, Default)]
583#[cfg_attr(feature = "openapi", derive(ToSchema))]
584pub struct TokenUsage {
585 pub input_tokens: u32,
588 pub output_tokens: u32,
590 #[serde(skip_serializing_if = "Option::is_none")]
592 pub cache_read_tokens: Option<u32>,
593 #[serde(skip_serializing_if = "Option::is_none")]
595 pub cache_creation_tokens: Option<u32>,
596
597 #[serde(skip_serializing_if = "Option::is_none")]
601 pub actual_cost_usd: Option<f64>,
602
603 #[serde(skip_serializing_if = "Option::is_none")]
608 pub estimated_cost_usd: Option<f64>,
609
610 #[serde(skip_serializing_if = "Option::is_none")]
618 pub effective_cost_usd: Option<f64>,
619}
620
621impl TokenUsage {
622 pub fn new(input_tokens: u32, output_tokens: u32) -> Self {
624 Self {
625 input_tokens,
626 output_tokens,
627 cache_read_tokens: None,
628 cache_creation_tokens: None,
629 actual_cost_usd: None,
630 estimated_cost_usd: None,
631 effective_cost_usd: None,
632 }
633 }
634
635 pub fn with_cache(
637 input_tokens: u32,
638 output_tokens: u32,
639 cache_read_tokens: Option<u32>,
640 cache_creation_tokens: Option<u32>,
641 ) -> Self {
642 Self {
643 input_tokens,
644 output_tokens,
645 cache_read_tokens,
646 cache_creation_tokens,
647 actual_cost_usd: None,
648 estimated_cost_usd: None,
649 effective_cost_usd: None,
650 }
651 }
652
653 pub fn with_cost(
657 mut self,
658 actual_cost_usd: Option<f64>,
659 estimated_cost_usd: Option<f64>,
660 ) -> Self {
661 self.actual_cost_usd = actual_cost_usd;
662 self.estimated_cost_usd = estimated_cost_usd;
663 self
664 }
665
666 pub fn with_effective_cost(mut self, effective_cost_usd: Option<f64>) -> Self {
671 self.effective_cost_usd = effective_cost_usd;
672 self
673 }
674
675 pub fn effective_cost_usd(&self) -> Option<f64> {
679 self.effective_cost_usd
680 .or(self.actual_cost_usd.or(self.estimated_cost_usd))
681 }
682
683 pub fn total_tokens(&self) -> u32 {
685 self.input_tokens.saturating_add(self.output_tokens)
686 }
687
688 pub fn add(&mut self, other: &TokenUsage) {
690 let current_cost = self.effective_cost_usd();
695 self.input_tokens = self.input_tokens.saturating_add(other.input_tokens);
696 self.output_tokens = self.output_tokens.saturating_add(other.output_tokens);
697 if let Some(cache) = other.cache_read_tokens {
698 let total = self.cache_read_tokens.get_or_insert(0);
699 *total = total.saturating_add(cache);
700 }
701 if let Some(cache) = other.cache_creation_tokens {
702 let total = self.cache_creation_tokens.get_or_insert(0);
703 *total = total.saturating_add(cache);
704 }
705 if let Some(cost) = other.actual_cost_usd {
706 *self.actual_cost_usd.get_or_insert(0.0) += cost;
707 }
708 if let Some(cost) = other.estimated_cost_usd {
709 *self.estimated_cost_usd.get_or_insert(0.0) += cost;
710 }
711 if let Some(cost) = other.effective_cost_usd() {
712 *self
713 .effective_cost_usd
714 .get_or_insert(current_cost.unwrap_or(0.0)) += cost;
715 }
716 }
717}
718
719#[cfg(test)]
720mod token_usage_tests {
721 use super::TokenUsage;
722
723 #[test]
724 fn effective_cost_precedence_preserves_zero_and_missing_values() {
725 for (actual, estimated, explicit, expected) in [
726 (None, None, None, None),
727 (None, Some(2.0), None, Some(2.0)),
728 (Some(1.0), Some(2.0), None, Some(1.0)),
729 (Some(0.0), Some(2.0), None, Some(0.0)),
730 (Some(1.0), Some(2.0), Some(3.0), Some(3.0)),
731 (Some(1.0), Some(2.0), Some(0.0), Some(0.0)),
732 ] {
733 let usage = TokenUsage::new(10, 5)
734 .with_cost(actual, estimated)
735 .with_effective_cost(explicit);
736 assert_eq!(usage.actual_cost_usd, actual);
737 assert_eq!(usage.estimated_cost_usd, estimated);
738 assert_eq!(usage.effective_cost_usd(), expected);
739 }
740 }
741
742 #[test]
743 fn add_seeds_effective_cost_from_accumulator_implicit_cost() {
744 let mut aggregate = TokenUsage::new(10, 5).with_cost(Some(1.0), Some(10.0));
745 let generation = TokenUsage::new(20, 10).with_cost(None, Some(2.0));
746
747 aggregate.add(&generation);
748
749 assert_eq!(aggregate.actual_cost_usd, Some(1.0));
750 assert_eq!(aggregate.estimated_cost_usd, Some(12.0));
751 assert_eq!(aggregate.effective_cost_usd(), Some(3.0));
752 }
753
754 #[test]
755 fn add_effective_cost_does_not_double_count_when_actual_is_mutated() {
756 let mut aggregate = TokenUsage::new(10, 5).with_cost(Some(1.0), None);
761 let generation = TokenUsage::new(20, 10).with_cost(Some(3.0), None);
762
763 aggregate.add(&generation);
764
765 assert_eq!(aggregate.actual_cost_usd, Some(4.0));
766 assert_eq!(aggregate.effective_cost_usd(), Some(4.0));
767 }
768
769 #[test]
770 fn aggregate_token_counters_saturate_at_their_bound() {
771 let mut ordinary = TokenUsage::with_cache(7, 3, None, Some(2));
772 ordinary.add(&TokenUsage::with_cache(11, 5, Some(4), None));
773 assert_eq!((ordinary.input_tokens, ordinary.output_tokens), (18, 8));
774 assert_eq!(ordinary.total_tokens(), 26);
775 assert_eq!(ordinary.cache_read_tokens, Some(4));
776 assert_eq!(ordinary.cache_creation_tokens, Some(2));
777
778 let mut aggregate = TokenUsage::with_cache(
779 u32::MAX - 1,
780 u32::MAX - 1,
781 Some(u32::MAX - 1),
782 Some(u32::MAX - 1),
783 );
784 aggregate.add(&TokenUsage::with_cache(10, 10, Some(10), Some(10)));
785
786 assert_eq!(aggregate.input_tokens, u32::MAX);
787 assert_eq!(aggregate.output_tokens, u32::MAX);
788 assert_eq!(aggregate.cache_read_tokens, Some(u32::MAX));
789 assert_eq!(aggregate.cache_creation_tokens, Some(u32::MAX));
790 assert_eq!(aggregate.total_tokens(), u32::MAX);
791 }
792}
793
794#[derive(Debug, Clone, Serialize, Deserialize)]
796#[cfg_attr(feature = "openapi", derive(ToSchema))]
797pub struct InputMessageData {
798 pub message: Message,
800}
801
802impl InputMessageData {
803 pub fn new(message: Message) -> Self {
804 Self { message }
805 }
806}
807
808#[derive(Debug, Clone, Serialize, Deserialize)]
817#[cfg_attr(feature = "openapi", derive(ToSchema))]
818pub struct OutputMessageStartedData {
819 #[serde(default, skip_serializing_if = "Option::is_none")]
822 pub reasoning_state: Option<everruns_provider::reasoning_updates::ReasoningState>,
823 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
825 pub turn_id: TurnId,
826
827 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_550e8400e29b41d4a716446655440000"))]
830 pub message_id: MessageId,
831
832 #[serde(skip_serializing_if = "Option::is_none")]
834 pub model: Option<String>,
835
836 #[serde(skip_serializing_if = "Option::is_none")]
839 pub iteration: Option<u32>,
840
841 #[serde(default, skip_serializing_if = "Option::is_none")]
851 pub phase: Option<ExecutionPhase>,
852}
853
854#[derive(Debug, Clone, Serialize, Deserialize)]
859#[cfg_attr(feature = "openapi", derive(ToSchema))]
860pub struct OutputMessageDeltaData {
861 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
863 pub turn_id: TurnId,
864
865 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_550e8400e29b41d4a716446655440000"))]
868 pub message_id: MessageId,
869
870 pub delta: String,
872
873 pub accumulated: String,
875
876 #[serde(default, skip_serializing_if = "Option::is_none")]
887 pub phase: Option<ExecutionPhase>,
888}
889
890#[derive(Debug, Clone, Serialize, Deserialize)]
892#[cfg_attr(feature = "openapi", derive(ToSchema))]
893pub struct OutputMessageCompletedData {
894 pub message: Message,
896
897 #[serde(skip_serializing_if = "Option::is_none")]
899 pub metadata: Option<ModelMetadata>,
900
901 #[serde(skip_serializing_if = "Option::is_none")]
903 pub usage: Option<TokenUsage>,
904
905 #[serde(default, skip_serializing_if = "Option::is_none")]
907 pub error_code: Option<String>,
908
909 #[serde(default, skip_serializing_if = "Option::is_none")]
911 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
912 pub error_fields: Option<UserFacingErrorFields>,
913
914 #[serde(default, skip_serializing_if = "Option::is_none")]
918 pub error_disclosure: Option<String>,
919}
920
921impl OutputMessageCompletedData {
922 pub fn new(message: Message) -> Self {
923 Self {
924 message,
925 metadata: None,
926 usage: None,
927 error_code: None,
928 error_fields: None,
929 error_disclosure: None,
930 }
931 }
932
933 pub fn with_metadata(mut self, metadata: ModelMetadata) -> Self {
934 self.metadata = Some(metadata);
935 self
936 }
937
938 pub fn with_usage(mut self, usage: TokenUsage) -> Self {
939 self.usage = Some(usage);
940 self
941 }
942
943 pub fn with_user_facing_error(mut self, error: &UserFacingError) -> Self {
944 error.apply_to_event_fields(&mut self.error_code, &mut self.error_fields);
945 self
946 }
947
948 pub fn with_error_disclosure(mut self, mode: crate::ErrorDisclosure) -> Self {
949 self.error_disclosure = Some(mode.as_str().to_string());
950 self
951 }
952}
953
954#[derive(Debug, Clone, Serialize, Deserialize)]
961#[cfg_attr(feature = "openapi", derive(ToSchema))]
962pub struct OutputMessageReplacedData {
963 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
965 pub turn_id: TurnId,
966
967 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_550e8400e29b41d4a716446655440000"))]
970 pub message_id: MessageId,
971
972 pub guardrail_capability_id: String,
975
976 pub guardrail_id: String,
978
979 pub reason_code: String,
982
983 pub replacement: String,
985}
986
987#[derive(Debug, Clone, Serialize, Deserialize)]
993#[cfg_attr(feature = "openapi", derive(ToSchema))]
994pub struct ReasonStartedData {
995 pub harness_id: HarnessId,
997
998 #[serde(skip_serializing_if = "Option::is_none")]
1000 pub agent_id: Option<AgentId>,
1001
1002 #[serde(skip_serializing_if = "Option::is_none")]
1004 pub metadata: Option<ModelMetadata>,
1005}
1006
1007#[derive(Debug, Clone, Serialize, Deserialize)]
1009#[cfg_attr(feature = "openapi", derive(ToSchema))]
1010pub struct ReasonCompletedData {
1011 pub success: bool,
1013
1014 #[serde(skip_serializing_if = "Option::is_none")]
1016 pub text_preview: Option<String>,
1017
1018 pub has_tool_calls: bool,
1020
1021 pub tool_call_count: u32,
1023
1024 #[serde(skip_serializing_if = "Option::is_none")]
1026 pub error: Option<String>,
1027
1028 #[serde(skip_serializing_if = "Option::is_none")]
1030 pub duration_ms: Option<u64>,
1031
1032 #[serde(skip_serializing_if = "Option::is_none")]
1034 pub usage: Option<TokenUsage>,
1035}
1036
1037impl ReasonCompletedData {
1038 pub fn success(
1039 text: &str,
1040 has_tool_calls: bool,
1041 tool_call_count: u32,
1042 duration_ms: Option<u64>,
1043 usage: Option<TokenUsage>,
1044 ) -> Self {
1045 let text_preview = if text.is_empty() {
1046 None
1047 } else {
1048 Some(text.chars().take(200).collect())
1049 };
1050
1051 Self {
1052 success: true,
1053 text_preview,
1054 has_tool_calls,
1055 tool_call_count,
1056 error: None,
1057 duration_ms,
1058 usage,
1059 }
1060 }
1061
1062 pub fn failure(error: String, duration_ms: Option<u64>) -> Self {
1063 Self {
1064 success: false,
1065 text_preview: None,
1066 has_tool_calls: false,
1067 tool_call_count: 0,
1068 error: Some(error),
1069 duration_ms,
1070 usage: None,
1071 }
1072 }
1073}
1074
1075#[derive(Debug, Clone, Serialize, Deserialize)]
1077#[cfg_attr(feature = "openapi", derive(ToSchema))]
1078#[serde(rename_all = "snake_case")]
1079pub enum RecoveryMode {
1080 Finalize,
1083 Restart,
1085}
1086
1087#[derive(Debug, Clone, Serialize, Deserialize)]
1093#[cfg_attr(feature = "openapi", derive(ToSchema))]
1094pub struct ReasonRecoveredData {
1095 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1097 pub turn_id: TurnId,
1098
1099 pub mode: RecoveryMode,
1101
1102 pub accumulated_len: usize,
1104}
1105
1106#[derive(Debug, Clone, Serialize, Deserialize)]
1108#[serde(rename_all = "snake_case")]
1109#[cfg_attr(feature = "openapi", derive(ToSchema))]
1110pub enum CapabilityUsageKind {
1111 Configured,
1112 Resolved,
1113 Exposed,
1114 Invoked,
1115 EffectRan,
1116}
1117
1118#[derive(Debug, Clone, Serialize, Deserialize)]
1122#[cfg_attr(feature = "openapi", derive(ToSchema))]
1123pub struct CapabilityUsageRecord {
1124 pub capability_id: String,
1126 #[serde(default, skip_serializing_if = "Option::is_none")]
1128 pub capability_name: Option<String>,
1129 pub usage_kind: CapabilityUsageKind,
1131 #[serde(default, skip_serializing_if = "Option::is_none")]
1133 pub tool_name: Option<String>,
1134 #[serde(default, skip_serializing_if = "Option::is_none")]
1136 pub usage_count: Option<u64>,
1137 #[serde(default, skip_serializing_if = "Option::is_none")]
1139 pub duration_ms: Option<u64>,
1140}
1141
1142#[derive(Debug, Clone, Serialize, Deserialize)]
1144#[cfg_attr(feature = "openapi", derive(ToSchema))]
1145pub struct CapabilityUsageData {
1146 pub records: Vec<CapabilityUsageRecord>,
1147}
1148
1149#[derive(Debug, Clone, Serialize, Deserialize)]
1151#[cfg_attr(feature = "openapi", derive(ToSchema))]
1152pub struct ToolCallSummary {
1153 pub id: String,
1154 pub name: String,
1155 #[serde(default, skip_serializing_if = "Option::is_none")]
1157 pub display_name: Option<String>,
1158 #[serde(default, skip_serializing_if = "Option::is_none")]
1160 pub narration: Option<String>,
1161 #[serde(default, skip_serializing_if = "Option::is_none")]
1163 pub completed_narration: Option<String>,
1164}
1165
1166impl From<&ToolCall> for ToolCallSummary {
1167 fn from(tc: &ToolCall) -> Self {
1168 Self {
1169 id: tc.id.clone(),
1170 name: tc.name.clone(),
1171 display_name: None,
1172 narration: None,
1173 completed_narration: None,
1174 }
1175 }
1176}
1177
1178#[derive(Debug, Clone, Serialize, Deserialize)]
1180#[cfg_attr(feature = "openapi", derive(ToSchema))]
1181pub struct ToolDefinitionSummary {
1182 pub name: String,
1184 #[serde(default, skip_serializing_if = "Option::is_none")]
1186 pub display_name: Option<String>,
1187 #[serde(default, skip_serializing_if = "Option::is_none")]
1189 pub category: Option<String>,
1190 #[serde(default, skip_serializing_if = "Option::is_none")]
1192 pub capability_id: Option<String>,
1193 #[serde(default, skip_serializing_if = "Option::is_none")]
1195 pub capability_name: Option<String>,
1196 pub description: String,
1198}
1199
1200impl From<&crate::tool_types::ToolDefinition> for ToolDefinitionSummary {
1201 fn from(tool: &crate::tool_types::ToolDefinition) -> Self {
1202 let capability_attribution = tool.capability_attribution();
1203 Self {
1204 name: tool.name().to_string(),
1205 display_name: tool.display_name().map(|s| s.to_string()),
1206 category: tool.category().map(|s| s.to_string()),
1207 capability_id: capability_attribution.map(|(id, _)| id.to_string()),
1208 capability_name: capability_attribution.and_then(|(_, name)| name.map(str::to_string)),
1209 description: tool.description().to_string(),
1210 }
1211 }
1212}
1213
1214#[derive(Debug, Clone, Serialize, Deserialize)]
1216#[cfg_attr(feature = "openapi", derive(ToSchema))]
1217pub struct ActStartedData {
1218 pub tool_calls: Vec<ToolCallSummary>,
1220 #[serde(default, skip_serializing_if = "Option::is_none")]
1222 pub headline: Option<String>,
1223}
1224
1225impl ActStartedData {
1226 pub fn new(tool_calls: &[ToolCall]) -> Self {
1227 Self::new_with_locale(tool_calls, None)
1228 }
1229
1230 pub fn new_with_locale(tool_calls: &[ToolCall], locale: Option<&str>) -> Self {
1231 Self {
1232 tool_calls: tool_calls.iter().map(ToolCallSummary::from).collect(),
1233 headline: render_group_headline_with_locale(
1234 tool_calls,
1235 &[],
1236 ToolNarrationPhase::Started,
1237 locale,
1238 ),
1239 }
1240 }
1241
1242 pub fn with_definitions(
1244 tool_calls: &[ToolCall],
1245 tool_defs: &[crate::tool_types::ToolDefinition],
1246 ) -> Self {
1247 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
1248 }
1249
1250 pub fn with_definitions_and_locale(
1251 tool_calls: &[ToolCall],
1252 tool_defs: &[crate::tool_types::ToolDefinition],
1253 locale: Option<&str>,
1254 ) -> Self {
1255 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
1256 tool_defs.iter().map(|d| (d.name(), d)).collect();
1257 Self {
1258 tool_calls: tool_calls
1259 .iter()
1260 .map(|tc| {
1261 let tool_def = def_map.get(tc.name.as_str()).copied();
1262 let display_name = localized_tool_display_name(
1263 &tc.name,
1264 tool_def.and_then(|d| d.display_name()),
1265 locale,
1266 );
1267 ToolCallSummary {
1268 id: tc.id.clone(),
1269 name: tc.name.clone(),
1270 display_name,
1271 narration: Some(render_tool_narration_with_locale(
1272 tool_def,
1273 tc,
1274 ToolNarrationPhase::Started,
1275 locale,
1276 )),
1277 completed_narration: Some(render_tool_narration_with_locale(
1278 tool_def,
1279 tc,
1280 ToolNarrationPhase::Completed,
1281 locale,
1282 )),
1283 }
1284 })
1285 .collect(),
1286 headline: render_group_headline_with_locale(
1287 tool_calls,
1288 tool_defs,
1289 ToolNarrationPhase::Started,
1290 locale,
1291 ),
1292 }
1293 }
1294}
1295
1296#[derive(Debug, Clone, Serialize, Deserialize)]
1298#[cfg_attr(feature = "openapi", derive(ToSchema))]
1299pub struct ActCompletedData {
1300 pub completed: bool,
1302
1303 pub success_count: u32,
1305
1306 pub error_count: u32,
1308
1309 #[serde(skip_serializing_if = "Option::is_none")]
1311 pub duration_ms: Option<u64>,
1312 #[serde(default, skip_serializing_if = "Option::is_none")]
1314 pub headline: Option<String>,
1315}
1316
1317#[derive(Debug, Clone, Serialize, Deserialize)]
1319#[cfg_attr(feature = "openapi", derive(ToSchema))]
1320pub struct ToolStartedData {
1321 pub tool_call: ToolCall,
1323 #[serde(default, skip_serializing_if = "Option::is_none")]
1325 pub tool_call_fingerprint: Option<String>,
1326 #[serde(default, skip_serializing_if = "Option::is_none")]
1328 pub display_name: Option<String>,
1329 #[serde(default, skip_serializing_if = "Option::is_none")]
1331 pub narration: Option<String>,
1332}
1333
1334#[derive(Debug, Clone, Serialize, Deserialize)]
1336#[cfg_attr(feature = "openapi", derive(ToSchema))]
1337pub struct ToolCompletedData {
1338 pub tool_call_id: String,
1340
1341 pub tool_name: String,
1343
1344 #[serde(default, skip_serializing_if = "Option::is_none")]
1346 pub tool_call_fingerprint: Option<String>,
1347
1348 #[serde(default, skip_serializing_if = "Option::is_none")]
1350 pub tool_result_fingerprint: Option<String>,
1351
1352 #[serde(default, skip_serializing_if = "Option::is_none")]
1354 pub display_name: Option<String>,
1355
1356 pub success: bool,
1358
1359 pub status: String,
1361
1362 #[serde(skip_serializing_if = "Option::is_none")]
1364 pub result: Option<Vec<ContentPart>>,
1365
1366 #[serde(skip_serializing_if = "Option::is_none")]
1368 pub error: Option<String>,
1369
1370 #[serde(skip_serializing_if = "Option::is_none")]
1372 pub duration_ms: Option<u64>,
1373
1374 #[serde(default, skip_serializing_if = "Option::is_none")]
1376 pub capability_id: Option<String>,
1377
1378 #[serde(default, skip_serializing_if = "Option::is_none")]
1380 pub capability_name: Option<String>,
1381
1382 #[serde(default, skip_serializing_if = "Option::is_none")]
1384 pub narration: Option<String>,
1385}
1386
1387impl ToolCompletedData {
1388 pub fn success(
1389 tool_call_id: String,
1390 tool_name: String,
1391 result: Vec<ContentPart>,
1392 duration_ms: Option<u64>,
1393 ) -> Self {
1394 Self {
1395 tool_call_id,
1396 tool_name,
1397 tool_call_fingerprint: None,
1398 tool_result_fingerprint: None,
1399 display_name: None,
1400 success: true,
1401 status: "success".to_string(),
1402 result: Some(result),
1403 error: None,
1404 duration_ms,
1405 capability_id: None,
1406 capability_name: None,
1407 narration: None,
1408 }
1409 }
1410
1411 pub fn failure(
1412 tool_call_id: String,
1413 tool_name: String,
1414 status: String,
1415 error: String,
1416 duration_ms: Option<u64>,
1417 ) -> Self {
1418 Self {
1419 tool_call_id,
1420 tool_name,
1421 tool_call_fingerprint: None,
1422 tool_result_fingerprint: None,
1423 display_name: None,
1424 success: false,
1425 status,
1426 result: None,
1427 error: Some(error),
1428 duration_ms,
1429 capability_id: None,
1430 capability_name: None,
1431 narration: None,
1432 }
1433 }
1434
1435 pub fn with_display_name(mut self, display_name: Option<String>) -> Self {
1437 self.display_name = display_name;
1438 self
1439 }
1440
1441 pub fn with_fingerprints(
1442 mut self,
1443 tool_call_fingerprint: String,
1444 tool_result_fingerprint: String,
1445 ) -> Self {
1446 self.tool_call_fingerprint = Some(tool_call_fingerprint);
1447 self.tool_result_fingerprint = Some(tool_result_fingerprint);
1448 self
1449 }
1450
1451 pub fn with_narration(mut self, narration: Option<String>) -> Self {
1453 self.narration = narration;
1454 self
1455 }
1456
1457 pub fn with_capability_attribution(
1459 mut self,
1460 capability_id: Option<String>,
1461 capability_name: Option<String>,
1462 ) -> Self {
1463 self.capability_id = capability_id;
1464 self.capability_name = capability_name;
1465 self
1466 }
1467}
1468
1469#[derive(Debug, Clone, Serialize, Deserialize)]
1475#[cfg_attr(feature = "openapi", derive(ToSchema))]
1476pub struct ToolProgressData {
1477 pub tool_call_id: String,
1479
1480 pub tool_name: String,
1482
1483 pub message: String,
1485
1486 #[serde(default, skip_serializing_if = "Option::is_none")]
1488 pub display_name: Option<String>,
1489}
1490
1491#[derive(Debug, Clone, Serialize, Deserialize)]
1501#[cfg_attr(feature = "openapi", derive(ToSchema))]
1502pub struct ToolOutputDeltaData {
1503 pub tool_call_id: String,
1505
1506 pub tool_name: String,
1508
1509 pub delta: String,
1511
1512 pub stream: String,
1514}
1515
1516#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1518#[cfg_attr(feature = "openapi", derive(ToSchema))]
1519#[serde(rename_all = "snake_case")]
1520pub enum TranscriptRepairAction {
1521 Replay,
1523 Synthesize,
1525}
1526
1527#[derive(Debug, Clone, Serialize, Deserialize)]
1533#[cfg_attr(feature = "openapi", derive(ToSchema))]
1534pub struct TranscriptRepairedData {
1535 pub tool_call_id: String,
1537
1538 #[serde(default, skip_serializing_if = "Option::is_none")]
1540 pub tool_name: Option<String>,
1541
1542 pub action: TranscriptRepairAction,
1544}
1545
1546#[derive(Debug, Clone, Serialize, Deserialize)]
1552#[cfg_attr(feature = "openapi", derive(ToSchema))]
1553pub struct ToolCallRepairedData {
1554 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
1556 pub turn_id: TurnId,
1557
1558 pub tool_call_id: String,
1560
1561 pub tool_name: String,
1563
1564 pub outcome: String,
1566}
1567
1568#[derive(Debug, Clone, Serialize, Deserialize)]
1573#[cfg_attr(feature = "openapi", derive(ToSchema))]
1574pub struct ToolCallRequestedData {
1575 pub tool_calls: Vec<ToolCall>,
1577 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1579 pub tool_summaries: Vec<ToolCallSummary>,
1580 #[serde(default, skip_serializing_if = "Option::is_none")]
1582 pub headline: Option<String>,
1583 #[serde(default, skip_serializing_if = "Option::is_none")]
1585 pub completed_headline: Option<String>,
1586}
1587
1588impl ToolCallRequestedData {
1589 pub fn with_definitions(
1590 tool_calls: &[ToolCall],
1591 tool_defs: &[crate::tool_types::ToolDefinition],
1592 ) -> Self {
1593 Self::with_definitions_and_locale(tool_calls, tool_defs, None)
1594 }
1595
1596 pub fn with_definitions_and_locale(
1597 tool_calls: &[ToolCall],
1598 tool_defs: &[crate::tool_types::ToolDefinition],
1599 locale: Option<&str>,
1600 ) -> Self {
1601 let def_map: std::collections::HashMap<&str, &crate::tool_types::ToolDefinition> =
1602 tool_defs.iter().map(|d| (d.name(), d)).collect();
1603
1604 let tool_summaries = tool_calls
1605 .iter()
1606 .map(|tool_call| {
1607 let tool_def = def_map.get(tool_call.name.as_str()).copied();
1608 ToolCallSummary {
1609 id: tool_call.id.clone(),
1610 name: tool_call.name.clone(),
1611 display_name: localized_tool_display_name(
1612 &tool_call.name,
1613 tool_def.and_then(|def| def.display_name()),
1614 locale,
1615 ),
1616 narration: Some(render_tool_narration_with_locale(
1617 tool_def,
1618 tool_call,
1619 ToolNarrationPhase::Waiting,
1620 locale,
1621 )),
1622 completed_narration: Some(render_tool_narration_with_locale(
1623 tool_def,
1624 tool_call,
1625 ToolNarrationPhase::Completed,
1626 locale,
1627 )),
1628 }
1629 })
1630 .collect();
1631
1632 Self {
1633 tool_calls: tool_calls.to_vec(),
1634 tool_summaries,
1635 headline: render_group_headline_with_locale(
1636 tool_calls,
1637 tool_defs,
1638 ToolNarrationPhase::Waiting,
1639 locale,
1640 ),
1641 completed_headline: render_group_headline_with_locale(
1642 tool_calls,
1643 tool_defs,
1644 ToolNarrationPhase::Completed,
1645 locale,
1646 ),
1647 }
1648 }
1649}
1650
1651#[derive(Debug, Clone, Serialize, Deserialize)]
1657#[cfg_attr(feature = "openapi", derive(ToSchema))]
1658pub struct LlmGenerationOutput {
1659 #[serde(skip_serializing_if = "Option::is_none")]
1661 pub text: Option<String>,
1662
1663 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1665 pub tool_calls: Vec<ToolCall>,
1666}
1667
1668#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1673#[cfg_attr(feature = "openapi", derive(ToSchema))]
1674pub struct LlmRequestOptions {
1675 #[serde(default, skip_serializing_if = "Option::is_none")]
1677 pub temperature: Option<f32>,
1678 #[serde(default, skip_serializing_if = "Option::is_none")]
1680 pub max_tokens: Option<u32>,
1681 #[serde(default, skip_serializing_if = "Option::is_none")]
1684 pub reasoning_effort: Option<String>,
1685 #[serde(default, skip_serializing_if = "Option::is_none")]
1687 pub stream: Option<bool>,
1688 #[serde(skip_serializing_if = "Option::is_none")]
1690 pub prompt_cache: Option<LlmPromptCacheInfo>,
1691 #[serde(skip_serializing_if = "Option::is_none")]
1693 pub tool_search: Option<LlmToolSearchInfo>,
1694 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1696 pub provider_options: HashMap<String, Value>,
1697 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1700 pub metadata: HashMap<String, String>,
1701}
1702
1703impl LlmRequestOptions {
1704 pub fn is_empty(&self) -> bool {
1705 self.temperature.is_none()
1706 && self.max_tokens.is_none()
1707 && self.reasoning_effort.is_none()
1708 && self.stream.is_none()
1709 && self.prompt_cache.is_none()
1710 && self.tool_search.is_none()
1711 && self.provider_options.is_empty()
1712 && self.metadata.is_empty()
1713 }
1714}
1715
1716#[derive(Debug, Clone, Serialize, Deserialize)]
1718#[cfg_attr(feature = "openapi", derive(ToSchema))]
1719pub struct LlmPromptCacheInfo {
1720 pub enabled: bool,
1722 pub strategy: crate::driver_registry::PromptCacheStrategy,
1724 #[serde(skip_serializing_if = "Option::is_none")]
1726 pub provider_mode: Option<String>,
1727}
1728
1729#[derive(Debug, Clone, Serialize, Deserialize)]
1731#[cfg_attr(feature = "openapi", derive(ToSchema))]
1732pub struct LlmToolSearchInfo {
1733 pub enabled: bool,
1735 pub threshold: usize,
1737}
1738
1739#[derive(Debug, Clone, Serialize, Deserialize)]
1741#[cfg_attr(feature = "openapi", derive(ToSchema))]
1742pub struct LlmGenerationMetadata {
1743 #[cfg_attr(feature = "openapi", schema(example = "claude-sonnet-4-5"))]
1745 pub model: String,
1746
1747 #[serde(skip_serializing_if = "Option::is_none")]
1749 #[cfg_attr(feature = "openapi", schema(example = "anthropic"))]
1750 pub provider: Option<String>,
1751
1752 #[serde(skip_serializing_if = "Option::is_none")]
1754 pub usage: Option<TokenUsage>,
1755
1756 #[serde(skip_serializing_if = "Option::is_none")]
1758 #[cfg_attr(feature = "openapi", schema(example = 1_842u64))]
1759 pub duration_ms: Option<u64>,
1760
1761 #[serde(skip_serializing_if = "Option::is_none")]
1763 #[cfg_attr(feature = "openapi", schema(example = 312u64))]
1764 pub time_to_first_token_ms: Option<u64>,
1765
1766 #[cfg_attr(feature = "openapi", schema(example = true))]
1768 pub success: bool,
1769
1770 #[serde(skip_serializing_if = "Option::is_none")]
1772 #[cfg_attr(feature = "openapi", schema(example = "provider returned 503"))]
1773 pub error: Option<String>,
1774
1775 #[serde(skip_serializing_if = "Option::is_none")]
1778 #[cfg_attr(feature = "openapi", schema(example = json!(["tool_calls"])))]
1779 pub finish_reasons: Option<Vec<String>>,
1780
1781 #[serde(skip_serializing_if = "Option::is_none")]
1784 #[cfg_attr(feature = "openapi", schema(example = "msg_01ABCDef0123456789"))]
1785 pub response_id: Option<String>,
1786
1787 #[serde(skip_serializing_if = "Option::is_none")]
1790 pub retry: Option<LlmRetryInfo>,
1791
1792 #[serde(skip_serializing_if = "Option::is_none")]
1795 pub compaction: Option<LlmCompactionInfo>,
1796
1797 #[serde(skip_serializing_if = "Option::is_none")]
1799 pub request_options: Option<LlmRequestOptions>,
1800}
1801
1802#[derive(Debug, Clone, Serialize, Deserialize)]
1804#[cfg_attr(feature = "openapi", derive(ToSchema))]
1805pub struct LlmRetryInfo {
1806 pub attempts: u32,
1808
1809 pub total_wait_ms: u64,
1811}
1812
1813#[derive(Debug, Clone, Serialize, Deserialize)]
1818#[cfg_attr(feature = "openapi", derive(ToSchema))]
1819pub struct LlmCompactionInfo {
1820 pub compacted: bool,
1822
1823 #[serde(skip_serializing_if = "Option::is_none")]
1825 pub input_tokens_before: Option<u32>,
1826
1827 #[serde(skip_serializing_if = "Option::is_none")]
1829 pub input_tokens_after: Option<u32>,
1830
1831 #[serde(skip_serializing_if = "Option::is_none")]
1833 pub duration_ms: Option<u64>,
1834
1835 #[serde(skip_serializing_if = "Option::is_none")]
1842 pub cost_usd: Option<f64>,
1843}
1844
1845impl LlmCompactionInfo {
1846 pub fn new(
1848 input_tokens_before: Option<u32>,
1849 input_tokens_after: Option<u32>,
1850 duration_ms: Option<u64>,
1851 cost_usd: Option<f64>,
1852 ) -> Self {
1853 Self {
1854 compacted: true,
1855 input_tokens_before,
1856 input_tokens_after,
1857 duration_ms,
1858 cost_usd,
1859 }
1860 }
1861}
1862
1863#[derive(Debug, Clone, Serialize, Deserialize)]
1868#[cfg_attr(feature = "openapi", derive(ToSchema))]
1869pub struct LlmGenerationData {
1870 pub messages: Vec<Message>,
1872
1873 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1875 pub tools: Vec<ToolDefinitionSummary>,
1876
1877 pub output: LlmGenerationOutput,
1879
1880 pub metadata: LlmGenerationMetadata,
1882}
1883
1884impl LlmGenerationData {
1885 #[allow(clippy::too_many_arguments)]
1887 pub fn success(
1888 messages: Vec<Message>,
1889 tools: Vec<ToolDefinitionSummary>,
1890 text: Option<String>,
1891 tool_calls: Vec<ToolCall>,
1892 model: String,
1893 provider: Option<String>,
1894 usage: Option<TokenUsage>,
1895 duration_ms: Option<u64>,
1896 time_to_first_token_ms: Option<u64>,
1897 ) -> Self {
1898 let finish_reasons = if !tool_calls.is_empty() {
1900 Some(vec!["tool_calls".to_string()])
1901 } else {
1902 Some(vec!["stop".to_string()])
1903 };
1904
1905 Self {
1906 messages,
1907 tools,
1908 output: LlmGenerationOutput { text, tool_calls },
1909 metadata: LlmGenerationMetadata {
1910 model,
1911 provider,
1912 usage,
1913 duration_ms,
1914 time_to_first_token_ms,
1915 success: true,
1916 error: None,
1917 finish_reasons,
1918 response_id: None,
1919 retry: None,
1920 compaction: None,
1921 request_options: None,
1922 },
1923 }
1924 }
1925
1926 #[allow(clippy::too_many_arguments)]
1928 pub fn success_with_metadata(
1929 messages: Vec<Message>,
1930 tools: Vec<ToolDefinitionSummary>,
1931 text: Option<String>,
1932 tool_calls: Vec<ToolCall>,
1933 model: String,
1934 provider: Option<String>,
1935 usage: Option<TokenUsage>,
1936 duration_ms: Option<u64>,
1937 time_to_first_token_ms: Option<u64>,
1938 finish_reasons: Option<Vec<String>>,
1939 response_id: Option<String>,
1940 ) -> Self {
1941 Self {
1942 messages,
1943 tools,
1944 output: LlmGenerationOutput { text, tool_calls },
1945 metadata: LlmGenerationMetadata {
1946 model,
1947 provider,
1948 usage,
1949 duration_ms,
1950 time_to_first_token_ms,
1951 success: true,
1952 error: None,
1953 finish_reasons,
1954 response_id,
1955 retry: None,
1956 compaction: None,
1957 request_options: None,
1958 },
1959 }
1960 }
1961
1962 #[allow(clippy::too_many_arguments)]
1964 pub fn success_with_retry(
1965 messages: Vec<Message>,
1966 tools: Vec<ToolDefinitionSummary>,
1967 text: Option<String>,
1968 tool_calls: Vec<ToolCall>,
1969 model: String,
1970 provider: Option<String>,
1971 usage: Option<TokenUsage>,
1972 duration_ms: Option<u64>,
1973 time_to_first_token_ms: Option<u64>,
1974 finish_reasons: Option<Vec<String>>,
1975 response_id: Option<String>,
1976 retry: Option<LlmRetryInfo>,
1977 ) -> Self {
1978 Self {
1979 messages,
1980 tools,
1981 output: LlmGenerationOutput { text, tool_calls },
1982 metadata: LlmGenerationMetadata {
1983 model,
1984 provider,
1985 usage,
1986 duration_ms,
1987 time_to_first_token_ms,
1988 success: true,
1989 error: None,
1990 finish_reasons,
1991 response_id,
1992 retry,
1993 compaction: None,
1994 request_options: None,
1995 },
1996 }
1997 }
1998
1999 pub fn failure(
2001 messages: Vec<Message>,
2002 tools: Vec<ToolDefinitionSummary>,
2003 model: String,
2004 provider: Option<String>,
2005 error: String,
2006 duration_ms: Option<u64>,
2007 time_to_first_token_ms: Option<u64>,
2008 ) -> Self {
2009 Self {
2010 messages,
2011 tools,
2012 output: LlmGenerationOutput {
2013 text: None,
2014 tool_calls: vec![],
2015 },
2016 metadata: LlmGenerationMetadata {
2017 model,
2018 provider,
2019 usage: None,
2020 duration_ms,
2021 time_to_first_token_ms,
2022 success: false,
2023 error: Some(error),
2024 finish_reasons: Some(vec!["error".to_string()]),
2025 response_id: None,
2026 retry: None,
2027 compaction: None,
2028 request_options: None,
2029 },
2030 }
2031 }
2032
2033 pub fn with_compaction(mut self, compaction: LlmCompactionInfo) -> Self {
2037 self.metadata.compaction = Some(compaction);
2038 self
2039 }
2040
2041 pub fn with_retry(mut self, retry: LlmRetryInfo) -> Self {
2043 self.metadata.retry = Some(retry);
2044 self
2045 }
2046
2047 pub fn with_request_options(mut self, request_options: LlmRequestOptions) -> Self {
2049 if !request_options.is_empty() {
2050 self.metadata.request_options = Some(request_options);
2051 }
2052 self
2053 }
2054}
2055
2056#[derive(Debug, Clone, Serialize, Deserialize)]
2066#[cfg_attr(feature = "openapi", derive(ToSchema))]
2067pub struct ReasonThinkingStartedData {
2068 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2070 pub turn_id: TurnId,
2071
2072 #[serde(skip_serializing_if = "Option::is_none")]
2074 pub model: Option<String>,
2075}
2076
2077#[derive(Debug, Clone, Serialize, Deserialize)]
2083#[cfg_attr(feature = "openapi", derive(ToSchema))]
2084pub struct ReasonThinkingDeltaData {
2085 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2087 pub turn_id: TurnId,
2088
2089 pub delta: String,
2091
2092 pub accumulated: String,
2094}
2095
2096#[derive(Debug, Clone, Serialize, Deserialize)]
2101#[cfg_attr(feature = "openapi", derive(ToSchema))]
2102pub struct ReasonThinkingCompletedData {
2103 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2105 pub turn_id: TurnId,
2106
2107 pub thinking: String,
2109}
2110
2111#[derive(Debug, Clone, Serialize, Deserialize)]
2121#[cfg_attr(feature = "openapi", derive(ToSchema))]
2122pub struct ReasonItemData {
2123 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2125 pub turn_id: TurnId,
2126
2127 pub provider: String,
2129
2130 #[serde(skip_serializing_if = "Option::is_none")]
2132 pub model: Option<String>,
2133
2134 pub item_id: String,
2136
2137 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2140 pub summary: Vec<String>,
2141
2142 #[serde(skip_serializing_if = "Option::is_none")]
2144 pub token_count: Option<u32>,
2145}
2146
2147#[derive(Debug, Clone, Serialize, Deserialize)]
2153#[cfg_attr(feature = "openapi", derive(ToSchema))]
2154pub struct TurnStartedData {
2155 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2157 pub turn_id: TurnId,
2158
2159 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
2161 pub input_message_id: MessageId,
2162
2163 #[serde(skip_serializing_if = "Option::is_none")]
2165 pub input_content: Option<String>,
2166
2167 #[serde(default, skip_serializing_if = "Option::is_none")]
2172 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agent_01933b5a00007000800000000000001"))]
2173 pub agent_id: Option<AgentId>,
2174
2175 #[serde(default, skip_serializing_if = "Option::is_none")]
2177 pub agent_name: Option<String>,
2178
2179 #[serde(default, skip_serializing_if = "Option::is_none")]
2181 pub agent_description: Option<String>,
2182}
2183
2184#[derive(Debug, Clone, Serialize, Deserialize)]
2186#[cfg_attr(feature = "openapi", derive(ToSchema))]
2187pub struct TurnCompletedData {
2188 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2190 pub turn_id: TurnId,
2191
2192 pub iterations: u32,
2194
2195 #[serde(skip_serializing_if = "Option::is_none")]
2197 pub duration_ms: Option<u64>,
2198
2199 #[serde(skip_serializing_if = "Option::is_none")]
2201 pub usage: Option<TokenUsage>,
2202
2203 #[serde(skip_serializing_if = "Option::is_none")]
2205 pub input_content: Option<String>,
2206
2207 #[serde(skip_serializing_if = "Option::is_none")]
2209 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "message_01933b5a00007000800000000000001"))]
2210 pub final_message_id: Option<MessageId>,
2211
2212 #[serde(skip_serializing_if = "Option::is_none")]
2214 pub final_answer_preview: Option<String>,
2215
2216 #[serde(skip_serializing_if = "Option::is_none")]
2218 pub time_to_first_token_ms: Option<u64>,
2219
2220 #[serde(skip_serializing_if = "Option::is_none")]
2222 pub tool_call_count: Option<u32>,
2223
2224 #[serde(skip_serializing_if = "Option::is_none")]
2226 pub llm_call_count: Option<u32>,
2227
2228 #[serde(skip_serializing_if = "Option::is_none")]
2230 pub status: Option<String>,
2231}
2232
2233#[derive(Debug, Clone, Serialize, Deserialize)]
2235#[cfg_attr(feature = "openapi", derive(ToSchema))]
2236pub struct TurnFailedData {
2237 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2239 pub turn_id: TurnId,
2240
2241 pub error: String,
2243
2244 #[serde(default, skip_serializing_if = "Option::is_none")]
2246 pub error_code: Option<String>,
2247
2248 #[serde(default, skip_serializing_if = "Option::is_none")]
2250 #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
2251 pub error_fields: Option<UserFacingErrorFields>,
2252
2253 #[serde(default, skip_serializing_if = "Option::is_none")]
2257 pub error_disclosure: Option<String>,
2258}
2259
2260#[derive(Debug, Clone, Serialize, Deserialize)]
2267#[cfg_attr(feature = "openapi", derive(ToSchema))]
2268pub struct TurnSealedData {
2269 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2271 pub turn_id: TurnId,
2272
2273 pub reason: String,
2276
2277 #[serde(default, skip_serializing_if = "Option::is_none")]
2279 pub detail: Option<String>,
2280
2281 #[serde(default, skip_serializing_if = "Option::is_none")]
2283 pub iterations: Option<u32>,
2284
2285 #[serde(default, skip_serializing_if = "Option::is_none")]
2287 pub usage: Option<TokenUsage>,
2288}
2289
2290#[derive(Debug, Clone, Serialize, Deserialize)]
2292#[cfg_attr(feature = "openapi", derive(ToSchema))]
2293pub struct TurnCancelledData {
2294 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2296 pub turn_id: TurnId,
2297
2298 #[serde(skip_serializing_if = "Option::is_none")]
2300 pub reason: Option<String>,
2301
2302 #[serde(skip_serializing_if = "Option::is_none")]
2304 pub usage: Option<TokenUsage>,
2305}
2306
2307#[derive(Debug, Clone, Serialize, Deserialize)]
2313#[cfg_attr(feature = "openapi", derive(ToSchema))]
2314pub struct SessionStartedData {
2315 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "harness_01933b5a00007000800000000000001"))]
2317 pub harness_id: HarnessId,
2318
2319 #[serde(skip_serializing_if = "Option::is_none")]
2321 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "agent_01933b5a00007000800000000000001"))]
2322 pub agent_id: Option<AgentId>,
2323
2324 #[serde(skip_serializing_if = "Option::is_none")]
2326 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
2327 pub model_id: Option<ModelId>,
2328}
2329
2330#[derive(Debug, Clone, Serialize, Deserialize)]
2332#[cfg_attr(feature = "openapi", derive(ToSchema))]
2333pub struct SessionActivatedData {
2334 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2336 pub turn_id: TurnId,
2337
2338 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "message_01933b5a00007000800000000000001"))]
2340 pub input_message_id: MessageId,
2341}
2342
2343#[derive(Debug, Clone, Serialize, Deserialize)]
2345#[cfg_attr(feature = "openapi", derive(ToSchema))]
2346pub struct SessionIdledData {
2347 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "turn_01933b5a00007000800000000000001"))]
2349 pub turn_id: TurnId,
2350
2351 #[serde(skip_serializing_if = "Option::is_none")]
2353 pub iterations: Option<u32>,
2354
2355 #[serde(skip_serializing_if = "Option::is_none")]
2357 pub usage: Option<TokenUsage>,
2358}
2359
2360#[derive(Debug, Clone, Serialize, Deserialize)]
2362#[cfg_attr(feature = "openapi", derive(ToSchema))]
2363pub struct SessionTitleUpdatedData {
2364 pub previous_title: Option<String>,
2366
2367 pub title: String,
2369}
2370
2371#[derive(Debug, Clone, Serialize, Deserialize)]
2377#[cfg_attr(feature = "openapi", derive(ToSchema))]
2378pub struct SessionModelChangedData {
2379 #[serde(default, skip_serializing_if = "Option::is_none")]
2382 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, example = "model_01933b5a00007000800000000000001"))]
2383 pub previous_model_id: Option<ModelId>,
2384
2385 #[serde(default, skip_serializing_if = "Option::is_none")]
2387 pub previous_model_name: Option<String>,
2388
2389 #[cfg_attr(feature = "openapi", schema(value_type = String, example = "model_01933b5a00007000800000000000002"))]
2391 pub model_id: ModelId,
2392
2393 pub model_name: String,
2395}
2396
2397#[derive(Debug, Clone, Serialize, Deserialize)]
2406#[cfg_attr(feature = "openapi", derive(ToSchema))]
2407pub struct SessionTaskEventData {
2408 pub task: crate::session_task::SessionTask,
2409}
2410
2411#[derive(Debug, Clone, Serialize, Deserialize)]
2413#[cfg_attr(feature = "openapi", derive(ToSchema))]
2414pub struct TaskMessageEventData {
2415 pub task_id: String,
2416 pub message: crate::session_task::TaskMessage,
2417}
2418
2419#[derive(Debug, Clone, Serialize, Deserialize)]
2425#[cfg_attr(feature = "openapi", derive(ToSchema))]
2426#[serde(rename_all = "snake_case")]
2427pub enum CompactionReason {
2428 ProactiveBudget,
2430 RequestTooLarge,
2432 Manual,
2434}
2435
2436impl std::fmt::Display for CompactionReason {
2437 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2438 match self {
2439 Self::ProactiveBudget => write!(f, "proactive_budget"),
2440 Self::RequestTooLarge => write!(f, "request_too_large"),
2441 Self::Manual => write!(f, "manual"),
2442 }
2443 }
2444}
2445
2446#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
2448#[cfg_attr(feature = "openapi", derive(ToSchema))]
2449#[serde(rename_all = "snake_case")]
2450pub enum CompactionTrigger {
2451 #[default]
2453 ContextBudget,
2454 CostPressure,
2456}
2457
2458#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2460#[cfg_attr(feature = "openapi", derive(ToSchema))]
2461#[serde(rename_all = "snake_case")]
2462pub enum CompactionSkipReason {
2463 StrategyExcludesNative,
2465 DriverUnsupported,
2467 CheckpointStoreUnavailable,
2469 CooldownActive,
2471 NativeReturnedNone,
2473 NoMaterialReduction,
2475 GuardRejected,
2477}
2478
2479#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2481#[cfg_attr(feature = "openapi", derive(ToSchema))]
2482#[serde(rename_all = "snake_case")]
2483pub enum CompactionFailStage {
2484 NativeCompaction,
2486 CheckpointInstall,
2488 Summarization,
2490}
2491
2492#[derive(Debug, Clone, Serialize, Deserialize)]
2499#[cfg_attr(feature = "openapi", derive(ToSchema))]
2500pub struct ContextCompactionSkippedData {
2501 #[cfg_attr(feature = "openapi", schema(example = "proactive_budget"))]
2503 pub reason: CompactionReason,
2504 #[cfg_attr(feature = "openapi", schema(example = "context_budget"))]
2506 pub trigger: CompactionTrigger,
2507 #[cfg_attr(feature = "openapi", schema(example = "cooldown_active"))]
2509 pub skip_reason: CompactionSkipReason,
2510 #[cfg_attr(feature = "openapi", schema(example = "summary_then_trim"))]
2512 pub strategy: String,
2513 #[cfg_attr(feature = "openapi", schema(example = "gpt-5-mini"))]
2515 pub model: String,
2516 #[serde(default, skip_serializing_if = "Option::is_none")]
2518 #[cfg_attr(feature = "openapi", schema(example = "openai"))]
2519 pub provider: Option<String>,
2520 #[serde(default, skip_serializing_if = "Option::is_none")]
2522 #[cfg_attr(feature = "openapi", schema(example = "openai-chat"))]
2523 pub driver: Option<String>,
2524 #[cfg_attr(feature = "openapi", schema(example = 184320))]
2526 pub tokens_observed: u64,
2527 #[serde(default, skip_serializing_if = "Option::is_none")]
2529 #[cfg_attr(feature = "openapi", schema(example = 8192))]
2530 pub budget_remaining_tokens: Option<u64>,
2531 #[serde(default, skip_serializing_if = "Option::is_none")]
2533 #[cfg_attr(feature = "openapi", schema(example = 481))]
2534 pub source_sequence: Option<i64>,
2535 #[cfg_attr(feature = "openapi", schema(example = 120))]
2537 pub messages_observed: usize,
2538}
2539
2540#[derive(Debug, Clone, Serialize, Deserialize)]
2545#[cfg_attr(feature = "openapi", derive(ToSchema))]
2546pub struct ContextCompactionFailedData {
2547 #[cfg_attr(feature = "openapi", schema(example = "proactive_budget"))]
2549 pub reason: CompactionReason,
2550 #[cfg_attr(feature = "openapi", schema(example = "context_budget"))]
2552 pub trigger: CompactionTrigger,
2553 #[cfg_attr(feature = "openapi", schema(example = "summarization"))]
2555 pub stage: CompactionFailStage,
2556 #[cfg_attr(
2558 feature = "openapi",
2559 schema(example = "summarizer request failed: upstream timed out")
2560 )]
2561 pub error: String,
2562 #[cfg_attr(feature = "openapi", schema(example = "summary_then_trim"))]
2564 pub strategy: String,
2565 #[cfg_attr(feature = "openapi", schema(example = "gpt-5-mini"))]
2567 pub model: String,
2568 #[serde(default, skip_serializing_if = "Option::is_none")]
2570 #[cfg_attr(feature = "openapi", schema(example = "openai"))]
2571 pub provider: Option<String>,
2572 #[serde(default, skip_serializing_if = "Option::is_none")]
2574 #[cfg_attr(feature = "openapi", schema(example = "openai-chat"))]
2575 pub driver: Option<String>,
2576 #[cfg_attr(feature = "openapi", schema(example = 184320))]
2578 pub tokens_before: u64,
2579 #[serde(default, skip_serializing_if = "Option::is_none")]
2581 #[cfg_attr(feature = "openapi", schema(example = 8192))]
2582 pub budget_remaining_tokens: Option<u64>,
2583 #[serde(default, skip_serializing_if = "Option::is_none")]
2585 #[cfg_attr(feature = "openapi", schema(example = 481))]
2586 pub source_sequence: Option<i64>,
2587 #[cfg_attr(feature = "openapi", schema(example = 120))]
2589 pub messages_before: usize,
2590 #[serde(default, skip_serializing_if = "Option::is_none")]
2592 #[cfg_attr(
2593 feature = "openapi",
2594 schema(example = "01934c2f-9f2e-7c1b-8d3e-4f5a6b7c8d9e")
2595 )]
2596 pub checkpoint_id: Option<String>,
2597}
2598
2599#[derive(Debug, Clone, Serialize, Deserialize)]
2601#[cfg_attr(feature = "openapi", derive(ToSchema))]
2602pub struct ContextCompactingData {
2603 #[cfg_attr(feature = "openapi", schema(example = "proactive_budget"))]
2605 pub reason: CompactionReason,
2606 #[cfg_attr(feature = "openapi", schema(example = "summary_then_trim"))]
2608 pub strategy: String,
2609 #[cfg_attr(feature = "openapi", schema(example = 120))]
2611 pub messages_before: usize,
2612 #[serde(default, skip_serializing_if = "Option::is_none")]
2614 #[cfg_attr(feature = "openapi", schema(example = 184320))]
2615 pub tokens_before: Option<u64>,
2616 #[serde(default, skip_serializing_if = "Option::is_none")]
2618 pub bytes_before: Option<u64>,
2619 #[serde(default)]
2621 #[cfg_attr(feature = "openapi", schema(example = "context_budget"))]
2622 pub trigger: CompactionTrigger,
2623 #[serde(default)]
2625 #[cfg_attr(feature = "openapi", schema(example = "gpt-5-mini"))]
2626 pub model: String,
2627 #[serde(default, skip_serializing_if = "Option::is_none")]
2629 #[cfg_attr(feature = "openapi", schema(example = "openai"))]
2630 pub provider: Option<String>,
2631 #[serde(default, skip_serializing_if = "Option::is_none")]
2633 #[cfg_attr(feature = "openapi", schema(example = "openai-chat"))]
2634 pub driver: Option<String>,
2635 #[serde(default, skip_serializing_if = "Option::is_none")]
2637 #[cfg_attr(feature = "openapi", schema(example = 8192))]
2638 pub budget_remaining_tokens: Option<u64>,
2639 #[serde(default, skip_serializing_if = "Option::is_none")]
2641 #[cfg_attr(feature = "openapi", schema(example = 481))]
2642 pub source_sequence: Option<i64>,
2643 #[serde(default, skip_serializing_if = "Option::is_none")]
2645 #[cfg_attr(feature = "openapi", schema(example = 90210))]
2646 pub cache_read_tokens: Option<u32>,
2647 #[serde(default, skip_serializing_if = "Option::is_none")]
2649 #[cfg_attr(feature = "openapi", schema(example = 1024))]
2650 pub cache_creation_tokens: Option<u32>,
2651}
2652
2653#[derive(Debug, Clone, Serialize, Deserialize)]
2655#[cfg_attr(feature = "openapi", derive(ToSchema))]
2656pub struct CompactionStepData {
2657 pub strategy: String,
2659 pub messages_after: usize,
2661 pub duration_ms: u64,
2663}
2664
2665#[derive(Debug, Clone, Serialize, Deserialize)]
2667#[cfg_attr(feature = "openapi", derive(ToSchema))]
2668pub struct ContextCompactedData {
2669 #[serde(default, skip_serializing_if = "Option::is_none")]
2671 #[cfg_attr(
2672 feature = "openapi",
2673 schema(example = "01934c2f-9f2e-7c1b-8d3e-4f5a6b7c8d9e")
2674 )]
2675 pub checkpoint_id: Option<String>,
2676 pub strategy_used: String,
2678 #[cfg_attr(feature = "openapi", schema(example = 120))]
2680 pub messages_before: usize,
2681 pub messages_after: usize,
2683 #[serde(default, skip_serializing_if = "Option::is_none")]
2685 #[cfg_attr(feature = "openapi", schema(example = 184320))]
2686 pub tokens_before: Option<u64>,
2687 #[serde(default, skip_serializing_if = "Option::is_none")]
2689 pub tokens_after: Option<u64>,
2690 #[serde(default, skip_serializing_if = "Option::is_none")]
2692 pub bytes_before: Option<u64>,
2693 #[serde(default, skip_serializing_if = "Option::is_none")]
2695 pub bytes_after: Option<u64>,
2696 pub duration_ms: u64,
2698 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2700 pub steps: Vec<CompactionStepData>,
2701 #[serde(default)]
2703 #[cfg_attr(feature = "openapi", schema(example = "context_budget"))]
2704 pub trigger: CompactionTrigger,
2705 #[serde(default)]
2707 #[cfg_attr(feature = "openapi", schema(example = "gpt-5-mini"))]
2708 pub model: String,
2709 #[serde(default, skip_serializing_if = "Option::is_none")]
2711 #[cfg_attr(feature = "openapi", schema(example = "openai"))]
2712 pub provider: Option<String>,
2713 #[serde(default, skip_serializing_if = "Option::is_none")]
2715 #[cfg_attr(feature = "openapi", schema(example = "openai-chat"))]
2716 pub driver: Option<String>,
2717 #[serde(default, skip_serializing_if = "Option::is_none")]
2719 #[cfg_attr(feature = "openapi", schema(example = 8192))]
2720 pub budget_remaining_tokens: Option<u64>,
2721 #[serde(default, skip_serializing_if = "Option::is_none")]
2723 #[cfg_attr(feature = "openapi", schema(example = 481))]
2724 pub source_sequence: Option<i64>,
2725 #[serde(default, skip_serializing_if = "Option::is_none")]
2727 #[cfg_attr(feature = "openapi", schema(example = 90210))]
2728 pub cache_read_tokens: Option<u32>,
2729 #[serde(default, skip_serializing_if = "Option::is_none")]
2731 #[cfg_attr(feature = "openapi", schema(example = 1024))]
2732 pub cache_creation_tokens: Option<u32>,
2733}
2734
2735#[derive(Debug, Clone, Serialize, Deserialize)]
2741#[cfg_attr(feature = "openapi", derive(ToSchema))]
2742pub struct FileWrittenData {
2743 pub path: String,
2745 pub operation: String,
2747 pub size_bytes: i64,
2749 pub created: bool,
2751}
2752
2753pub const FILE_OP_CREATE: &str = "create";
2755pub const FILE_OP_UPDATE: &str = "update";
2756
2757#[derive(Debug, Clone, Serialize, Deserialize)]
2763#[cfg_attr(feature = "openapi", derive(ToSchema))]
2764pub struct BudgetEventData {
2765 pub budget_id: String,
2767 pub balance: f64,
2769 pub limit: f64,
2771 pub currency: String,
2773 #[serde(skip_serializing_if = "Option::is_none")]
2775 pub message: Option<String>,
2776 #[serde(skip_serializing_if = "Option::is_none")]
2778 pub soft_limit: Option<f64>,
2779}
2780
2781#[derive(Debug, Clone, Serialize, Deserialize)]
2787#[cfg_attr(feature = "openapi", derive(ToSchema))]
2788pub struct VoiceSessionStartedData {
2789 #[cfg_attr(
2791 feature = "openapi",
2792 schema(example = "voice_01933b5a00007000800000000000001")
2793 )]
2794 pub voice_connection_id: String,
2795 #[cfg_attr(feature = "openapi", schema(example = "gpt-realtime"))]
2797 pub model: String,
2798 #[cfg_attr(feature = "openapi", schema(example = "alloy"))]
2800 pub voice: String,
2801 #[cfg_attr(feature = "openapi", schema(example = "medium"))]
2803 pub reasoning_effort: String,
2804 #[cfg_attr(feature = "openapi", schema(example = "webrtc"))]
2806 pub transport: String,
2807}
2808
2809#[derive(Debug, Clone, Serialize, Deserialize)]
2811#[cfg_attr(feature = "openapi", derive(ToSchema))]
2812pub struct VoiceTranscriptData {
2813 pub voice_connection_id: String,
2815 #[serde(default, skip_serializing_if = "Option::is_none")]
2817 pub item_id: Option<String>,
2818 #[serde(default, skip_serializing_if = "Option::is_none")]
2820 pub response_id: Option<String>,
2821 #[serde(default, skip_serializing_if = "Option::is_none")]
2823 pub phase: Option<String>,
2824 #[serde(default, skip_serializing_if = "String::is_empty")]
2826 pub delta: String,
2827 pub accumulated: String,
2829}
2830
2831#[derive(Debug, Clone, Serialize, Deserialize)]
2833#[cfg_attr(feature = "openapi", derive(ToSchema))]
2834pub struct VoiceSessionEndedData {
2835 #[cfg_attr(
2837 feature = "openapi",
2838 schema(example = "voice_01933b5a00007000800000000000001")
2839 )]
2840 pub voice_connection_id: String,
2841 #[serde(default, skip_serializing_if = "Option::is_none")]
2843 #[cfg_attr(
2844 feature = "openapi",
2845 schema(example = "User hung up after refund confirmed.")
2846 )]
2847 pub reason: Option<String>,
2848 #[serde(default, skip_serializing_if = "Option::is_none")]
2851 #[cfg_attr(feature = "openapi", schema(example = 184_500_u64))]
2852 pub duration_ms: Option<u64>,
2853}
2854
2855#[derive(Debug, Clone, Serialize, Deserialize)]
2857#[cfg_attr(feature = "openapi", derive(ToSchema))]
2858pub struct VoiceSessionFailedData {
2859 #[cfg_attr(
2861 feature = "openapi",
2862 schema(example = "voice_01933b5a00007000800000000000001")
2863 )]
2864 pub voice_connection_id: String,
2865 #[cfg_attr(
2867 feature = "openapi",
2868 schema(example = "realtime provider closed stream: 1011 internal_error")
2869 )]
2870 pub error: String,
2871}
2872
2873#[derive(Debug, Clone, Serialize)]
2922#[serde(untagged)]
2923#[cfg_attr(feature = "openapi", derive(ToSchema))]
2924#[cfg_attr(feature = "openapi", schema(
2925 title = "EventData",
2926 description = "Event-specific payload. The schema depends on the event type field.",
2927 example = json!({"message": {"id": "...", "role": "user", "content": []}})
2928))]
2929pub enum EventData {
2930 InputMessage(InputMessageData),
2932
2933 OutputMessageDelta(OutputMessageDeltaData),
2935 OutputMessageStarted(OutputMessageStartedData),
2936 OutputMessageReplaced(OutputMessageReplacedData),
2937 OutputMessageCompleted(OutputMessageCompletedData),
2938
2939 TurnStarted(TurnStartedData),
2941 TurnCompleted(TurnCompletedData),
2942 TurnFailed(TurnFailedData),
2943
2944 ReasonStarted(ReasonStartedData),
2946 ReasonCompleted(ReasonCompletedData),
2947 ReasonRecovered(ReasonRecoveredData),
2948 CapabilityUsage(CapabilityUsageData),
2949 ActStarted(ActStartedData),
2950 ActCompleted(ActCompletedData),
2951 ToolStarted(ToolStartedData),
2952 ToolCompleted(ToolCompletedData),
2953 ToolProgress(ToolProgressData),
2954 ToolOutputDelta(ToolOutputDeltaData),
2955 ToolCallRequested(ToolCallRequestedData),
2956
2957 TranscriptRepaired(TranscriptRepairedData),
2959 ToolCallRepaired(ToolCallRepairedData),
2960
2961 LlmGeneration(LlmGenerationData),
2963
2964 ReasonThinkingDelta(ReasonThinkingDeltaData),
2966 ReasonItem(ReasonItemData),
2967 ReasonThinkingStarted(ReasonThinkingStartedData),
2968 ReasonThinkingCompleted(ReasonThinkingCompletedData),
2969
2970 TurnSealed(TurnSealedData),
2971 TurnCancelled(TurnCancelledData),
2972
2973 SessionStarted(SessionStartedData),
2975 SessionActivated(SessionActivatedData),
2976 SessionIdled(SessionIdledData),
2977 SessionTitleUpdated(SessionTitleUpdatedData),
2978 SessionModelChanged(SessionModelChangedData),
2979
2980 TaskCreated(SessionTaskEventData),
2982 TaskUpdated(SessionTaskEventData),
2983 TaskMessageSent(TaskMessageEventData),
2984 TaskMessageReceived(TaskMessageEventData),
2985
2986 ContextCompacting(ContextCompactingData),
2988 ContextCompacted(ContextCompactedData),
2989 ContextCompactionSkipped(ContextCompactionSkippedData),
2991 ContextCompactionFailed(ContextCompactionFailedData),
2993
2994 FileWritten(FileWrittenData),
2996
2997 BudgetWarning(BudgetEventData),
2999 BudgetPaused(BudgetEventData),
3000 BudgetExhausted(BudgetEventData),
3001 BudgetResumed(BudgetEventData),
3002
3003 VoiceSessionStarted(VoiceSessionStartedData),
3005 VoiceInputTranscriptDelta(VoiceTranscriptData),
3006 VoiceInputTranscriptCompleted(VoiceTranscriptData),
3007 VoiceOutputTranscriptDelta(VoiceTranscriptData),
3008 VoiceOutputTranscriptCompleted(VoiceTranscriptData),
3009 VoiceSessionEnded(VoiceSessionEndedData),
3010 VoiceSessionFailed(VoiceSessionFailedData),
3011
3012 #[serde(skip)]
3016 Unsupported {
3017 event_type: String,
3019 data: serde_json::Value,
3021 },
3022}
3023
3024impl EventData {
3025 pub fn needs_public_projection(&self) -> bool {
3040 matches!(
3041 self,
3042 EventData::InputMessage(_)
3043 | EventData::OutputMessageCompleted(_)
3044 | EventData::LlmGeneration(_)
3045 )
3046 }
3047
3048 pub fn into_public(self) -> Self {
3049 match self {
3050 EventData::InputMessage(mut data) => {
3051 data.message = data.message.into_public();
3052 EventData::InputMessage(data)
3053 }
3054 EventData::OutputMessageCompleted(mut data) => {
3055 data.message = data.message.into_public();
3056 EventData::OutputMessageCompleted(data)
3057 }
3058 EventData::LlmGeneration(mut data) => {
3061 data.messages = data
3062 .messages
3063 .into_iter()
3064 .map(Message::into_public)
3065 .collect();
3066 EventData::LlmGeneration(data)
3067 }
3068 other => other,
3069 }
3070 }
3071
3072 pub fn is_unsupported(&self) -> bool {
3075 matches!(self, EventData::Unsupported { .. })
3076 }
3077
3078 pub fn unsupported(event_type: String, data: serde_json::Value) -> Self {
3081 tracing::warn!(
3082 event_type = %event_type,
3083 "Encountered unsupported event type - will be filtered from API responses"
3084 );
3085 EventData::Unsupported { event_type, data }
3086 }
3087}
3088
3089macro_rules! event_data_kinds {
3103 ($( $variant:ident($data:ty) = $type_const:path ),+ $(,)?) => {
3104 impl EventData {
3105 pub fn event_type(&self) -> &'static str {
3108 match self {
3109 $( EventData::$variant(_) => $type_const, )+
3110 EventData::Unsupported { .. } => "unsupported",
3111 }
3112 }
3113 }
3114
3115 pub fn deserialize_event_data(event_type: &str, data: serde_json::Value) -> EventData {
3127 let result = match event_type {
3128 $(
3129 $type_const => serde_json::from_value::<$data>(data.clone())
3130 .map(EventData::$variant),
3131 )+
3132 _ => return EventData::unsupported(event_type.to_string(), data),
3133 };
3134
3135 result.unwrap_or_else(|e| {
3136 tracing::warn!(
3137 event_type = %event_type,
3138 error = %e,
3139 "Failed to deserialize known event type - treating as unsupported"
3140 );
3141 EventData::Unsupported {
3142 event_type: event_type.to_string(),
3143 data,
3144 }
3145 })
3146 }
3147 };
3148}
3149
3150event_data_kinds! {
3151 InputMessage(InputMessageData) = INPUT_MESSAGE,
3153
3154 OutputMessageStarted(OutputMessageStartedData) = OUTPUT_MESSAGE_STARTED,
3156 OutputMessageDelta(OutputMessageDeltaData) = OUTPUT_MESSAGE_DELTA,
3157 OutputMessageReplaced(OutputMessageReplacedData) = OUTPUT_MESSAGE_REPLACED,
3158 OutputMessageCompleted(OutputMessageCompletedData) = OUTPUT_MESSAGE_COMPLETED,
3159
3160 TurnStarted(TurnStartedData) = TURN_STARTED,
3162 TurnCompleted(TurnCompletedData) = TURN_COMPLETED,
3163 TurnFailed(TurnFailedData) = TURN_FAILED,
3164 TurnSealed(TurnSealedData) = TURN_SEALED,
3165 TurnCancelled(TurnCancelledData) = TURN_CANCELLED,
3166
3167 ReasonStarted(ReasonStartedData) = REASON_STARTED,
3169 ReasonCompleted(ReasonCompletedData) = REASON_COMPLETED,
3170 ReasonRecovered(ReasonRecoveredData) = REASON_RECOVERED,
3171 CapabilityUsage(CapabilityUsageData) = CAPABILITY_USAGE,
3172 ActStarted(ActStartedData) = ACT_STARTED,
3173 ActCompleted(ActCompletedData) = ACT_COMPLETED,
3174 ToolStarted(ToolStartedData) = TOOL_STARTED,
3175 ToolCompleted(ToolCompletedData) = TOOL_COMPLETED,
3176 ToolProgress(ToolProgressData) = TOOL_PROGRESS,
3177 ToolOutputDelta(ToolOutputDeltaData) = TOOL_OUTPUT_DELTA,
3178 ToolCallRequested(ToolCallRequestedData) = TOOL_CALL_REQUESTED,
3179
3180 TranscriptRepaired(TranscriptRepairedData) = TRANSCRIPT_REPAIRED,
3182 ToolCallRepaired(ToolCallRepairedData) = TOOL_CALL_REPAIRED,
3183
3184 LlmGeneration(LlmGenerationData) = LLM_GENERATION,
3186
3187 ReasonThinkingStarted(ReasonThinkingStartedData) = REASON_THINKING_STARTED,
3189 ReasonThinkingDelta(ReasonThinkingDeltaData) = REASON_THINKING_DELTA,
3190 ReasonThinkingCompleted(ReasonThinkingCompletedData) = REASON_THINKING_COMPLETED,
3191 ReasonItem(ReasonItemData) = REASON_ITEM,
3192
3193 SessionStarted(SessionStartedData) = SESSION_STARTED,
3195 SessionActivated(SessionActivatedData) = SESSION_ACTIVATED,
3196 SessionIdled(SessionIdledData) = SESSION_IDLED,
3197 SessionTitleUpdated(SessionTitleUpdatedData) = SESSION_TITLE_UPDATED,
3198 SessionModelChanged(SessionModelChangedData) = SESSION_MODEL_CHANGED,
3199
3200 ContextCompacting(ContextCompactingData) = CONTEXT_COMPACTING,
3202 ContextCompacted(ContextCompactedData) = CONTEXT_COMPACTED,
3203 ContextCompactionSkipped(ContextCompactionSkippedData) = CONTEXT_COMPACTION_SKIPPED,
3204 ContextCompactionFailed(ContextCompactionFailedData) = CONTEXT_COMPACTION_FAILED,
3205
3206 FileWritten(FileWrittenData) = FILE_WRITTEN,
3208
3209 BudgetWarning(BudgetEventData) = BUDGET_WARNING,
3211 BudgetPaused(BudgetEventData) = BUDGET_PAUSED,
3212 BudgetExhausted(BudgetEventData) = BUDGET_EXHAUSTED,
3213 BudgetResumed(BudgetEventData) = BUDGET_RESUMED,
3214
3215 VoiceSessionStarted(VoiceSessionStartedData) = VOICE_SESSION_STARTED,
3217 VoiceInputTranscriptDelta(VoiceTranscriptData) = VOICE_INPUT_TRANSCRIPT_DELTA,
3218 VoiceInputTranscriptCompleted(VoiceTranscriptData) = VOICE_INPUT_TRANSCRIPT_COMPLETED,
3219 VoiceOutputTranscriptDelta(VoiceTranscriptData) = VOICE_OUTPUT_TRANSCRIPT_DELTA,
3220 VoiceOutputTranscriptCompleted(VoiceTranscriptData) = VOICE_OUTPUT_TRANSCRIPT_COMPLETED,
3221 VoiceSessionEnded(VoiceSessionEndedData) = VOICE_SESSION_ENDED,
3222 VoiceSessionFailed(VoiceSessionFailedData) = VOICE_SESSION_FAILED,
3223
3224 TaskCreated(SessionTaskEventData) = TASK_CREATED,
3226 TaskUpdated(SessionTaskEventData) = TASK_UPDATED,
3227 TaskMessageSent(TaskMessageEventData) = TASK_MESSAGE_SENT,
3228 TaskMessageReceived(TaskMessageEventData) = TASK_MESSAGE_RECEIVED,
3229}
3230
3231macro_rules! impl_from_event_data {
3235 ($($data_type:ty => $variant:ident),* $(,)?) => {
3236 $(
3237 impl From<$data_type> for EventData {
3238 fn from(data: $data_type) -> Self {
3239 EventData::$variant(data)
3240 }
3241 }
3242 )*
3243 };
3244}
3245
3246impl_from_event_data! {
3248 InputMessageData => InputMessage,
3249 OutputMessageStartedData => OutputMessageStarted,
3250 OutputMessageDeltaData => OutputMessageDelta,
3251 OutputMessageReplacedData => OutputMessageReplaced,
3252 OutputMessageCompletedData => OutputMessageCompleted,
3253 TurnStartedData => TurnStarted,
3254 TurnCompletedData => TurnCompleted,
3255 TurnFailedData => TurnFailed,
3256 TurnSealedData => TurnSealed,
3257 TurnCancelledData => TurnCancelled,
3258 ReasonStartedData => ReasonStarted,
3259 ReasonCompletedData => ReasonCompleted,
3260 ReasonRecoveredData => ReasonRecovered,
3261 CapabilityUsageData => CapabilityUsage,
3262 ActStartedData => ActStarted,
3263 ActCompletedData => ActCompleted,
3264 ToolStartedData => ToolStarted,
3265 ToolCompletedData => ToolCompleted,
3266 ToolProgressData => ToolProgress,
3267 ToolOutputDeltaData => ToolOutputDelta,
3268 ToolCallRequestedData => ToolCallRequested,
3269 TranscriptRepairedData => TranscriptRepaired,
3270 ToolCallRepairedData => ToolCallRepaired,
3271 LlmGenerationData => LlmGeneration,
3272 ReasonThinkingStartedData => ReasonThinkingStarted,
3273 ReasonThinkingDeltaData => ReasonThinkingDelta,
3274 ReasonThinkingCompletedData => ReasonThinkingCompleted,
3275 ReasonItemData => ReasonItem,
3276 SessionStartedData => SessionStarted,
3277 SessionActivatedData => SessionActivated,
3278 SessionIdledData => SessionIdled,
3279 SessionTitleUpdatedData => SessionTitleUpdated,
3280 SessionModelChangedData => SessionModelChanged,
3281 ContextCompactingData => ContextCompacting,
3282 ContextCompactedData => ContextCompacted,
3283 ContextCompactionSkippedData => ContextCompactionSkipped,
3284 ContextCompactionFailedData => ContextCompactionFailed,
3285 FileWrittenData => FileWritten,
3286 VoiceSessionStartedData => VoiceSessionStarted,
3287 VoiceSessionEndedData => VoiceSessionEnded,
3288 VoiceSessionFailedData => VoiceSessionFailed,
3289}
3290
3291impl EventData {
3292 pub fn voice_transcript_event(data: VoiceTranscriptData, event_type: &str) -> Self {
3293 match event_type {
3294 VOICE_INPUT_TRANSCRIPT_DELTA => EventData::VoiceInputTranscriptDelta(data),
3295 VOICE_INPUT_TRANSCRIPT_COMPLETED => EventData::VoiceInputTranscriptCompleted(data),
3296 VOICE_OUTPUT_TRANSCRIPT_DELTA => EventData::VoiceOutputTranscriptDelta(data),
3297 VOICE_OUTPUT_TRANSCRIPT_COMPLETED => EventData::VoiceOutputTranscriptCompleted(data),
3298 _ => EventData::unsupported(
3299 event_type.to_string(),
3300 serde_json::to_value(&data).unwrap_or(serde_json::Value::Null),
3301 ),
3302 }
3303 }
3304}
3305
3306impl EventData {
3309 pub fn budget_event(data: BudgetEventData, event_type: &str) -> Self {
3310 match event_type {
3311 BUDGET_WARNING => EventData::BudgetWarning(data),
3312 BUDGET_PAUSED => EventData::BudgetPaused(data),
3313 BUDGET_EXHAUSTED => EventData::BudgetExhausted(data),
3314 BUDGET_RESUMED => EventData::BudgetResumed(data),
3315 _ => EventData::unsupported(
3316 event_type.to_string(),
3317 serde_json::to_value(&data).unwrap_or(serde_json::Value::Null),
3318 ),
3319 }
3320 }
3321}
3322
3323#[derive(Debug, Clone, Serialize)]
3333#[cfg_attr(feature = "openapi", derive(ToSchema))]
3334pub struct EventRequest {
3335 #[serde(rename = "type")]
3337 pub event_type: String,
3338
3339 pub ts: DateTime<Utc>,
3341
3342 pub session_id: SessionId,
3344
3345 pub context: EventContext,
3347
3348 pub data: EventData,
3350
3351 #[serde(skip_serializing_if = "Option::is_none")]
3353 pub metadata: Option<serde_json::Value>,
3354
3355 #[serde(skip_serializing_if = "Option::is_none")]
3357 pub tags: Option<Vec<String>>,
3358}
3359
3360#[derive(Debug, Deserialize)]
3361struct RawEventRequest {
3362 #[serde(rename = "type")]
3363 event_type: String,
3364 ts: DateTime<Utc>,
3365 session_id: SessionId,
3366 context: EventContext,
3367 data: serde_json::Value,
3368 metadata: Option<serde_json::Value>,
3369 tags: Option<Vec<String>>,
3370}
3371
3372impl<'de> Deserialize<'de> for EventRequest {
3373 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
3374 where
3375 D: Deserializer<'de>,
3376 {
3377 let raw = RawEventRequest::deserialize(deserializer)?;
3378 let data = deserialize_event_data(&raw.event_type, raw.data);
3379 Ok(Self {
3380 event_type: raw.event_type,
3381 ts: raw.ts,
3382 session_id: raw.session_id,
3383 context: raw.context,
3384 data,
3385 metadata: raw.metadata,
3386 tags: raw.tags,
3387 })
3388 }
3389}
3390
3391impl EventRequest {
3392 pub fn new(session_id: SessionId, context: EventContext, data: impl Into<EventData>) -> Self {
3396 let data = data.into();
3397 let event_type = data.event_type().to_string();
3398 Self {
3399 event_type,
3400 ts: Utc::now(),
3401 session_id,
3402 context,
3403 data,
3404 metadata: None,
3405 tags: None,
3406 }
3407 }
3408
3409 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
3411 self.metadata = Some(metadata);
3412 self
3413 }
3414
3415 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
3417 self.tags = Some(tags);
3418 self
3419 }
3420
3421 pub fn is_ephemeral(&self) -> bool {
3429 is_ephemeral_event_type(&self.event_type)
3430 }
3431
3432 pub fn into_event(self, id: EventId, sequence: i32) -> Event {
3434 Event {
3435 id,
3436 event_type: self.event_type,
3437 ts: self.ts,
3438 session_id: self.session_id,
3439 context: self.context,
3440 data: self.data,
3441 metadata: self.metadata,
3442 tags: self.tags,
3443 sequence: Some(sequence),
3444 }
3445 }
3446}
3447
3448pub struct EventBuilder {
3454 session_id: SessionId,
3455 context: EventContext,
3456}
3457
3458impl EventBuilder {
3459 pub fn new(session_id: SessionId) -> Self {
3460 Self {
3461 session_id,
3462 context: EventContext::empty(),
3463 }
3464 }
3465
3466 pub fn with_turn(mut self, turn_id: TurnId, input_message_id: MessageId) -> Self {
3467 self.context.turn_id = Some(turn_id);
3468 self.context.input_message_id = Some(input_message_id);
3469 self
3470 }
3471
3472 pub fn with_exec(mut self, exec_id: ExecId) -> Self {
3473 self.context.exec_id = Some(exec_id);
3474 self
3475 }
3476
3477 pub fn build(self, data: impl Into<EventData>) -> Event {
3478 Event::new(self.session_id, self.context, data)
3479 }
3480}
3481
3482#[cfg(test)]
3487mod tests {
3488 use super::*;
3489 use crate::driver_registry::PromptCacheStrategy;
3490 use serde_json::json;
3491 use std::collections::HashMap;
3492
3493 fn message_with_replay_state() -> Message {
3496 let mut message = Message::assistant("the answer");
3497 message.content.push(ContentPart::reasoning(
3498 everruns_provider::reasoning::ReasoningContentPart::opaque("anthropic")
3499 .with_item_id("rs_abc")
3500 .with_signature("sig-do-not-publish")
3501 .with_encrypted("enc-do-not-publish")
3502 .with_text(everruns_provider::reasoning::ReasoningText::Plain {
3503 text: "visible reasoning".to_string(),
3504 }),
3505 ));
3506 message
3507 }
3508
3509 fn generation_metadata() -> LlmGenerationMetadata {
3510 LlmGenerationMetadata {
3511 model: "test-model".to_string(),
3512 provider: Some("anthropic".to_string()),
3513 usage: None,
3514 duration_ms: None,
3515 time_to_first_token_ms: None,
3516 success: true,
3517 error: None,
3518 finish_reasons: None,
3519 response_id: None,
3520 retry: None,
3521 compaction: None,
3522 request_options: None,
3523 }
3524 }
3525
3526 fn reasoning_part(message: &Message) -> &everruns_provider::reasoning::ReasoningContentPart {
3527 message
3528 .content
3529 .iter()
3530 .find_map(|part| match part {
3531 ContentPart::Reasoning(r) => Some(r),
3532 _ => None,
3533 })
3534 .expect("reasoning part")
3535 }
3536
3537 #[test]
3541 fn into_public_strips_replay_state_from_completed_messages() {
3542 let data = EventData::OutputMessageCompleted(OutputMessageCompletedData::new(
3543 message_with_replay_state(),
3544 ));
3545
3546 let EventData::OutputMessageCompleted(public) = data.into_public() else {
3547 panic!("variant must be preserved");
3548 };
3549 let part = reasoning_part(&public.message);
3550
3551 assert_eq!(part.signature, None, "signature must not be published");
3552 assert_eq!(
3553 part.encrypted, None,
3554 "encrypted payload must not be published"
3555 );
3556 assert_eq!(
3557 part.item_id.as_deref(),
3558 Some("rs_abc"),
3559 "the provider id is an identifier, not replay state"
3560 );
3561 assert_eq!(
3562 part.display_text().as_deref(),
3563 Some("visible reasoning"),
3564 "readable reasoning is content and must survive"
3565 );
3566 }
3567
3568 #[test]
3569 fn into_public_strips_replay_state_from_input_and_generation_messages() {
3570 let EventData::InputMessage(input) =
3571 EventData::InputMessage(InputMessageData::new(message_with_replay_state()))
3572 .into_public()
3573 else {
3574 panic!("variant must be preserved");
3575 };
3576 assert_eq!(reasoning_part(&input.message).signature, None);
3577 assert_eq!(reasoning_part(&input.message).encrypted, None);
3578 assert_eq!(
3579 reasoning_part(&input.message).display_text().as_deref(),
3580 Some("visible reasoning")
3581 );
3582
3583 let generation = LlmGenerationData {
3585 messages: vec![message_with_replay_state()],
3586 tools: Vec::new(),
3587 output: LlmGenerationOutput {
3588 text: Some("the answer".to_string()),
3589 tool_calls: Vec::new(),
3590 },
3591 metadata: generation_metadata(),
3592 };
3593 let EventData::LlmGeneration(public) = EventData::LlmGeneration(generation).into_public()
3594 else {
3595 panic!("variant must be preserved");
3596 };
3597 assert_eq!(reasoning_part(&public.messages[0]).signature, None);
3598 assert_eq!(reasoning_part(&public.messages[0]).encrypted, None);
3599 assert_eq!(
3600 reasoning_part(&public.messages[0])
3601 .display_text()
3602 .as_deref(),
3603 Some("visible reasoning")
3604 );
3605 assert_eq!(public.output.text.as_deref(), Some("the answer"));
3606 }
3607
3608 #[test]
3611 fn needs_public_projection_covers_every_message_bearing_variant() {
3612 assert!(
3613 EventData::InputMessage(InputMessageData::new(Message::user("hi")))
3614 .needs_public_projection()
3615 );
3616 assert!(
3617 EventData::OutputMessageCompleted(OutputMessageCompletedData::new(Message::assistant(
3618 "hi"
3619 )))
3620 .needs_public_projection()
3621 );
3622 assert!(
3623 EventData::LlmGeneration(LlmGenerationData {
3624 messages: Vec::new(),
3625 tools: Vec::new(),
3626 output: LlmGenerationOutput {
3627 text: None,
3628 tool_calls: Vec::new(),
3629 },
3630 metadata: generation_metadata(),
3631 })
3632 .needs_public_projection()
3633 );
3634 assert!(
3635 !EventData::ReasonItem(ReasonItemData {
3636 turn_id: TurnId::new(),
3637 provider: "openai".to_string(),
3638 model: None,
3639 item_id: "rs_abc".to_string(),
3640 summary: Vec::new(),
3641 token_count: None,
3642 })
3643 .needs_public_projection(),
3644 "a message-free variant must not pay for a clone"
3645 );
3646 }
3647
3648 #[test]
3649 fn test_event_creation() {
3650 let session_id = SessionId::new();
3651 let context = EventContext::empty();
3652 let data = InputMessageData::new(Message::user("test"));
3653
3654 let event = Event::new(session_id, context, data);
3655
3656 assert_eq!(event.event_type, "input.message");
3657 assert_eq!(event.session_uuid(), session_id.uuid());
3658 assert!(event.is_input_event());
3659 assert!(event.is_message_event());
3660 }
3661
3662 #[test]
3663 fn test_event_context_from_execution_context() {
3664 let session_id = SessionId::new();
3665 let turn_id = TurnId::new();
3666 let input_message_id = MessageId::new();
3667
3668 let atom_ctx = ExecutionContext::new(session_id, turn_id, input_message_id);
3669 let context = EventContext::from_execution_context(&atom_ctx);
3670
3671 assert_eq!(context.turn_id, Some(turn_id));
3672 assert_eq!(context.input_message_id, Some(input_message_id));
3673 assert_eq!(context.exec_id, Some(atom_ctx.exec_id));
3674 }
3675
3676 #[test]
3677 fn transcript_repaired_is_valid_filter_event_type() {
3678 assert!(VALID_EVENT_TYPES.contains(&TRANSCRIPT_REPAIRED));
3679 }
3680
3681 #[test]
3685 fn capability_usage_is_valid_filter_event_type() {
3686 assert!(VALID_EVENT_TYPES.contains(&CAPABILITY_USAGE));
3687 }
3688
3689 #[test]
3690 fn test_event_builder() {
3691 let session_id = SessionId::new();
3692 let turn_id = TurnId::new();
3693 let input_message_id = MessageId::new();
3694 let exec_id = ExecId::new();
3695
3696 let event = EventBuilder::new(session_id)
3697 .with_turn(turn_id, input_message_id)
3698 .with_exec(exec_id)
3699 .build(ReasonStartedData {
3700 harness_id: HarnessId::from_seed(1),
3701 agent_id: Some(AgentId::new()),
3702 metadata: Some(ModelMetadata {
3703 model: "gpt-5.2".to_string(),
3704 model_id: None,
3705 provider_id: None,
3706 }),
3707 });
3708
3709 assert_eq!(event.event_type, "reason.started");
3710 assert_eq!(event.session_id, session_id);
3711 assert_eq!(event.context.turn_id, Some(turn_id));
3712 assert_eq!(event.context.input_message_id, Some(input_message_id));
3713 assert_eq!(event.context.exec_id, Some(exec_id));
3714 }
3715
3716 #[test]
3717 fn test_reason_completed_data() {
3718 let data = ReasonCompletedData::success("Hello world", true, 2, Some(1000), None);
3719 assert!(data.success);
3720 assert_eq!(data.text_preview, Some("Hello world".to_string()));
3721 assert!(data.has_tool_calls);
3722 assert_eq!(data.tool_call_count, 2);
3723 assert_eq!(data.duration_ms, Some(1000));
3724 assert!(data.usage.is_none());
3725
3726 let data = ReasonCompletedData::failure("Network error".to_string(), Some(500));
3727 assert!(!data.success);
3728 assert_eq!(data.error, Some("Network error".to_string()));
3729 assert_eq!(data.duration_ms, Some(500));
3730 }
3731
3732 #[test]
3733 fn test_llm_generation_data_success() {
3734 let messages = vec![Message::user("Hello"), Message::assistant("Hi there!")];
3735 let tools = vec![ToolDefinitionSummary {
3736 name: "get_weather".to_string(),
3737 display_name: None,
3738 category: None,
3739 capability_id: None,
3740 capability_name: None,
3741 description: "Get weather for a city".to_string(),
3742 }];
3743 let tool_calls = vec![ToolCall {
3744 id: "call_weather".into(),
3745 name: "get_weather".into(),
3746 arguments: json!({"city": "Kyiv"}),
3747 }];
3748 let data = LlmGenerationData::success(
3749 messages.clone(),
3750 tools.clone(),
3751 Some("Hi there!".to_string()),
3752 tool_calls.clone(),
3753 "gpt-5.2".to_string(),
3754 Some("openai".to_string()),
3755 Some(TokenUsage {
3756 input_tokens: 10,
3757 output_tokens: 5,
3758 cache_read_tokens: None,
3759 cache_creation_tokens: None,
3760 actual_cost_usd: None,
3761 estimated_cost_usd: None,
3762 effective_cost_usd: None,
3763 }),
3764 Some(100),
3765 Some(25), );
3767
3768 assert_eq!(
3769 serde_json::to_value(&data.messages).unwrap(),
3770 serde_json::to_value(messages).unwrap()
3771 );
3772 assert_eq!(
3773 serde_json::to_value(&data.tools).unwrap(),
3774 serde_json::to_value(tools).unwrap()
3775 );
3776 assert_eq!(
3777 serde_json::to_value(&data.output.tool_calls).unwrap(),
3778 serde_json::to_value(tool_calls).unwrap()
3779 );
3780 assert_eq!(data.output.text.as_deref(), Some("Hi there!"));
3781 assert_eq!(
3782 serde_json::to_value(&data.metadata).unwrap(),
3783 json!({
3784 "model": "gpt-5.2", "provider": "openai", "success": true,
3785 "usage": {"input_tokens": 10, "output_tokens": 5},
3786 "duration_ms": 100, "time_to_first_token_ms": 25, "finish_reasons": ["tool_calls"]
3787 })
3788 );
3789 }
3790
3791 #[test]
3792 fn test_llm_generation_data_with_full_metadata() {
3793 let messages = vec![Message::user("Hello")];
3794 let data = LlmGenerationData::success_with_metadata(
3795 messages.clone(),
3796 vec![],
3797 Some("Hi!".to_string()),
3798 vec![],
3799 "claude-opus-5".to_string(),
3800 Some("anthropic".to_string()),
3801 Some(TokenUsage {
3802 input_tokens: 5,
3803 output_tokens: 3,
3804 cache_read_tokens: None,
3805 cache_creation_tokens: None,
3806 actual_cost_usd: None,
3807 estimated_cost_usd: None,
3808 effective_cost_usd: None,
3809 }),
3810 Some(50),
3811 Some(25), Some(vec!["end_turn".to_string()]),
3813 Some("msg_12345".to_string()),
3814 );
3815
3816 assert_eq!(
3817 serde_json::to_value(&data.messages).unwrap(),
3818 serde_json::to_value(&messages).unwrap()
3819 );
3820 assert_eq!(
3821 serde_json::to_value(&data.metadata).unwrap(),
3822 json!({
3823 "model": "claude-opus-5", "provider": "anthropic",
3824 "usage": {"input_tokens": 5, "output_tokens": 3},
3825 "duration_ms": 50, "time_to_first_token_ms": 25,
3826 "success": true, "finish_reasons": ["end_turn"], "response_id": "msg_12345"
3827 })
3828 );
3829 assert_eq!(
3830 serde_json::to_value(&data.output).unwrap(),
3831 json!({"text": "Hi!"})
3832 );
3833 assert!(data.tools.is_empty());
3834 }
3835
3836 #[test]
3837 fn test_llm_generation_data_failure() {
3838 let messages = vec![Message::user("Hello")];
3839 let data = LlmGenerationData::failure(
3840 messages.clone(),
3841 vec![],
3842 "gpt-5.2".to_string(),
3843 Some("openai".to_string()),
3844 "Rate limit exceeded".to_string(),
3845 Some(50),
3846 None, );
3848
3849 assert_eq!(
3850 serde_json::to_value(&data.messages).unwrap(),
3851 serde_json::to_value(messages).unwrap()
3852 );
3853 assert_eq!(
3854 serde_json::to_value(&data.metadata).unwrap(),
3855 json!({
3856 "model": "gpt-5.2", "provider": "openai", "success": false,
3857 "error": "Rate limit exceeded", "duration_ms": 50, "finish_reasons": ["error"]
3858 })
3859 );
3860 assert_eq!(serde_json::to_value(&data.output).unwrap(), json!({}));
3861 assert!(data.tools.is_empty());
3862 }
3863
3864 #[test]
3865 fn test_delta_events_are_ephemeral() {
3866 let session_id = SessionId::new();
3867 let turn_id = TurnId::new();
3868
3869 let voice = VoiceTranscriptData {
3870 voice_connection_id: "voice_1".into(),
3871 item_id: Some("item_2".into()),
3872 response_id: None,
3873 phase: None,
3874 delta: "world".into(),
3875 accumulated: "Hello world".into(),
3876 };
3877 let cases: Vec<(EventData, bool)> = vec![
3878 (
3879 OutputMessageDeltaData {
3880 turn_id,
3881 message_id: MessageId::new(),
3882 delta: "world".into(),
3883 accumulated: "Hello world".into(),
3884 phase: None,
3885 }
3886 .into(),
3887 true,
3888 ),
3889 (
3890 ReasonThinkingDeltaData {
3891 turn_id,
3892 delta: "next".into(),
3893 accumulated: "first next".into(),
3894 }
3895 .into(),
3896 true,
3897 ),
3898 (
3899 ToolOutputDeltaData {
3900 tool_call_id: "call_123".into(),
3901 tool_name: "bash".into(),
3902 delta: "line".into(),
3903 stream: "stdout".into(),
3904 }
3905 .into(),
3906 true,
3907 ),
3908 (EventData::VoiceInputTranscriptDelta(voice.clone()), true),
3909 (EventData::VoiceOutputTranscriptDelta(voice.clone()), true),
3910 (
3911 EventData::VoiceInputTranscriptCompleted(voice.clone()),
3912 false,
3913 ),
3914 (EventData::VoiceOutputTranscriptCompleted(voice), false),
3915 (
3916 OutputMessageCompletedData::new(Message::assistant("done")).into(),
3917 false,
3918 ),
3919 (
3920 LlmGenerationData::success(
3921 vec![],
3922 vec![],
3923 Some("done".into()),
3924 vec![],
3925 "model".into(),
3926 None,
3927 None,
3928 None,
3929 None,
3930 )
3931 .into(),
3932 false,
3933 ),
3934 ];
3935 for (data, expected) in cases {
3936 let request = EventRequest::new(session_id, EventContext::empty(), data);
3937 assert_eq!(
3938 request.is_ephemeral(),
3939 expected,
3940 "{} request",
3941 request.event_type
3942 );
3943 let event = request.into_event(EventId::new(), 7);
3944 assert_eq!(event.is_ephemeral(), expected, "{} event", event.event_type);
3945 }
3946 }
3947
3948 #[test]
3949 fn test_llm_generation_data_with_request_options() {
3950 let mut provider_options = HashMap::new();
3951 provider_options.insert(
3952 "openai".to_string(),
3953 json!({ "previous_response_id": true }),
3954 );
3955
3956 let data = LlmGenerationData::success(
3957 vec![Message::user("Hello")],
3958 vec![],
3959 Some("Hi".to_string()),
3960 vec![],
3961 "gpt-5.4".to_string(),
3962 Some("openai".to_string()),
3963 None,
3964 Some(42),
3965 Some(12),
3966 )
3967 .with_request_options(LlmRequestOptions {
3968 temperature: Some(0.5),
3969 max_tokens: Some(256),
3970 reasoning_effort: Some("high".into()),
3971 stream: Some(false),
3972 prompt_cache: Some(LlmPromptCacheInfo {
3973 enabled: true,
3974 strategy: PromptCacheStrategy::Auto,
3975 provider_mode: Some("prompt_cache_key".to_string()),
3976 }),
3977 tool_search: Some(LlmToolSearchInfo {
3978 enabled: true,
3979 threshold: 8,
3980 }),
3981 provider_options,
3982 metadata: HashMap::from([("project".into(), "test".into())]),
3983 });
3984
3985 let json = serde_json::to_value(&data).unwrap();
3986 assert_eq!(
3987 json["metadata"]["request_options"],
3988 json!({
3989 "temperature": 0.5, "max_tokens": 256, "reasoning_effort": "high", "stream": false,
3990 "prompt_cache": {"enabled": true, "strategy": "auto", "provider_mode": "prompt_cache_key"},
3991 "tool_search": {"enabled": true, "threshold": 8},
3992 "provider_options": {"openai": {"previous_response_id": true}}, "metadata": {"project": "test"}
3993 })
3994 );
3995 let empty = LlmGenerationData::success(
3996 vec![],
3997 vec![],
3998 None,
3999 vec![],
4000 "model".into(),
4001 None,
4002 None,
4003 None,
4004 None,
4005 )
4006 .with_request_options(LlmRequestOptions::default());
4007 assert!(
4008 serde_json::to_value(empty).unwrap()["metadata"]
4009 .get("request_options")
4010 .is_none()
4011 );
4012 }
4013
4014 #[test]
4015 fn test_output_message_started_data_without_model() {
4016 let turn_id = TurnId::from_uuid(Uuid::now_v7());
4017 let data = OutputMessageStartedData {
4018 reasoning_state: None,
4019 turn_id,
4020 message_id: MessageId::new(),
4021 model: None,
4022 iteration: None,
4023 phase: None,
4024 };
4025
4026 let json = serde_json::to_value(&data).unwrap();
4028 assert!(json.get("model").is_none());
4029 }
4030
4031 #[test]
4032 fn test_output_message_lifecycle_shares_message_id() {
4033 let turn_id = TurnId::new();
4034 let message_id = MessageId::from_uuid(Uuid::from_u128(3));
4035 let next_message_id = MessageId::from_uuid(Uuid::from_u128(4));
4036
4037 let started = OutputMessageStartedData {
4038 reasoning_state: None,
4039 turn_id,
4040 message_id,
4041 model: None,
4042 iteration: Some(1),
4043 phase: None,
4044 };
4045 let next_started = OutputMessageStartedData {
4046 reasoning_state: None,
4047 turn_id,
4048 message_id: next_message_id,
4049 model: None,
4050 iteration: Some(2),
4051 phase: None,
4052 };
4053 let delta = OutputMessageDeltaData {
4054 turn_id,
4055 message_id,
4056 delta: "Hello".to_string(),
4057 accumulated: "Hello".to_string(),
4058 phase: None,
4059 };
4060 let replaced = OutputMessageReplacedData {
4061 turn_id,
4062 message_id,
4063 guardrail_capability_id: "guardrails".to_string(),
4064 guardrail_id: "example".to_string(),
4065 reason_code: "blocked".to_string(),
4066 replacement: "Safe response".to_string(),
4067 };
4068 let completed = OutputMessageCompletedData::new(
4069 Message::assistant("Safe response").with_id(message_id),
4070 );
4071
4072 for value in [
4073 serde_json::to_value(started).unwrap(),
4074 serde_json::to_value(delta).unwrap(),
4075 serde_json::to_value(replaced).unwrap(),
4076 ] {
4077 assert_eq!(value["message_id"], message_id.to_string());
4078 }
4079 let completed = serde_json::to_value(completed).unwrap();
4080 assert_eq!(completed["message"]["id"], message_id.to_string());
4081 assert_eq!(
4082 completed["message"]["content"],
4083 json!([{"type": "text", "text": "Safe response"}])
4084 );
4085 let next = serde_json::to_value(next_started).unwrap();
4086 assert_eq!(next["message_id"], next_message_id.to_string());
4087 assert_eq!(next["iteration"], 2);
4088 }
4089
4090 #[test]
4091 fn test_output_message_phase_hint_serde() {
4092 let turn_id = TurnId::new();
4093 let message_id = MessageId::new();
4094 for (phase, wire) in [
4095 (None, None),
4096 (Some(ExecutionPhase::Commentary), Some("commentary")),
4097 (Some(ExecutionPhase::FinalAnswer), Some("final_answer")),
4098 ] {
4099 let cases: Vec<(&str, EventData)> = vec![
4100 (
4101 "output.message.started",
4102 OutputMessageStartedData {
4103 reasoning_state: None,
4104 turn_id,
4105 message_id,
4106 model: None,
4107 iteration: None,
4108 phase,
4109 }
4110 .into(),
4111 ),
4112 (
4113 "output.message.delta",
4114 OutputMessageDeltaData {
4115 turn_id,
4116 message_id,
4117 delta: "done".into(),
4118 accumulated: "nearly done".into(),
4119 phase,
4120 }
4121 .into(),
4122 ),
4123 ];
4124 for (kind, data) in cases {
4125 let json = serde_json::to_value(&data).unwrap();
4126 assert_eq!(
4127 json.get("phase"),
4128 wire.map(serde_json::Value::from).as_ref(),
4129 "{kind}"
4130 );
4131 let decoded = deserialize_event_data(kind, json.clone());
4132 assert!(!decoded.is_unsupported(), "{kind}");
4133 assert_eq!(serde_json::to_value(decoded).unwrap(), json);
4134 }
4135 }
4136 }
4137
4138 #[test]
4139 fn test_output_message_delta_deserialization_preserves_fields() {
4140 let json = serde_json::json!({
4141 "turn_id": TurnId::new(), "message_id": MessageId::new(),
4142 "delta": "world", "accumulated": "Hello world", "phase": "final_answer"
4143 });
4144 let deserialized = deserialize_event_data("output.message.delta", json.clone());
4145 assert!(matches!(&deserialized, EventData::OutputMessageDelta(_)));
4146 assert_eq!(serde_json::to_value(deserialized).unwrap(), json);
4147 }
4148
4149 #[test]
4150 fn test_output_message_started_deserialization() {
4151 let turn_id = TurnId::from_uuid(Uuid::now_v7());
4152 let data = OutputMessageStartedData {
4153 reasoning_state: None,
4154 turn_id,
4155 message_id: MessageId::new(),
4156 model: Some("claude-opus-5".to_string()),
4157 iteration: Some(3),
4158 phase: Some(ExecutionPhase::FinalAnswer),
4159 };
4160
4161 let json = serde_json::to_value(EventData::OutputMessageStarted(data.clone())).unwrap();
4163
4164 let deserialized = deserialize_event_data("output.message.started", json.clone());
4166
4167 assert!(matches!(&deserialized, EventData::OutputMessageStarted(_)));
4168 assert_eq!(serde_json::to_value(deserialized).unwrap(), json);
4169 }
4170
4171 #[test]
4172 fn test_event_deserialize_reason_item_uses_event_type_dispatch() {
4173 let turn_id = TurnId::from_uuid(Uuid::now_v7());
4174 let payload = serde_json::json!({
4175 "id": EventId::new().to_string(),
4176 "type": REASON_ITEM,
4177 "ts": Utc::now().to_rfc3339(),
4178 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
4179 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
4180 "data": {
4181 "turn_id": turn_id.to_string(),
4182 "provider": "openai",
4183 "model": "gpt-5",
4184 "item_id": "rs_event",
4185 "summary": ["safe"],
4186 "token_count": 9
4187 }
4188 });
4189
4190 let event: Event = serde_json::from_value(payload.clone()).expect("event deserializes");
4191 assert_eq!(serde_json::to_value(&event.data).unwrap(), payload["data"]);
4192 match event.data {
4193 EventData::ReasonItem(data) => {
4194 assert_eq!(data.turn_id, turn_id);
4195 assert_eq!(data.provider, "openai");
4196 assert_eq!(data.item_id, "rs_event");
4197 assert_eq!(data.token_count, Some(9));
4198 }
4199 other => panic!("expected reason.item data, got {}", other.event_type()),
4200 }
4201 }
4202
4203 #[test]
4204 fn test_event_request_deserialize_reason_item_uses_event_type_dispatch() {
4205 let turn_id = TurnId::from_uuid(Uuid::now_v7());
4206 let payload = serde_json::json!({
4207 "type": REASON_ITEM,
4208 "ts": Utc::now().to_rfc3339(),
4209 "session_id": SessionId::from_uuid(Uuid::now_v7()).to_string(),
4210 "context": {"trace_id": "t", "span_id": "s", "parent_span_id": null},
4211 "data": {
4212 "turn_id": turn_id.to_string(),
4213 "provider": "openai",
4214 "item_id": "rs_request",
4215 "summary": ["safe"]
4216 }
4217 });
4218
4219 let req: EventRequest =
4220 serde_json::from_value(payload.clone()).expect("request deserializes");
4221 assert_eq!(serde_json::to_value(&req.data).unwrap(), payload["data"]);
4222 match req.data {
4223 EventData::ReasonItem(data) => {
4224 assert_eq!(data.turn_id, turn_id);
4225 assert_eq!(data.provider, "openai");
4226 assert_eq!(data.item_id, "rs_request");
4227 }
4228 other => panic!("expected reason.item data, got {}", other.event_type()),
4229 }
4230 }
4231
4232 #[test]
4241 fn test_reason_item_resolves_via_type_dispatch_despite_overlap() {
4242 let turn_id = TurnId::from_uuid(Uuid::now_v7());
4247 let json = serde_json::json!({
4248 "turn_id": turn_id.to_string(),
4249 "provider": "openai",
4250 "model": "gpt-5",
4251 "item_id": "rs_keep",
4252 "summary": ["s"],
4253 "token_count": 7,
4254 });
4255
4256 let as_thinking: ReasonThinkingStartedData =
4261 serde_json::from_value(json.clone()).expect("thinking ignores extra fields");
4262 assert_eq!(as_thinking.turn_id, turn_id);
4263 assert_eq!(as_thinking.model.as_deref(), Some("gpt-5"));
4264
4265 let as_item: ReasonItemData =
4266 serde_json::from_value(json.clone()).expect("ReasonItem accepts payload");
4267 assert_eq!(as_item.item_id, "rs_keep");
4268 assert_eq!(as_item.provider, "openai");
4269
4270 let event_data = deserialize_event_data(REASON_ITEM, json);
4273 match event_data {
4274 EventData::ReasonItem(out) => {
4275 assert_eq!(out.item_id, "rs_keep");
4276 assert_eq!(out.provider, "openai");
4277 }
4278 other => panic!(
4279 "Typed dispatcher must select ReasonItem for {REASON_ITEM}, got {}",
4280 other.event_type()
4281 ),
4282 }
4283 }
4284
4285 #[test]
4293 fn test_reason_item_data_excludes_plaintext_reasoning() {
4294 let turn_id = TurnId::from_uuid(Uuid::now_v7());
4295 let data = ReasonItemData {
4296 turn_id,
4297 provider: "openai".to_string(),
4298 model: Some("gpt-5".to_string()),
4299 item_id: "rs_secret".to_string(),
4300 summary: vec!["safe summary mentioning content and thinking".to_string()],
4304 token_count: Some(1),
4305 };
4306
4307 let value = serde_json::to_value(&data).expect("serializable");
4308 let object = value.as_object().expect("data serializes to JSON object");
4309 for forbidden in [
4310 "content",
4311 "reasoning_text",
4312 "thinking",
4313 "reasoning_content",
4314 "raw_reasoning",
4315 "encrypted_content",
4318 "signature",
4319 "encrypted",
4320 ] {
4321 assert!(
4322 !object.contains_key(forbidden),
4323 "ReasonItemData JSON must not expose `{forbidden}` key, got: {object:?}",
4324 );
4325 }
4326 assert!(object.contains_key("summary"));
4328 }
4329
4330 #[test]
4331 fn test_llm_generation_ttft_omitted_when_none() {
4332 let messages = vec![Message::user("test")];
4333 let data = LlmGenerationData::success(
4334 messages,
4335 vec![],
4336 Some("response".to_string()),
4337 vec![],
4338 "model".to_string(),
4339 None,
4340 None,
4341 None,
4342 None, );
4344
4345 assert!(data.metadata.time_to_first_token_ms.is_none());
4347
4348 let json = serde_json::to_value(&data).unwrap();
4350 assert!(json["metadata"].get("time_to_first_token_ms").is_none());
4351 }
4352}
4353
4354#[cfg(test)]
4363mod contract_tests {
4364 use super::*;
4365 use insta::{assert_json_snapshot, with_settings};
4366 use serde_json::json;
4367
4368 fn test_session_id() -> SessionId {
4370 SessionId::from_uuid(uuid::Uuid::from_u128(
4371 0x0000_0000_0000_0000_0000_0000_0000_0001,
4372 ))
4373 }
4374
4375 fn test_turn_id() -> TurnId {
4376 TurnId::from_uuid(uuid::Uuid::from_u128(
4377 0x0000_0000_0000_0000_0000_0000_0000_0002,
4378 ))
4379 }
4380
4381 fn test_message_id() -> MessageId {
4382 MessageId::from_uuid(uuid::Uuid::from_u128(
4383 0x0000_0000_0000_0000_0000_0000_0000_0003,
4384 ))
4385 }
4386
4387 fn test_agent_id() -> AgentId {
4388 AgentId::from_uuid(uuid::Uuid::from_u128(
4389 0x0000_0000_0000_0000_0000_0000_0000_0004,
4390 ))
4391 }
4392
4393 fn test_model_id() -> ModelId {
4394 ModelId::from_uuid(uuid::Uuid::from_u128(
4395 0x0000_0000_0000_0000_0000_0000_0000_0006,
4396 ))
4397 }
4398
4399 fn test_harness_id() -> HarnessId {
4400 HarnessId::from_uuid(uuid::Uuid::from_u128(
4401 0x0000_0000_0000_0000_0000_0000_0000_0005,
4402 ))
4403 }
4404
4405 #[test]
4412 fn snapshot_input_message() {
4413 let data = InputMessageData::new(Message::user("Hello, world!"));
4414 with_settings!({
4415 sort_maps => true,
4416 }, {
4417 assert_json_snapshot!("event_data_input_message", data, {
4419 ".message.id" => "[MESSAGE_ID]",
4420 ".message.created_at" => "[TIMESTAMP]"
4421 });
4422 });
4423 }
4424
4425 #[test]
4426 fn snapshot_output_message_started() {
4427 let data = OutputMessageStartedData {
4428 reasoning_state: None,
4429 turn_id: test_turn_id(),
4430 message_id: test_message_id(),
4431 model: Some("gpt-5.2".to_string()),
4432 iteration: None,
4433 phase: None,
4434 };
4435 with_settings!({
4436 sort_maps => true,
4437 }, {
4438 assert_json_snapshot!("event_data_output_message_started", data);
4439 });
4440 }
4441
4442 #[test]
4443 fn snapshot_output_message_delta() {
4444 let data = OutputMessageDeltaData {
4445 turn_id: test_turn_id(),
4446 message_id: test_message_id(),
4447 delta: "Hello".to_string(),
4448 accumulated: "Well, Hello".to_string(),
4449 phase: None,
4450 };
4451 with_settings!({
4452 sort_maps => true,
4453 }, {
4454 assert_json_snapshot!("event_data_output_message_delta", data);
4455 });
4456 }
4457
4458 #[test]
4459 fn snapshot_output_message_completed() {
4460 let data = OutputMessageCompletedData::new(Message::assistant("Hello!"));
4461 with_settings!({
4462 sort_maps => true,
4463 }, {
4464 assert_json_snapshot!("event_data_output_message_completed", data, {
4466 ".message.id" => "[MESSAGE_ID]",
4467 ".message.created_at" => "[TIMESTAMP]"
4468 });
4469 });
4470 }
4471
4472 #[test]
4473 fn snapshot_turn_started() {
4474 let data = TurnStartedData {
4475 turn_id: test_turn_id(),
4476 input_message_id: test_message_id(),
4477 input_content: Some("Hello".to_string()),
4478 agent_id: None,
4479 agent_name: None,
4480 agent_description: None,
4481 };
4482 with_settings!({
4483 sort_maps => true,
4484 }, {
4485 assert_json_snapshot!("event_data_turn_started", data);
4486 });
4487 }
4488
4489 #[test]
4490 fn snapshot_turn_completed() {
4491 let data = TurnCompletedData {
4492 turn_id: test_turn_id(),
4493 iterations: 3,
4494 duration_ms: Some(1500),
4495 usage: Some(TokenUsage::new(100, 50)),
4496 input_content: None,
4497 final_message_id: Some(test_message_id()),
4498 final_answer_preview: Some("Done.".to_string()),
4499 time_to_first_token_ms: Some(120),
4500 tool_call_count: Some(2),
4501 llm_call_count: Some(3),
4502 status: Some("completed".to_string()),
4503 };
4504 with_settings!({
4505 sort_maps => true,
4506 }, {
4507 assert_json_snapshot!("event_data_turn_completed", data);
4508 });
4509 }
4510
4511 #[test]
4512 fn snapshot_turn_failed() {
4513 let data = TurnFailedData {
4514 turn_id: test_turn_id(),
4515 error: "Rate limit exceeded".to_string(),
4516 error_code: Some("RATE_LIMIT".to_string()),
4517 error_fields: None,
4518 error_disclosure: None,
4519 };
4520 with_settings!({
4521 sort_maps => true,
4522 }, {
4523 assert_json_snapshot!("event_data_turn_failed", data);
4524 });
4525 }
4526
4527 #[test]
4528 fn snapshot_turn_cancelled() {
4529 let data = TurnCancelledData {
4530 turn_id: test_turn_id(),
4531 reason: Some("User requested".to_string()),
4532 usage: Some(TokenUsage::new(50, 25)),
4533 };
4534 with_settings!({
4535 sort_maps => true,
4536 }, {
4537 assert_json_snapshot!("event_data_turn_cancelled", data);
4538 });
4539 }
4540
4541 #[test]
4542 fn snapshot_reason_started() {
4543 let data = ReasonStartedData {
4544 harness_id: test_harness_id(),
4545 agent_id: Some(test_agent_id()),
4546 metadata: Some(ModelMetadata {
4547 model: "gpt-5.2".to_string(),
4548 model_id: None,
4549 provider_id: None,
4550 }),
4551 };
4552 with_settings!({
4553 sort_maps => true,
4554 }, {
4555 assert_json_snapshot!("event_data_reason_started", data);
4556 });
4557 }
4558
4559 #[test]
4560 fn snapshot_reason_completed() {
4561 let data = ReasonCompletedData::success(
4562 "Hello world",
4563 true,
4564 2,
4565 Some(1000),
4566 Some(TokenUsage::new(100, 50)),
4567 );
4568 with_settings!({
4569 sort_maps => true,
4570 }, {
4571 assert_json_snapshot!("event_data_reason_completed", data);
4572 });
4573 }
4574
4575 #[test]
4576 fn snapshot_act_started() {
4577 let data = ActStartedData {
4578 tool_calls: vec![ToolCallSummary {
4579 id: "tc_1".to_string(),
4580 name: "get_weather".to_string(),
4581 display_name: None,
4582 narration: None,
4583 completed_narration: None,
4584 }],
4585 headline: None,
4586 };
4587 with_settings!({
4588 sort_maps => true,
4589 }, {
4590 assert_json_snapshot!("event_data_act_started", data);
4591 });
4592 }
4593
4594 #[test]
4595 fn snapshot_act_completed() {
4596 let data = ActCompletedData {
4597 completed: true,
4598 success_count: 2,
4599 error_count: 0,
4600 duration_ms: Some(500),
4601 headline: None,
4602 };
4603 with_settings!({
4604 sort_maps => true,
4605 }, {
4606 assert_json_snapshot!("event_data_act_completed", data);
4607 });
4608 }
4609
4610 #[test]
4611 fn snapshot_tool_started() {
4612 let data = ToolStartedData {
4613 tool_call: ToolCall {
4614 id: "tc_1".to_string(),
4615 name: "get_weather".to_string(),
4616 arguments: serde_json::json!({"city": "London"}),
4617 },
4618 tool_call_fingerprint: None,
4619 display_name: None,
4620 narration: None,
4621 };
4622 with_settings!({
4623 sort_maps => true,
4624 }, {
4625 assert_json_snapshot!("event_data_tool_started", data);
4626 });
4627 }
4628
4629 #[test]
4630 fn snapshot_tool_completed() {
4631 let data = ToolCompletedData::success(
4632 "tc_1".to_string(),
4633 "get_weather".to_string(),
4634 vec![crate::message::ContentPart::text("Sunny, 22°C")],
4635 Some(250),
4636 );
4637 with_settings!({
4638 sort_maps => true,
4639 }, {
4640 assert_json_snapshot!("event_data_tool_completed", data);
4641 });
4642 }
4643
4644 #[test]
4645 fn snapshot_llm_generation() {
4646 let data = LlmGenerationData::success(
4647 vec![Message::user("Hello")],
4648 vec![ToolDefinitionSummary {
4649 name: "tool1".to_string(),
4650 display_name: None,
4651 category: None,
4652 capability_id: None,
4653 capability_name: None,
4654 description: "A tool".to_string(),
4655 }],
4656 Some("Hi there!".to_string()),
4657 vec![],
4658 "gpt-5.2".to_string(),
4659 Some("openai".to_string()),
4660 Some(TokenUsage::new(10, 5)),
4661 Some(100),
4662 Some(25),
4663 );
4664 with_settings!({
4665 sort_maps => true,
4666 }, {
4667 assert_json_snapshot!("event_data_llm_generation", data, {
4669 ".messages[].id" => "[MESSAGE_ID]",
4670 ".messages[].created_at" => "[TIMESTAMP]"
4671 });
4672 });
4673 }
4674
4675 #[test]
4676 fn snapshot_reason_thinking_started() {
4677 let data = ReasonThinkingStartedData {
4678 turn_id: test_turn_id(),
4679 model: Some("claude-4-opus".to_string()),
4680 };
4681 with_settings!({
4682 sort_maps => true,
4683 }, {
4684 assert_json_snapshot!("event_data_reason_thinking_started", data);
4685 });
4686 }
4687
4688 #[test]
4689 fn snapshot_reason_thinking_delta() {
4690 let data = ReasonThinkingDeltaData {
4691 turn_id: test_turn_id(),
4692 delta: "Let me think...".to_string(),
4693 accumulated: "First thought. Let me think...".to_string(),
4694 };
4695 with_settings!({
4696 sort_maps => true,
4697 }, {
4698 assert_json_snapshot!("event_data_reason_thinking_delta", data);
4699 });
4700 }
4701
4702 #[test]
4703 fn snapshot_reason_thinking_completed() {
4704 let data = ReasonThinkingCompletedData {
4705 turn_id: test_turn_id(),
4706 thinking: "I need to consider...".to_string(),
4707 };
4708 with_settings!({
4709 sort_maps => true,
4710 }, {
4711 assert_json_snapshot!("event_data_reason_thinking_completed", data);
4712 });
4713 }
4714
4715 #[test]
4716 fn snapshot_reason_item() {
4717 let data = ReasonItemData {
4718 turn_id: test_turn_id(),
4719 provider: "openai".to_string(),
4720 model: Some("gpt-5.5".to_string()),
4721 item_id: "rs_test".to_string(),
4722 summary: vec!["safe summary".to_string()],
4723 token_count: Some(42),
4724 };
4725 with_settings!({
4726 sort_maps => true,
4727 }, {
4728 assert_json_snapshot!("event_data_reason_item", data);
4729 });
4730 }
4731
4732 #[test]
4733 fn snapshot_session_started() {
4734 let data = SessionStartedData {
4735 harness_id: test_harness_id(),
4736 agent_id: Some(test_agent_id()),
4737 model_id: None,
4738 };
4739 with_settings!({
4740 sort_maps => true,
4741 }, {
4742 assert_json_snapshot!("event_data_session_started", data);
4743 });
4744 }
4745
4746 #[test]
4747 fn snapshot_session_activated() {
4748 let data = SessionActivatedData {
4749 turn_id: test_turn_id(),
4750 input_message_id: test_message_id(),
4751 };
4752 with_settings!({
4753 sort_maps => true,
4754 }, {
4755 assert_json_snapshot!("event_data_session_activated", data);
4756 });
4757 }
4758
4759 #[test]
4760 fn snapshot_session_idled() {
4761 let data = SessionIdledData {
4762 turn_id: test_turn_id(),
4763 iterations: Some(3),
4764 usage: Some(TokenUsage::new(500, 200)),
4765 };
4766 with_settings!({
4767 sort_maps => true,
4768 }, {
4769 assert_json_snapshot!("event_data_session_idled", data);
4770 });
4771 }
4772
4773 #[test]
4774 fn snapshot_session_title_updated() {
4775 let data = SessionTitleUpdatedData {
4776 previous_title: Some("Old title".to_string()),
4777 title: "Automatic Session Titles".to_string(),
4778 };
4779 with_settings!({
4780 sort_maps => true,
4781 }, {
4782 assert_json_snapshot!("event_data_session_title_updated", data);
4783 });
4784
4785 let untitled = serde_json::to_value(SessionTitleUpdatedData {
4786 previous_title: None,
4787 title: "First title".to_string(),
4788 })
4789 .expect("serialize title event");
4790 assert!(untitled["previous_title"].is_null());
4791 }
4792
4793 #[test]
4799 fn tool_call_summary_display_name_wire_contract() {
4800 for display_name in [None, Some("Get Weather")] {
4801 let summary = ToolCallSummary {
4802 id: "tc_1".into(),
4803 name: "get_weather".into(),
4804 display_name: display_name.map(str::to_owned),
4805 narration: None,
4806 completed_narration: None,
4807 };
4808 let mut expected = serde_json::json!({"id": "tc_1", "name": "get_weather"});
4809 if let Some(name) = display_name {
4810 expected["display_name"] = name.into();
4811 }
4812 assert_eq!(serde_json::to_value(summary).unwrap(), expected);
4813 let decoded: ToolCallSummary = serde_json::from_value(expected.clone()).unwrap();
4814 assert_eq!(decoded.display_name.as_deref(), display_name);
4815 assert_eq!(serde_json::to_value(decoded).unwrap(), expected);
4816 }
4817 }
4818
4819 #[test]
4820 fn act_started_with_definitions_populates_display_names() {
4821 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4822
4823 let tool_calls = vec![
4824 ToolCall {
4825 id: "tc_1".to_string(),
4826 name: "get_weather".to_string(),
4827 arguments: serde_json::json!({}),
4828 },
4829 ToolCall {
4830 id: "tc_2".to_string(),
4831 name: "unknown_tool".to_string(),
4832 arguments: serde_json::json!({}),
4833 },
4834 ];
4835 let tool_defs = vec![crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4836 name: "get_weather".to_string(),
4837 display_name: Some("Get Weather".to_string()),
4838 description: "Gets weather".to_string(),
4839 parameters: serde_json::json!({}),
4840 policy: ToolPolicy::Auto,
4841 category: None,
4842 deferrable: DeferrablePolicy::default(),
4843 hints: crate::tool_types::ToolHints::default(),
4844 full_parameters: None,
4845 })];
4846
4847 let data = ActStartedData::with_definitions(&tool_calls, &tool_defs);
4848 assert_eq!(data.tool_calls.len(), 2);
4849 assert_eq!(
4850 data.tool_calls[0].display_name.as_deref(),
4851 Some("Get Weather")
4852 );
4853 assert_eq!(
4854 data.tool_calls[0].narration.as_deref(),
4855 Some("Running Get Weather")
4856 );
4857 assert_eq!(
4858 data.tool_calls[0].completed_narration.as_deref(),
4859 Some("Ran Get Weather")
4860 );
4861 assert_eq!(data.tool_calls[1].display_name, None);
4862 }
4863
4864 #[test]
4865 fn tool_completed_with_display_name_roundtrip() {
4866 let data = ToolCompletedData::success(
4867 "tc_1".to_string(),
4868 "get_weather".to_string(),
4869 vec![crate::message::ContentPart::text("Sunny")],
4870 Some(100),
4871 )
4872 .with_display_name(Some("Get Weather".to_string()));
4873
4874 assert_eq!(data.display_name.as_deref(), Some("Get Weather"));
4875
4876 let json = serde_json::to_value(&data).unwrap();
4877 assert_eq!(json["display_name"], "Get Weather");
4878
4879 let deserialized: ToolCompletedData = serde_json::from_value(json).unwrap();
4880 assert_eq!(deserialized.display_name.as_deref(), Some("Get Weather"));
4881 }
4882
4883 #[test]
4884 fn tool_started_display_name_serialization() {
4885 let data = ToolStartedData {
4886 tool_call: ToolCall {
4887 id: "tc_1".to_string(),
4888 name: "bash".to_string(),
4889 arguments: serde_json::json!({"command": "ls"}),
4890 },
4891 tool_call_fingerprint: None,
4892 display_name: Some("Bash".to_string()),
4893 narration: None,
4894 };
4895
4896 let json = serde_json::to_value(&data).unwrap();
4897 assert_eq!(json["display_name"], "Bash");
4898 }
4899
4900 #[test]
4901 fn tool_definition_summary_display_name() {
4902 use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolPolicy};
4903
4904 let def = crate::tool_types::ToolDefinition::Builtin(BuiltinTool {
4905 name: "read_file".to_string(),
4906 display_name: Some("Read File".to_string()),
4907 description: "Reads a file".to_string(),
4908 parameters: serde_json::json!({}),
4909 policy: ToolPolicy::Auto,
4910 category: None,
4911 deferrable: DeferrablePolicy::default(),
4912 hints: crate::tool_types::ToolHints::default(),
4913 full_parameters: None,
4914 });
4915
4916 let summary = ToolDefinitionSummary::from(&def);
4917 assert_eq!(summary.display_name.as_deref(), Some("Read File"));
4918
4919 let json = serde_json::to_value(&summary).unwrap();
4920 assert_eq!(json["display_name"], "Read File");
4921 }
4922
4923 #[test]
4930 fn forward_compat_unknown_fields_ignored() {
4931 let json = r#"{
4933 "turn_id": "turn_00000000000000000000000000000002",
4934 "iterations": 3,
4935 "duration_ms": 1500,
4936 "usage": {"input_tokens": 100, "output_tokens": 50},
4937 "future_field": "should be ignored",
4938 "another_new_field": 42
4939 }"#;
4940
4941 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4942 assert_eq!(data.iterations, 3);
4943 assert_eq!(data.duration_ms, Some(1500));
4944 assert_eq!(data.turn_id, test_turn_id());
4945 assert_eq!(
4946 serde_json::to_value(data.usage).unwrap(),
4947 json!({"input_tokens": 100, "output_tokens": 50})
4948 );
4949 }
4950
4951 #[test]
4952 fn forward_compat_unsupported_preserves_data() {
4953 for (kind, original) in [
4956 (
4957 "unknown.event",
4958 serde_json::json!({"key": "value", "nested": {"a": 1}}),
4959 ),
4960 (
4961 "turn.completed",
4962 serde_json::json!({"iterations": "invalid"}),
4963 ),
4964 ] {
4965 let data = deserialize_event_data(kind, original.clone());
4966 assert!(data.is_unsupported());
4967 assert_eq!(data.event_type(), "unsupported");
4968 match data {
4969 EventData::Unsupported { event_type, data } => {
4970 assert_eq!(event_type, kind);
4971 assert_eq!(data, original);
4972 }
4973 _ => panic!("Expected Unsupported variant"),
4974 }
4975 }
4976 }
4977
4978 #[test]
4979 fn forward_compat_optional_fields_absent() {
4980 let json = r#"{
4982 "turn_id": "turn_00000000000000000000000000000002",
4983 "iterations": 3
4984 }"#;
4985
4986 let data: TurnCompletedData = serde_json::from_str(json).unwrap();
4987 assert_eq!(data.iterations, 3);
4988 assert!(data.duration_ms.is_none());
4989 assert!(data.usage.is_none());
4990 assert!(data.input_content.is_none());
4991 assert!(data.final_message_id.is_none());
4992 assert!(data.final_answer_preview.is_none());
4993 assert!(data.time_to_first_token_ms.is_none());
4994 assert!(data.tool_call_count.is_none());
4995 assert!(data.llm_call_count.is_none());
4996 assert!(data.status.is_none());
4997 }
4998
4999 #[test]
5005 fn representative_event_payloads_preserve_wire_identity() {
5006 let test_cases: Vec<(&str, EventData)> = vec![
5008 (
5009 "input.message",
5010 InputMessageData::new(Message::user("test")).into(),
5011 ),
5012 (
5013 "output.message.started",
5014 OutputMessageStartedData {
5015 reasoning_state: None,
5016 turn_id: test_turn_id(),
5017 message_id: test_message_id(),
5018 model: None,
5019 iteration: None,
5020 phase: None,
5021 }
5022 .into(),
5023 ),
5024 (
5025 "output.message.delta",
5026 OutputMessageDeltaData {
5027 turn_id: test_turn_id(),
5028 message_id: test_message_id(),
5029 delta: "x".to_string(),
5030 accumulated: "x".to_string(),
5031 phase: None,
5032 }
5033 .into(),
5034 ),
5035 (
5036 "output.message.completed",
5037 OutputMessageCompletedData::new(Message::assistant("hi")).into(),
5038 ),
5039 (
5040 "turn.started",
5041 TurnStartedData {
5042 turn_id: test_turn_id(),
5043 input_message_id: test_message_id(),
5044 input_content: None,
5045 agent_id: None,
5046 agent_name: None,
5047 agent_description: None,
5048 }
5049 .into(),
5050 ),
5051 (
5052 "turn.completed",
5053 TurnCompletedData {
5054 turn_id: test_turn_id(),
5055 iterations: 1,
5056 duration_ms: None,
5057 usage: None,
5058 input_content: None,
5059 final_message_id: None,
5060 final_answer_preview: None,
5061 time_to_first_token_ms: None,
5062 tool_call_count: None,
5063 llm_call_count: None,
5064 status: None,
5065 }
5066 .into(),
5067 ),
5068 (
5069 "turn.failed",
5070 TurnFailedData {
5071 turn_id: test_turn_id(),
5072 error: "err".to_string(),
5073 error_code: None,
5074 error_fields: None,
5075 error_disclosure: None,
5076 }
5077 .into(),
5078 ),
5079 (
5080 "turn.cancelled",
5081 TurnCancelledData {
5082 turn_id: test_turn_id(),
5083 reason: None,
5084 usage: None,
5085 }
5086 .into(),
5087 ),
5088 (
5089 "turn.sealed",
5090 TurnSealedData {
5091 turn_id: test_turn_id(),
5092 reason: "no_progress".to_string(),
5093 detail: Some("sealed".to_string()),
5094 iterations: Some(3),
5095 usage: None,
5096 }
5097 .into(),
5098 ),
5099 (
5100 "reason.started",
5101 ReasonStartedData {
5102 harness_id: test_harness_id(),
5103 agent_id: Some(test_agent_id()),
5104 metadata: None,
5105 }
5106 .into(),
5107 ),
5108 (
5109 "reason.completed",
5110 ReasonCompletedData::success("", false, 0, None, None).into(),
5111 ),
5112 (
5113 "act.started",
5114 ActStartedData {
5115 tool_calls: vec![],
5116 headline: None,
5117 }
5118 .into(),
5119 ),
5120 (
5121 "act.completed",
5122 ActCompletedData {
5123 completed: true,
5124 success_count: 0,
5125 error_count: 0,
5126 duration_ms: None,
5127 headline: None,
5128 }
5129 .into(),
5130 ),
5131 (
5132 "session.started",
5133 SessionStartedData {
5134 harness_id: test_harness_id(),
5135 agent_id: Some(test_agent_id()),
5136 model_id: None,
5137 }
5138 .into(),
5139 ),
5140 (
5141 "session.activated",
5142 SessionActivatedData {
5143 turn_id: test_turn_id(),
5144 input_message_id: test_message_id(),
5145 }
5146 .into(),
5147 ),
5148 (
5149 "session.idled",
5150 SessionIdledData {
5151 turn_id: test_turn_id(),
5152 iterations: None,
5153 usage: None,
5154 }
5155 .into(),
5156 ),
5157 (
5158 "session.title.updated",
5159 SessionTitleUpdatedData {
5160 previous_title: Some("Old title".to_string()),
5161 title: "New title".to_string(),
5162 }
5163 .into(),
5164 ),
5165 (
5166 "session.model.changed",
5167 SessionModelChangedData {
5168 previous_model_id: Some(test_model_id()),
5169 previous_model_name: Some("GPT-5.6 Sol".to_string()),
5170 model_id: test_model_id(),
5171 model_name: "GPT-5.6 Terra".to_string(),
5172 }
5173 .into(),
5174 ),
5175 (
5176 "tool.started",
5177 ToolStartedData {
5178 tool_call: ToolCall {
5179 id: "call_1".into(),
5180 name: "lookup".into(),
5181 arguments: json!({"q": "test"}),
5182 },
5183 tool_call_fingerprint: None,
5184 display_name: Some("Lookup".into()),
5185 narration: None,
5186 }
5187 .into(),
5188 ),
5189 (
5190 "tool.completed",
5191 ToolCompletedData::success(
5192 "call_1".into(),
5193 "lookup".into(),
5194 vec![ContentPart::text("result")],
5195 Some(7),
5196 )
5197 .into(),
5198 ),
5199 (
5200 "llm.generation",
5201 LlmGenerationData::success(
5202 vec![Message::user("prompt")],
5203 vec![],
5204 Some("answer".into()),
5205 vec![],
5206 "model".into(),
5207 None,
5208 None,
5209 Some(9),
5210 Some(2),
5211 )
5212 .into(),
5213 ),
5214 (
5215 "reason.thinking.started",
5216 ReasonThinkingStartedData {
5217 turn_id: test_turn_id(),
5218 model: Some("reasoner".into()),
5219 }
5220 .into(),
5221 ),
5222 (
5223 "reason.thinking.delta",
5224 ReasonThinkingDeltaData {
5225 turn_id: test_turn_id(),
5226 delta: "next".into(),
5227 accumulated: "first next".into(),
5228 }
5229 .into(),
5230 ),
5231 (
5232 "reason.thinking.completed",
5233 ReasonThinkingCompletedData {
5234 turn_id: test_turn_id(),
5235 thinking: "complete thought".into(),
5236 }
5237 .into(),
5238 ),
5239 (
5240 "reason.item",
5241 ReasonItemData {
5242 turn_id: test_turn_id(),
5243 provider: "openai".into(),
5244 model: Some("reasoner".into()),
5245 item_id: "rs_1".into(),
5246 summary: vec!["safe".into()],
5247 token_count: Some(42),
5248 }
5249 .into(),
5250 ),
5251 ];
5252
5253 for (event_type, original) in test_cases {
5254 assert_eq!(original.event_type(), event_type);
5255 assert!(
5256 VALID_EVENT_TYPES.contains(&event_type),
5257 "filter must accept {event_type}"
5258 );
5259 assert!(!original.is_unsupported());
5260 let json = serde_json::to_value(&original).unwrap();
5261 let deserialized = deserialize_event_data(event_type, json.clone());
5262 assert_eq!(
5263 std::mem::discriminant(&original),
5264 std::mem::discriminant(&deserialized),
5265 "{event_type}"
5266 );
5267 assert_eq!(
5268 serde_json::to_value(deserialized).unwrap(),
5269 json,
5270 "{event_type}"
5271 );
5272 let request = EventRequest::new(test_session_id(), EventContext::empty(), original);
5273 assert_eq!(serde_json::to_value(request).unwrap()["type"], event_type);
5274 }
5275 }
5276
5277 #[test]
5283 fn event_structure_has_required_fields() {
5284 let ts = "2026-01-02T03:04:05Z".parse::<DateTime<Utc>>().unwrap();
5285 let message = Message::user("test").with_id(test_message_id());
5286 let message_json = serde_json::to_value(&message).unwrap();
5287 let event_id = EventId::from_uuid(Uuid::from_u128(7));
5288 let context = EventContext::turn(test_turn_id(), test_message_id());
5289 let mut event = Event::with_id(
5290 event_id,
5291 test_session_id(),
5292 context,
5293 InputMessageData::new(message),
5294 )
5295 .with_metadata(json!({"source": "replay"}))
5296 .with_tags(vec!["audit".into()])
5297 .with_sequence(12);
5298 event.ts = ts;
5299 let expected = json!({
5300 "id": "event_00000000000000000000000000000007",
5301 "type": "input.message", "ts": "2026-01-02T03:04:05Z",
5302 "session_id": "session_00000000000000000000000000000001",
5303 "context": {"turn_id": "turn_00000000000000000000000000000002",
5304 "input_message_id": "message_00000000000000000000000000000003"},
5305 "data": {"message": message_json}, "metadata": {"source": "replay"},
5306 "tags": ["audit"], "sequence": 12
5307 });
5308 assert_eq!(serde_json::to_value(&event).unwrap(), expected);
5309 let decoded: Event = serde_json::from_value(expected.clone()).unwrap();
5310 assert!(matches!(&decoded.data, EventData::InputMessage(_)));
5311 assert_eq!(serde_json::to_value(decoded).unwrap(), expected);
5312
5313 let mut request_json = expected.clone();
5314 request_json.as_object_mut().unwrap().remove("id");
5315 request_json.as_object_mut().unwrap().remove("sequence");
5316 let request: EventRequest = serde_json::from_value(request_json.clone()).unwrap();
5317 assert_eq!(serde_json::to_value(&request).unwrap(), request_json);
5318 assert_eq!(
5319 serde_json::to_value(request.into_event(event_id, 12)).unwrap(),
5320 expected
5321 );
5322 }
5323
5324 #[test]
5325 fn event_context_span_fields() {
5326 let context = EventContext::empty().with_span(
5327 "trace123".to_string(),
5328 "span456".to_string(),
5329 Some("parent789".to_string()),
5330 );
5331
5332 let json = serde_json::to_value(&context).unwrap();
5333 assert_eq!(
5334 json.get("trace_id").and_then(|v| v.as_str()),
5335 Some("trace123")
5336 );
5337 assert_eq!(
5338 json.get("span_id").and_then(|v| v.as_str()),
5339 Some("span456")
5340 );
5341 assert_eq!(
5342 json.get("parent_span_id").and_then(|v| v.as_str()),
5343 Some("parent789")
5344 );
5345 }
5346}