1use serde::Deserialize;
4use serde::Deserializer;
5use serde::Serialize;
6
7pub use self::replay::events as replay_events;
8pub(crate) use self::replay::{
9 ATTACHMENTS_FIELD, CONTEXT_COMPACTED_MARKER, INTERNAL_MESSAGE_FIELD, REPLAY_REASONING_FIELD,
10 TOOL_ERROR_FIELD, internal_message_kind, is_internal_message, strip_attachment_references,
11};
12
13mod replay;
14
15pub const MAX_USER_INPUT_BYTES: usize = 1024 * 1024;
17
18pub const MAX_CAPABILITY_INPUT_BYTES: usize = 64 * 1024;
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(deny_unknown_fields)]
26pub struct SessionFileReference {
27 pub id: String,
28 pub name: String,
29 pub size: u64,
30 pub media_type: String,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct Submission {
36 pub id: String,
38 pub op: Op,
40}
41
42#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
48pub struct SessionContext {
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub tenant_id: Option<String>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub user_id: Option<String>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub user_name: Option<String>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub workspace_id: Option<String>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub workspace_label: Option<String>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub origin_label: Option<String>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(tag = "type", rename_all = "snake_case")]
72#[non_exhaustive]
73pub enum Op {
74 UserInput {
76 text: String,
77 attachments: Vec<SessionFileReference>,
78 },
79 ActiveInput {
81 operation: String,
82 turn_id: String,
83 text: String,
84 },
85 Interrupt { turn_id: String },
87 ExecApproval {
89 id: String,
90 decision: ReviewDecision,
91 },
92 CapabilityCommand {
94 capability: String,
95 command: String,
96 arguments: String,
97 #[serde(deserialize_with = "required_option")]
101 input: Option<String>,
102 #[serde(deserialize_with = "required_option")]
103 target: Option<MessageTarget>,
104 },
105 SetModel { route: String },
107 ResumeSession { session_id: String },
109}
110
111fn required_option<'de, D, T>(deserializer: D) -> std::result::Result<Option<T>, D::Error>
112where
113 D: Deserializer<'de>,
114 T: Deserialize<'de>,
115{
116 Option::deserialize(deserializer)
117}
118
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
121pub struct Event {
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub submission_id: Option<String>,
125 pub msg: EventMsg,
127}
128
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131#[serde(tag = "type", rename_all = "snake_case")]
132#[non_exhaustive]
133pub enum EventMsg {
134 Error(ErrorEvent),
135 Warning(WarningEvent),
136 SessionConfigured(SessionConfiguredEvent),
137 #[serde(rename = "task_started")]
138 TurnStarted(TurnStartedEvent),
139 #[serde(rename = "task_complete")]
140 TurnComplete(TurnCompleteEvent),
141 TurnAborted(TurnAbortedEvent),
142 UserMessage(UserMessageEvent),
143 AgentMessage(AgentMessageEvent),
144 AgentMessageContentDelta(AgentMessageContentDeltaEvent),
145 AgentReasoningContentDelta(AgentReasoningContentDeltaEvent),
146 ModelStepStarted(ModelStepStartedEvent),
147 ModelStepCompleted(ModelStepCompletedEvent),
148 SessionHistory(SessionHistoryEvent),
149 ModelChanged(ModelChangedEvent),
150 SessionResumeRequested(SessionResumeRequestedEvent),
151 ToolCallBegin(ToolCallBeginEvent),
152 ToolCallEnd(ToolCallEndEvent),
153 ExecApprovalRequest(ExecApprovalRequestEvent),
154 ExecApprovalReview(ExecApprovalReviewEvent),
155 TokenCount(TokenCountEvent),
156 ContextCompacted,
157 WebSearchBegin(WebSearchBeginEvent),
158 WebSearchEnd(WebSearchEndEvent),
159 Frontend(FrontendEvent),
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
164pub enum ModelEvent {
165 TextDelta(String),
166 CommentaryDelta(String),
167 ReasoningDelta(String),
168 WebSearchStarted {
169 call_id: String,
170 },
171 WebSearchCompleted {
172 call_id: String,
173 action: WebSearchAction,
174 },
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(tag = "type", rename_all = "snake_case")]
180pub enum WebSearchAction {
181 Search {
182 queries: Vec<String>,
183 },
184 OpenPage {
185 url: Option<String>,
186 },
187 FindInPage {
188 url: Option<String>,
189 pattern: Option<String>,
190 },
191 Interrupted,
192 Other,
193}
194
195impl ModelEvent {
196 #[must_use]
198 pub fn into_event(self, session_id: &str, turn_id: &str, model_step_id: &str) -> EventMsg {
199 match self {
200 Self::TextDelta(delta) => {
201 EventMsg::AgentMessageContentDelta(AgentMessageContentDeltaEvent {
202 session_id: session_id.into(),
203 turn_id: turn_id.into(),
204 model_step_id: model_step_id.into(),
205 delta,
206 phase: AgentMessagePhase::FinalAnswer,
207 })
208 }
209 Self::CommentaryDelta(delta) => {
210 EventMsg::AgentMessageContentDelta(AgentMessageContentDeltaEvent {
211 session_id: session_id.into(),
212 turn_id: turn_id.into(),
213 model_step_id: model_step_id.into(),
214 delta,
215 phase: AgentMessagePhase::Commentary,
216 })
217 }
218 Self::ReasoningDelta(delta) => {
219 EventMsg::AgentReasoningContentDelta(AgentReasoningContentDeltaEvent {
220 session_id: session_id.into(),
221 turn_id: turn_id.into(),
222 model_step_id: model_step_id.into(),
223 delta,
224 })
225 }
226 Self::WebSearchStarted { call_id } => EventMsg::WebSearchBegin(WebSearchBeginEvent {
227 session_id: session_id.into(),
228 turn_id: turn_id.into(),
229 model_step_id: model_step_id.into(),
230 call_id,
231 }),
232 Self::WebSearchCompleted { call_id, action } => {
233 EventMsg::WebSearchEnd(WebSearchEndEvent {
234 session_id: session_id.into(),
235 turn_id: turn_id.into(),
236 model_step_id: model_step_id.into(),
237 call_id,
238 action,
239 })
240 }
241 }
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247pub struct FrontendCommand {
248 pub name: String,
249 pub arguments: String,
250 pub description: String,
251}
252
253#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
255pub struct FrontendContribution {
256 pub capability: String,
257 pub accepts_file_attachments: bool,
259 pub count: Option<usize>,
261 pub commands: Vec<FrontendCommand>,
262 pub widgets: Vec<FrontendWidget>,
263 pub references: Vec<FrontendReference>,
264 pub active_input: Option<FrontendActiveInput>,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
269pub struct MiddlewareFeature {
270 pub id: String,
271 pub label: String,
272 pub description: String,
273 pub required: bool,
274 pub settings: Vec<FrontendSetting>,
275}
276
277#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
279pub struct FrontendSetting {
280 pub id: String,
281 pub label: String,
282 pub description: String,
283 #[serde(flatten)]
284 pub kind: FrontendSettingKind,
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
289#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
290pub enum FrontendSettingKind {
291 Integer {
292 min: i64,
293 #[serde(default, skip_serializing_if = "Option::is_none")]
294 max: Option<i64>,
295 step: i64,
296 },
297 Select {
298 options: Vec<FrontendSettingOption>,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
300 unset_label: Option<String>,
301 },
302}
303
304#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
306pub struct FrontendSettingOption {
307 pub value: String,
308 pub label: String,
309 pub description: String,
310}
311
312#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
314#[serde(untagged)]
315pub enum FrontendSettingValue {
316 Integer(i64),
317 String(String),
318}
319
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
322pub struct FrontendActiveInput {
323 pub operation: String,
324}
325
326#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
328pub struct FrontendReference {
329 pub trigger: char,
330 pub value: String,
331 pub description: String,
332}
333
334#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
336pub struct FrontendWidget {
337 pub id: String,
338 pub slot: FrontendSlot,
339 pub text: String,
340 pub tone: FrontendTone,
341 pub symbol: Option<FrontendSymbol>,
342 pub icon_only: bool,
343 pub progress: Option<FrontendProgress>,
344 pub content: Option<FrontendWidgetContent>,
345 pub action: Option<Op>,
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
351pub struct FrontendProgress {
352 pub completed: usize,
353 pub total: usize,
354}
355
356#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
358#[serde(tag = "type", rename_all = "snake_case")]
359pub enum FrontendWidgetContent {
360 Blocks {
361 title: String,
362 blocks: Vec<FrontendBlock>,
363 },
364 Picker {
365 title: String,
366 options: Vec<FrontendPickerOption>,
367 },
368 ActionList {
369 title: String,
370 items: Vec<FrontendActionListItem>,
371 },
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
376#[serde(rename_all = "snake_case")]
377pub enum FrontendSlot {
378 Header,
379 ComposerHeader,
380 ComposerFooter,
381 MessageActions,
382 TranscriptTail,
384 Navigation,
386 ChatMenu,
388}
389
390#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
392pub struct FrontendBlock {
393 pub id: Option<String>,
394 pub group: Option<String>,
395 pub update: FrontendBlockUpdate,
396 pub state: FrontendBlockState,
397 pub role: FrontendBlockRole,
398 pub title: String,
400 pub text: String,
402 pub symbol: Option<FrontendSymbol>,
403 pub files: Vec<SessionFileReference>,
405 pub format: FrontendBlockFormat,
406 pub tone: FrontendTone,
407}
408
409#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
411pub struct RenderedBlock {
412 pub capability: String,
413 pub block: FrontendBlock,
414}
415
416#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
418#[serde(rename_all = "snake_case")]
419pub enum FrontendBlockUpdate {
420 Replace,
421 Append,
422}
423
424#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
426#[serde(rename_all = "snake_case")]
427pub enum FrontendBlockState {
428 Pending,
429 Complete,
430}
431
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
434#[serde(rename_all = "snake_case")]
435pub enum FrontendBlockRole {
436 Activity,
437 Tool,
438 WebSearch,
439 Artifact,
440 Approval,
441 Notice,
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
446#[serde(rename_all = "snake_case")]
447pub enum FrontendBlockFormat {
448 PlainText,
449 UnifiedDiff,
450}
451
452#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
454pub struct FrontendPickerOption {
455 pub label: String,
456 pub description: String,
457 pub detail: String,
458 pub symbol: Option<FrontendSymbol>,
459 pub shows_detail: bool,
460 pub op: Op,
461}
462
463#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
465pub struct FrontendActionListItem {
466 pub id: String,
467 pub text: String,
468 pub state: FrontendListItemState,
469 pub actions: Vec<FrontendAction>,
470}
471
472#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
474#[serde(rename_all = "snake_case")]
475pub enum FrontendListItemState {
476 Plain,
477 Pending,
478 InProgress,
479 Completed,
480}
481
482#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
484pub struct FrontendAction {
485 pub id: String,
486 pub label: String,
487 pub symbol: FrontendSymbol,
488 pub tone: FrontendTone,
489 pub op: Op,
490}
491
492#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
494#[serde(tag = "frontend_type", rename_all = "snake_case")]
495pub enum FrontendEvent {
496 Render {
497 capability: String,
498 block: FrontendBlock,
499 },
500 Widget {
501 capability: String,
502 item: FrontendWidget,
503 },
504 RemoveWidget {
505 capability: String,
506 id: String,
507 },
508 Picker {
509 title: String,
510 options: Vec<FrontendPickerOption>,
511 },
512 Preview {
513 id: String,
514 title: String,
515 subtitle: String,
516 page_id: String,
517 update: FrontendPreviewUpdate,
518 events: Vec<EventMsg>,
519 next: Option<Op>,
520 },
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
525#[serde(rename_all = "snake_case")]
526pub enum FrontendPreviewUpdate {
527 Replace,
528 Prepend,
529}
530
531#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
533#[serde(rename_all = "snake_case")]
534pub enum FrontendTone {
535 Neutral,
536 Success,
537 Warning,
538 Error,
539}
540
541impl EventMsg {
542 #[must_use]
544 pub fn presentation(&self) -> Option<RenderedBlock> {
545 let block = match self {
546 Self::Error(error) => FrontendBlock {
547 id: None,
548 group: None,
549 update: FrontendBlockUpdate::Replace,
550 state: FrontendBlockState::Complete,
551 role: FrontendBlockRole::Notice,
552 title: "Error".into(),
553 text: error.message.clone(),
554 symbol: None,
555 files: Vec::new(),
556 format: FrontendBlockFormat::PlainText,
557 tone: FrontendTone::Error,
558 },
559 Self::Warning(warning) => FrontendBlock {
560 id: None,
561 group: None,
562 update: FrontendBlockUpdate::Replace,
563 state: FrontendBlockState::Complete,
564 role: FrontendBlockRole::Notice,
565 title: "Warning".into(),
566 text: warning.message.clone(),
567 symbol: None,
568 files: Vec::new(),
569 format: FrontendBlockFormat::PlainText,
570 tone: FrontendTone::Warning,
571 },
572 Self::TurnAborted(turn) => FrontendBlock {
573 id: None,
574 group: Some(turn.turn_id.clone()),
575 update: FrontendBlockUpdate::Replace,
576 state: FrontendBlockState::Complete,
577 role: FrontendBlockRole::Notice,
578 title: "Turn aborted".into(),
579 text: turn.reason.clone(),
580 symbol: None,
581 files: Vec::new(),
582 format: FrontendBlockFormat::PlainText,
583 tone: FrontendTone::Warning,
584 },
585 Self::ModelStepCompleted(step) if step.outcome == ModelStepOutcome::Retrying => {
586 FrontendBlock {
587 id: Some(format!("{}/retry", step.model_step_id)),
588 group: Some(step.turn_id.clone()),
589 update: FrontendBlockUpdate::Replace,
590 state: FrontendBlockState::Complete,
591 role: FrontendBlockRole::Notice,
592 title: "Reconnecting…".into(),
593 text: String::new(),
594 symbol: None,
595 files: Vec::new(),
596 format: FrontendBlockFormat::PlainText,
597 tone: FrontendTone::Warning,
598 }
599 }
600 Self::WebSearchBegin(search) => FrontendBlock {
601 id: Some(format!("{}/{}", search.model_step_id, search.call_id)),
602 group: Some(search.turn_id.clone()),
603 update: FrontendBlockUpdate::Replace,
604 state: FrontendBlockState::Pending,
605 role: FrontendBlockRole::WebSearch,
606 title: "Searching the web".into(),
607 text: String::new(),
608 symbol: Some(FrontendSymbol::Search),
609 files: Vec::new(),
610 format: FrontendBlockFormat::PlainText,
611 tone: FrontendTone::Neutral,
612 },
613 Self::WebSearchEnd(search) => {
614 let (title, text, tone) = match &search.action {
615 WebSearchAction::Search { queries } => (
616 "Searched the web",
617 queries.join("\n"),
618 FrontendTone::Success,
619 ),
620 WebSearchAction::OpenPage { url } => (
621 "Opened a web page",
622 url.clone().unwrap_or_default(),
623 FrontendTone::Success,
624 ),
625 WebSearchAction::FindInPage { url, pattern } => {
626 let text = match (url, pattern) {
627 (Some(url), Some(pattern)) => format!("{pattern}\n{url}"),
628 (Some(url), None) => url.clone(),
629 (None, Some(pattern)) => pattern.clone(),
630 (None, None) => String::new(),
631 };
632 ("Searched a web page", text, FrontendTone::Success)
633 }
634 WebSearchAction::Interrupted => (
635 "Web search interrupted",
636 String::new(),
637 FrontendTone::Warning,
638 ),
639 WebSearchAction::Other => {
640 ("Web search complete", String::new(), FrontendTone::Success)
641 }
642 };
643 FrontendBlock {
644 id: Some(format!("{}/{}", search.model_step_id, search.call_id)),
645 group: Some(search.turn_id.clone()),
646 update: FrontendBlockUpdate::Replace,
647 state: FrontendBlockState::Complete,
648 role: FrontendBlockRole::WebSearch,
649 title: title.into(),
650 text,
651 symbol: Some(FrontendSymbol::Search),
652 files: Vec::new(),
653 format: FrontendBlockFormat::PlainText,
654 tone,
655 }
656 }
657 Self::Frontend(FrontendEvent::Render { capability, block }) => {
658 return Some(RenderedBlock {
659 capability: capability.clone(),
660 block: block.clone(),
661 });
662 }
663 _ => return None,
664 };
665 Some(RenderedBlock {
666 capability: match self {
667 Self::WebSearchBegin(_) | Self::WebSearchEnd(_) => "web_search",
668 _ => "agent",
669 }
670 .into(),
671 block,
672 })
673 }
674}
675
676#[derive(Debug, Clone, PartialEq, Eq)]
690pub enum FrontendSymbol {
691 Agent,
692 Brain,
693 Branch,
694 Chat,
695 ChatGpt,
696 Claude,
697 Deepseek,
698 Delete,
699 Edit,
700 Kimi,
701 Moon,
702 Promote,
703 Route,
704 Search,
705 Sparkle,
706 Storage,
707 Task,
708 Custom(String),
709}
710
711impl FrontendSymbol {
712 pub fn as_str(&self) -> &str {
714 match self {
715 Self::Agent => "agent",
716 Self::Brain => "brain",
717 Self::Branch => "branch",
718 Self::Chat => "chat",
719 Self::ChatGpt => "chat_gpt",
720 Self::Claude => "claude",
721 Self::Deepseek => "deepseek",
722 Self::Delete => "delete",
723 Self::Edit => "edit",
724 Self::Kimi => "kimi",
725 Self::Moon => "moon",
726 Self::Promote => "promote",
727 Self::Route => "route",
728 Self::Search => "search",
729 Self::Sparkle => "sparkle",
730 Self::Storage => "storage",
731 Self::Task => "task",
732 Self::Custom(name) => name,
733 }
734 }
735
736 fn from_wire(name: &str) -> Self {
739 match name {
740 "agent" => Self::Agent,
741 "brain" => Self::Brain,
742 "branch" => Self::Branch,
743 "chat" => Self::Chat,
744 "chat_gpt" => Self::ChatGpt,
745 "claude" => Self::Claude,
746 "deepseek" => Self::Deepseek,
747 "delete" => Self::Delete,
748 "edit" => Self::Edit,
749 "kimi" => Self::Kimi,
750 "moon" => Self::Moon,
751 "promote" => Self::Promote,
752 "route" => Self::Route,
753 "search" => Self::Search,
754 "sparkle" => Self::Sparkle,
755 "storage" => Self::Storage,
756 "task" => Self::Task,
757 other => Self::Custom(other.to_owned()),
758 }
759 }
760}
761
762impl std::fmt::Display for FrontendSymbol {
763 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
764 formatter.write_str(self.as_str())
765 }
766}
767
768impl Serialize for FrontendSymbol {
769 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
770 serializer.serialize_str(self.as_str())
771 }
772}
773
774impl<'de> Deserialize<'de> for FrontendSymbol {
775 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
776 String::deserialize(deserializer).map(|name| Self::from_wire(&name))
779 }
780}
781
782#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
783pub struct ErrorEvent {
784 pub kind: ErrorKind,
785 pub message: String,
786 pub retryable: bool,
787 pub status: Option<u16>,
788 pub retry_after: Option<String>,
789}
790
791impl ErrorEvent {
792 pub(crate) fn from_error(error: &crate::Error) -> Self {
793 let (kind, retryable, status, retry_after) = match error {
794 crate::Error::Config(_) => (ErrorKind::Configuration, false, None, None),
795 crate::Error::Duplicate(_) => (ErrorKind::DuplicateRegistration, false, None, None),
796 crate::Error::Unknown(_) => (ErrorKind::UnknownRegistration, false, None, None),
797 crate::Error::Provider(error) => (
798 ErrorKind::Provider,
799 error.is_retryable(),
800 error.status(),
801 error.retry_after().map(str::to_owned),
802 ),
803 crate::Error::Auth(_) => (ErrorKind::Authentication, false, None, None),
804 crate::Error::Sandbox(_) => (ErrorKind::Sandbox, false, None, None),
805 crate::Error::Tool(_) => (ErrorKind::Tool, false, None, None),
806 crate::Error::Checkpoint(_) => (ErrorKind::Checkpoint, false, None, None),
807 crate::Error::Busy(_) => (ErrorKind::Busy, false, None, None),
808 crate::Error::Stopped(_) => (ErrorKind::Stopped, false, None, None),
809 crate::Error::Rollback { .. } => (ErrorKind::Rollback, false, None, None),
810 crate::Error::Io(_) => (ErrorKind::Io, false, None, None),
811 crate::Error::Http(_) => (ErrorKind::Http, false, None, None),
812 crate::Error::Json(_) => (ErrorKind::Json, false, None, None),
813 crate::Error::Sqlite(_) => (ErrorKind::Storage, false, None, None),
814 };
815 Self {
816 kind,
817 message: error.to_string(),
818 retryable,
819 status,
820 retry_after,
821 }
822 }
823}
824
825#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
827#[serde(rename_all = "snake_case")]
828pub enum ErrorKind {
829 Configuration,
830 DuplicateRegistration,
831 UnknownRegistration,
832 Provider,
833 Authentication,
834 Sandbox,
835 Tool,
836 Checkpoint,
837 Busy,
838 Stopped,
839 Rollback,
840 Io,
841 Http,
842 Json,
843 Storage,
844}
845
846#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
847pub struct WarningEvent {
848 pub message: String,
849}
850
851#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
853pub struct SessionConfiguredEvent {
854 pub session_id: String,
855 pub context: SessionContext,
856 pub model: ModelChangedEvent,
857}
858
859#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
860pub struct TurnStartedEvent {
861 pub turn_id: String,
862 pub model_context_window: Option<i64>,
863}
864
865#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
866pub struct TurnCompleteEvent {
867 pub turn_id: String,
868}
869
870#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
871pub struct TurnAbortedEvent {
872 pub turn_id: String,
873 pub reason: String,
874}
875
876#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
878pub struct MessageTarget {
879 pub checkpoint_sequence: u64,
881 #[serde(deserialize_with = "positive_usize")]
883 pub batch_item_count: usize,
884}
885
886fn positive_usize<'de, D>(deserializer: D) -> std::result::Result<usize, D::Error>
887where
888 D: Deserializer<'de>,
889{
890 let value = usize::deserialize(deserializer)?;
891 if value == 0 {
892 return Err(serde::de::Error::custom(
893 "message target item count must be positive",
894 ));
895 }
896 Ok(value)
897}
898
899#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
900pub struct UserMessageEvent {
901 pub message: String,
902 pub attachments: Vec<SessionFileReference>,
903 #[serde(deserialize_with = "required_option")]
904 pub message_target: Option<MessageTarget>,
905}
906
907#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
908pub struct AgentMessageEvent {
909 pub session_id: String,
910 pub turn_id: String,
911 pub model_step_id: String,
912 pub message: String,
913 pub phase: AgentMessagePhase,
914 #[serde(deserialize_with = "required_option")]
915 pub message_target: Option<MessageTarget>,
916}
917
918#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
919pub struct AgentMessageContentDeltaEvent {
920 pub session_id: String,
921 pub turn_id: String,
922 pub model_step_id: String,
923 pub delta: String,
924 pub phase: AgentMessagePhase,
925}
926
927#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
928pub struct AgentReasoningContentDeltaEvent {
929 pub session_id: String,
930 pub turn_id: String,
931 pub model_step_id: String,
932 pub delta: String,
933}
934
935#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
937pub struct ModelStepStartedEvent {
938 pub session_id: String,
939 pub turn_id: String,
940 pub model_step_id: String,
941 pub step_index: usize,
942 pub started_at_ms: i64,
943}
944
945#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
947pub struct ModelStepCompletedEvent {
948 pub session_id: String,
949 pub turn_id: String,
950 pub model_step_id: String,
951 pub step_index: usize,
952 pub started_at_ms: i64,
953 pub completed_at_ms: i64,
954 pub outcome: ModelStepOutcome,
955}
956
957#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
959#[serde(tag = "status", rename_all = "snake_case")]
960pub enum ModelStepOutcome {
961 Completed {
962 end_turn: bool,
963 tool_call_ids: Vec<String>,
964 usage: TokenUsage,
965 content: Vec<ModelStepContent>,
966 },
967 Failed,
968 Interrupted,
969 Retrying,
971}
972
973#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
975pub struct ModelStepContent {
976 pub output_index: usize,
977 pub part_index: usize,
978 pub phase: ModelStepContentPhase,
979 pub text: String,
980 pub annotations: Vec<ModelStepAnnotation>,
981}
982
983#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
985#[serde(tag = "type", rename_all = "snake_case")]
986pub enum ModelStepAnnotation {
987 UrlCitation {
988 url: String,
989 title: String,
990 start_index: usize,
991 end_index: usize,
992 },
993 FileCitation {
994 file_id: String,
995 filename: String,
996 index: usize,
997 },
998 ContainerFileCitation {
999 container_id: String,
1000 file_id: String,
1001 filename: String,
1002 start_index: usize,
1003 end_index: usize,
1004 },
1005 FilePath {
1006 file_id: String,
1007 index: usize,
1008 },
1009 DocumentCharacterCitation {
1010 cited_text: String,
1011 document_index: usize,
1012 document_title: Option<String>,
1013 file_id: Option<String>,
1014 start_char_index: usize,
1015 end_char_index: usize,
1016 },
1017 DocumentPageCitation {
1018 cited_text: String,
1019 document_index: usize,
1020 document_title: Option<String>,
1021 file_id: Option<String>,
1022 start_page_number: usize,
1023 end_page_number: usize,
1024 },
1025 DocumentContentBlockCitation {
1026 cited_text: String,
1027 document_index: usize,
1028 document_title: Option<String>,
1029 file_id: Option<String>,
1030 start_block_index: usize,
1031 end_block_index: usize,
1032 },
1033 SearchResultCitation {
1034 cited_text: String,
1035 search_result_index: usize,
1036 source: String,
1037 title: Option<String>,
1038 start_block_index: usize,
1039 end_block_index: usize,
1040 },
1041 WebSearchResultCitation {
1042 cited_text: String,
1043 encrypted_index: String,
1044 title: Option<String>,
1045 url: String,
1046 },
1047}
1048
1049#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1051#[serde(rename_all = "snake_case")]
1052pub enum ModelStepContentPhase {
1053 Reasoning,
1054 Commentary,
1055 FinalAnswer,
1056}
1057
1058#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1060pub struct SessionHistoryEvent {
1061 pub events: Vec<EventMsg>,
1062}
1063
1064#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1065pub struct ModelChangedEvent {
1066 pub route: String,
1067 pub model: String,
1068 pub reasoning_effort: Option<String>,
1069 pub model_context_window: Option<i64>,
1070}
1071
1072#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1073pub struct SessionResumeRequestedEvent {
1074 pub session_id: String,
1075 pub context: SessionContext,
1076}
1077
1078#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1080#[serde(rename_all = "snake_case")]
1081pub enum AgentMessagePhase {
1082 Commentary,
1083 FinalAnswer,
1084}
1085
1086#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1087pub struct ToolCallBeginEvent {
1088 pub turn_id: String,
1089 pub call_id: String,
1090 pub name: String,
1091 pub arguments: serde_json::Value,
1092}
1093
1094#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1095pub struct ToolCallEndEvent {
1096 pub turn_id: String,
1097 pub call_id: String,
1098 pub name: String,
1099 pub output: String,
1100 pub is_error: bool,
1101}
1102
1103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1104pub struct ExecApprovalRequestEvent {
1105 pub id: String,
1106 pub turn_id: String,
1107 pub calls: Vec<ApprovalCall>,
1108 pub reason: String,
1109}
1110
1111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1112pub struct ExecApprovalReviewEvent {
1113 pub id: String,
1114 pub turn_id: String,
1115 pub calls: Vec<ApprovalCall>,
1116 pub status: ApprovalReviewStatus,
1117 #[serde(skip_serializing_if = "Option::is_none")]
1118 pub reason: Option<ApprovalReviewEscalation>,
1119}
1120
1121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1122#[serde(rename_all = "snake_case")]
1123pub enum ApprovalReviewStatus {
1124 Reviewing,
1125 Approved,
1126 Escalated,
1127}
1128
1129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1130#[serde(rename_all = "snake_case")]
1131pub enum ApprovalReviewEscalation {
1132 ReviewerAsked,
1133 ReviewDataUnavailable,
1134 ReviewerUnavailable,
1135 InvalidResponse,
1136}
1137
1138#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1139pub struct ApprovalCall {
1140 pub call_id: String,
1141 pub name: String,
1142 pub arguments: serde_json::Value,
1143}
1144
1145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1147#[serde(rename_all = "snake_case")]
1148pub enum ReviewDecision {
1149 Approved,
1150 ApprovedForSession,
1151 Denied { rejection: String },
1152 Abort,
1153}
1154
1155#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1156pub struct TokenUsage {
1157 pub input_tokens: i64,
1158 pub cached_input_tokens: i64,
1159 pub cache_write_input_tokens: i64,
1160 pub output_tokens: i64,
1161 pub reasoning_output_tokens: i64,
1162 pub total_tokens: i64,
1163}
1164
1165impl TokenUsage {
1166 pub fn checked_add(&mut self, other: &Self) -> Option<()> {
1168 let input_tokens = self.input_tokens.checked_add(other.input_tokens)?;
1169 let cached_input_tokens = self
1170 .cached_input_tokens
1171 .checked_add(other.cached_input_tokens)?;
1172 let cache_write_input_tokens = self
1173 .cache_write_input_tokens
1174 .checked_add(other.cache_write_input_tokens)?;
1175 let output_tokens = self.output_tokens.checked_add(other.output_tokens)?;
1176 let reasoning_output_tokens = self
1177 .reasoning_output_tokens
1178 .checked_add(other.reasoning_output_tokens)?;
1179 let total_tokens = self.total_tokens.checked_add(other.total_tokens)?;
1180 *self = Self {
1181 input_tokens,
1182 cached_input_tokens,
1183 cache_write_input_tokens,
1184 output_tokens,
1185 reasoning_output_tokens,
1186 total_tokens,
1187 };
1188 Some(())
1189 }
1190}
1191
1192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1193pub struct TokenUsageInfo {
1194 pub total_token_usage: TokenUsage,
1195 pub last_token_usage: TokenUsage,
1196 pub model_context_window: Option<i64>,
1197}
1198
1199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1200pub struct TokenCountEvent {
1201 pub info: Option<TokenUsageInfo>,
1202 pub rate_limits: Option<serde_json::Value>,
1203}
1204
1205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1206pub struct WebSearchBeginEvent {
1207 pub session_id: String,
1208 pub turn_id: String,
1209 pub model_step_id: String,
1210 pub call_id: String,
1211}
1212
1213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1214pub struct WebSearchEndEvent {
1215 pub session_id: String,
1216 pub turn_id: String,
1217 pub model_step_id: String,
1218 pub call_id: String,
1219 pub action: WebSearchAction,
1220}
1221
1222#[cfg(test)]
1223mod tests {
1224 use serde_json::json;
1225
1226 use super::*;
1227
1228 #[test]
1229 fn model_events_keep_typed_correlation_and_web_search_fields() {
1230 let delta = ModelEvent::CommentaryDelta("Checking".into()).into_event(
1231 "session-1",
1232 "turn-1",
1233 "step-1",
1234 );
1235 let search = ModelEvent::WebSearchCompleted {
1236 call_id: "search-1".into(),
1237 action: WebSearchAction::Search {
1238 queries: vec!["Horus framework".into(), "Horus gateway".into()],
1239 },
1240 }
1241 .into_event("session-1", "turn-1", "step-1");
1242
1243 assert_eq!(
1244 serde_json::to_value(delta).expect("serialize delta"),
1245 json!({
1246 "type": "agent_message_content_delta",
1247 "session_id": "session-1",
1248 "turn_id": "turn-1",
1249 "model_step_id": "step-1",
1250 "delta": "Checking",
1251 "phase": "commentary"
1252 })
1253 );
1254 assert_eq!(
1255 serde_json::to_value(search).expect("serialize web search"),
1256 json!({
1257 "type": "web_search_end",
1258 "session_id": "session-1",
1259 "turn_id": "turn-1",
1260 "model_step_id": "step-1",
1261 "call_id": "search-1",
1262 "action": {
1263 "type": "search",
1264 "queries": ["Horus framework", "Horus gateway"]
1265 }
1266 })
1267 );
1268 }
1269
1270 #[test]
1271 fn interrupted_web_search_renders_a_terminal_warning_block() {
1272 let event = EventMsg::WebSearchEnd(WebSearchEndEvent {
1273 session_id: "session-1".into(),
1274 turn_id: "turn-1".into(),
1275 model_step_id: "step-1".into(),
1276 call_id: "search-1".into(),
1277 action: WebSearchAction::Interrupted,
1278 });
1279
1280 assert_eq!(
1281 serde_json::to_value(&event).expect("serialize interrupted search"),
1282 json!({
1283 "type": "web_search_end",
1284 "session_id": "session-1",
1285 "turn_id": "turn-1",
1286 "model_step_id": "step-1",
1287 "call_id": "search-1",
1288 "action": {"type": "interrupted"}
1289 })
1290 );
1291 let block = event.presentation().expect("interrupted search renders");
1292 assert_eq!(block.capability, "web_search");
1293 assert_eq!(block.block.id.as_deref(), Some("step-1/search-1"));
1294 assert_eq!(block.block.state, FrontendBlockState::Complete);
1295 assert_eq!(block.block.tone, FrontendTone::Warning);
1296 assert_eq!(&*block.block.title, "Web search interrupted");
1297 }
1298
1299 #[test]
1300 fn retrying_model_step_has_a_provider_neutral_reconnect_notice() {
1301 let event = EventMsg::ModelStepCompleted(ModelStepCompletedEvent {
1302 session_id: "session-1".into(),
1303 turn_id: "turn-1".into(),
1304 model_step_id: "step-1".into(),
1305 step_index: 0,
1306 started_at_ms: 1,
1307 completed_at_ms: 2,
1308 outcome: ModelStepOutcome::Retrying,
1309 });
1310
1311 let rendered = event.presentation().expect("retry presentation");
1312
1313 assert_eq!(rendered.capability, "agent");
1314 assert_eq!(rendered.block.id.as_deref(), Some("step-1/retry"));
1315 assert_eq!(rendered.block.title, "Reconnecting…");
1316 assert_eq!(rendered.block.tone, FrontendTone::Warning);
1317 assert_eq!(rendered.block.state, FrontendBlockState::Complete);
1318 }
1319
1320 #[test]
1321 fn middleware_settings_have_a_generic_wire_shape() {
1322 let feature = MiddlewareFeature {
1323 id: "example".into(),
1324 label: "Example".into(),
1325 description: "Example capability".into(),
1326 required: false,
1327 settings: vec![FrontendSetting {
1328 id: "limit".into(),
1329 label: "Limit".into(),
1330 description: "Example limit".into(),
1331 kind: FrontendSettingKind::Integer {
1332 min: 1,
1333 max: None,
1334 step: 10,
1335 },
1336 }],
1337 };
1338
1339 assert_eq!(
1340 serde_json::to_value(feature).expect("serialize middleware setting"),
1341 json!({
1342 "id": "example",
1343 "label": "Example",
1344 "description": "Example capability",
1345 "required": false,
1346 "settings": [{
1347 "id": "limit",
1348 "label": "Limit",
1349 "description": "Example limit",
1350 "type": "integer",
1351 "min": 1,
1352 "step": 10
1353 }]
1354 })
1355 );
1356 }
1357
1358 #[test]
1359 fn session_configured_has_a_stable_wire_shape() {
1360 let event = EventMsg::SessionConfigured(SessionConfiguredEvent {
1361 session_id: "session-1".into(),
1362 context: SessionContext {
1363 tenant_id: Some("tenant-1".into()),
1364 user_id: Some("user-1".into()),
1365 user_name: Some("Ada".into()),
1366 workspace_id: Some("workspace-1".into()),
1367 workspace_label: Some("Project One".into()),
1368 origin_label: Some("cron".into()),
1369 },
1370 model: ModelChangedEvent {
1371 route: "default".into(),
1372 model: "test-model".into(),
1373 reasoning_effort: Some("high".into()),
1374 model_context_window: Some(128_000),
1375 },
1376 });
1377
1378 assert_eq!(
1379 serde_json::to_value(event).expect("serialize session event"),
1380 json!({
1381 "type": "session_configured",
1382 "session_id": "session-1",
1383 "context": {
1384 "tenant_id": "tenant-1",
1385 "user_id": "user-1",
1386 "user_name": "Ada",
1387 "workspace_id": "workspace-1",
1388 "workspace_label": "Project One",
1389 "origin_label": "cron"
1390 },
1391 "model": {
1392 "route": "default",
1393 "model": "test-model",
1394 "reasoning_effort": "high",
1395 "model_context_window": 128_000
1396 }
1397 })
1398 );
1399 }
1400
1401 #[test]
1402 fn session_resume_request_carries_the_target_context() {
1403 let event = EventMsg::SessionResumeRequested(SessionResumeRequestedEvent {
1404 session_id: "session-2".into(),
1405 context: SessionContext {
1406 workspace_label: Some("Project Two".into()),
1407 origin_label: Some("cron".into()),
1408 ..SessionContext::default()
1409 },
1410 });
1411
1412 assert_eq!(
1413 serde_json::to_value(event).expect("serialize resume event"),
1414 json!({
1415 "type": "session_resume_requested",
1416 "session_id": "session-2",
1417 "context": {
1418 "workspace_label": "Project Two",
1419 "origin_label": "cron"
1420 }
1421 })
1422 );
1423 }
1424
1425 #[test]
1426 fn frontend_event_has_a_distinct_nested_discriminator() {
1427 let event = EventMsg::Frontend(FrontendEvent::Widget {
1428 capability: "subagents".into(),
1429 item: FrontendWidget {
1430 id: "status".into(),
1431 slot: FrontendSlot::ComposerHeader,
1432 text: "2 agents".into(),
1433 tone: FrontendTone::Neutral,
1434 symbol: Some(FrontendSymbol::Agent),
1435 icon_only: true,
1436 progress: None,
1437 content: None,
1438 action: None,
1439 },
1440 });
1441 let value = serde_json::to_value(&event).expect("serialize frontend event");
1442
1443 assert_eq!(value["type"], "frontend");
1444 assert_eq!(value["frontend_type"], "widget");
1445 assert_eq!(
1446 serde_json::from_value::<EventMsg>(value).expect("deserialize frontend event"),
1447 event
1448 );
1449 }
1450
1451 #[test]
1452 fn capability_surface_slots_have_stable_wire_names() {
1453 assert_eq!(
1454 serde_json::to_value(FrontendSlot::Navigation).expect("navigation slot"),
1455 json!("navigation")
1456 );
1457 assert_eq!(
1458 serde_json::to_value(FrontendSlot::ChatMenu).expect("chat menu slot"),
1459 json!("chat_menu")
1460 );
1461 assert_eq!(
1462 serde_json::to_value(FrontendSlot::TranscriptTail).expect("transcript tail slot"),
1463 json!("transcript_tail")
1464 );
1465 }
1466
1467 #[test]
1468 fn interrupt_has_a_targeted_wire_shape() {
1469 let submission = Submission {
1470 id: "cancel-1".into(),
1471 op: Op::Interrupt {
1472 turn_id: "turn-1".into(),
1473 },
1474 };
1475
1476 assert_eq!(
1477 serde_json::to_value(submission).expect("serialize interrupt"),
1478 json!({
1479 "id": "cancel-1",
1480 "op": {
1481 "type": "interrupt",
1482 "turn_id": "turn-1"
1483 }
1484 })
1485 );
1486 }
1487
1488 #[test]
1489 fn user_input_has_one_text_payload() {
1490 let submission = Submission {
1491 id: "input-1".into(),
1492 op: Op::UserInput {
1493 text: "hello".into(),
1494 attachments: Vec::new(),
1495 },
1496 };
1497
1498 assert_eq!(
1499 serde_json::to_value(submission).expect("serialize input"),
1500 json!({
1501 "id": "input-1",
1502 "op": {
1503 "type": "user_input",
1504 "text": "hello",
1505 "attachments": []
1506 }
1507 })
1508 );
1509 }
1510
1511 #[test]
1512 fn system_event_omits_submission_correlation() {
1513 let event = Event {
1514 submission_id: None,
1515 msg: EventMsg::Warning(WarningEvent {
1516 message: "system notice".into(),
1517 }),
1518 };
1519
1520 assert_eq!(
1521 serde_json::to_value(event).expect("serialize system event"),
1522 json!({
1523 "msg": {
1524 "type": "warning",
1525 "message": "system notice"
1526 }
1527 })
1528 );
1529 }
1530
1531 #[test]
1532 fn context_compacted_is_a_unit_event() {
1533 assert_eq!(
1534 serde_json::to_value(EventMsg::ContextCompacted).expect("serialize compaction"),
1535 json!({"type": "context_compacted"})
1536 );
1537 }
1538
1539 #[test]
1540 fn approval_review_serializes_the_wire_contract() {
1541 let calls = || {
1542 vec![ApprovalCall {
1543 call_id: "call-1".into(),
1544 name: "bash".into(),
1545 arguments: json!({"command": "git status"}),
1546 }]
1547 };
1548 let reviewing = EventMsg::ExecApprovalReview(ExecApprovalReviewEvent {
1549 id: "approval-1".into(),
1550 turn_id: "turn-1".into(),
1551 calls: calls(),
1552 status: ApprovalReviewStatus::Reviewing,
1553 reason: None,
1554 });
1555 let escalated = EventMsg::ExecApprovalReview(ExecApprovalReviewEvent {
1556 id: "approval-1".into(),
1557 turn_id: "turn-1".into(),
1558 calls: calls(),
1559 status: ApprovalReviewStatus::Escalated,
1560 reason: Some(ApprovalReviewEscalation::ReviewerAsked),
1561 });
1562
1563 assert_eq!(
1565 serde_json::to_value(reviewing).expect("serialize reviewing"),
1566 json!({
1567 "type": "exec_approval_review",
1568 "id": "approval-1",
1569 "turn_id": "turn-1",
1570 "calls": [{"call_id": "call-1", "name": "bash", "arguments": {"command": "git status"}}],
1571 "status": "reviewing"
1572 })
1573 );
1574 assert_eq!(
1575 serde_json::to_value(escalated).expect("serialize escalated"),
1576 json!({
1577 "type": "exec_approval_review",
1578 "id": "approval-1",
1579 "turn_id": "turn-1",
1580 "calls": [{"call_id": "call-1", "name": "bash", "arguments": {"command": "git status"}}],
1581 "status": "escalated",
1582 "reason": "reviewer_asked"
1583 })
1584 );
1585 }
1586
1587 #[test]
1588 fn token_usage_overflow_does_not_partially_update_the_total() {
1589 let mut total = TokenUsage {
1590 input_tokens: 7,
1591 total_tokens: i64::MAX,
1592 ..TokenUsage::default()
1593 };
1594 let original = total.clone();
1595
1596 assert!(
1597 total
1598 .checked_add(&TokenUsage {
1599 input_tokens: 1,
1600 total_tokens: 1,
1601 ..TokenUsage::default()
1602 })
1603 .is_none()
1604 );
1605 assert_eq!(total, original);
1606 }
1607
1608 #[test]
1609 fn symbols_round_trip_and_keep_unknown_names() {
1610 for symbol in [
1611 FrontendSymbol::Agent,
1612 FrontendSymbol::Brain,
1613 FrontendSymbol::Branch,
1614 FrontendSymbol::Chat,
1615 FrontendSymbol::ChatGpt,
1616 FrontendSymbol::Claude,
1617 FrontendSymbol::Deepseek,
1618 FrontendSymbol::Delete,
1619 FrontendSymbol::Edit,
1620 FrontendSymbol::Kimi,
1621 FrontendSymbol::Moon,
1622 FrontendSymbol::Promote,
1623 FrontendSymbol::Route,
1624 FrontendSymbol::Search,
1625 FrontendSymbol::Sparkle,
1626 FrontendSymbol::Storage,
1627 FrontendSymbol::Task,
1628 ] {
1629 let json = serde_json::to_string(&symbol).expect("symbol serializes");
1630 assert_eq!(json, format!("\"{}\"", symbol.as_str()));
1631 let decoded: FrontendSymbol = serde_json::from_str(&json).expect("symbol deserializes");
1632 assert_eq!(decoded, symbol);
1633 }
1634
1635 let custom: FrontendSymbol =
1637 serde_json::from_str("\"telescope\"").expect("unknown symbol deserializes");
1638 assert_eq!(custom, FrontendSymbol::Custom("telescope".into()));
1639 assert_eq!(custom.as_str(), "telescope");
1640
1641 let normalized: FrontendSymbol = serde_json::from_str(
1644 &serde_json::to_string(&FrontendSymbol::Custom("edit".into()))
1645 .expect("custom serializes"),
1646 )
1647 .expect("custom deserializes");
1648 assert_eq!(normalized, FrontendSymbol::Edit);
1649 }
1650}