1use std::path::PathBuf;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Deserializer, Serialize};
5
6use crate::id::Id;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct EventMsg {
10 pub timestamp: DateTime<Utc>,
11 pub id: Id,
12 pub event: Evt,
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub parent: Option<Id>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct OpMsg {
19 pub op: Op,
20 pub id: Id,
21}
22
23pub fn op_msg(op: Op) -> OpMsg {
25 OpMsg { op, id: Id::op() }
26}
27
28pub fn event_msg(event: Evt, parent: Option<Id>) -> EventMsg {
31 EventMsg { timestamp: Utc::now(), id: Id::evt(), event, parent }
32}
33
34#[allow(clippy::large_enum_variant)]
35#[derive(Debug, Clone, Deserialize, Serialize)]
36pub enum Op {
37 StartSession(SessionRequest),
42 UpdateSession(SessionUpdate),
43 Interrupt,
44 UserInput(String),
45 ShellInput(String),
46 Steer(String),
47 ApprovalResponse {
48 turn_id: Id,
49 responses: Vec<ToolDecision>,
50 },
51 QuestionResponse {
57 turn_id: Id,
58 tool_use_id: String,
59 reply: QuestionReply,
60 },
61 SlashCommand {
62 name: String,
63 args: String,
64 },
65 ResumeSession {
71 session_id: Id,
72 #[serde(default)]
73 unattended: bool,
74 },
75 RegisterLocalProvider {
76 port: u16,
77 model: Option<ModelSpec>,
78 },
79 RestoreLocalProvider,
80 Compact {
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 instructions: Option<String>,
86 },
87 ContextReport,
90 Goal(GoalCommand),
95 AmbientPhrase {
100 draft: String,
101 req_id: u64,
102 },
103 AmbientSuggestion {
109 recent_user: String,
110 recent_agent: String,
111 req_id: u64,
112 },
113 Shutdown,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
118pub enum GoalCommand {
119 Set(String),
121 Clear,
123 Status,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
130pub enum AmbientKind {
131 ThinkingPhrase,
133 PromptSuggestion,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub enum Evt {
139 SessionStart(Box<SessionInfo>),
140 SessionUpdated(Box<SessionInfo>),
141 ExtensionRefreshed(Box<ExtensionRefreshed>),
142 SessionEnd {
145 session_id: Id,
146 reason: SessionEndReason,
147 usage: Usage,
148 },
149 UserInput(String),
150 ShellOutput {
151 command: String,
152 stdout: String,
153 stderr: String,
154 exit_code: Option<i32>,
155 },
156 AgentMessage(String),
157 Thinking(String),
158 MessageDelta(String),
159 ThinkingDelta(String),
160 Info(String),
161 InfoBlockStart {
170 id: String,
171 header: String,
172 #[serde(default)]
173 loading: bool,
174 },
175 InfoBlockAppend {
178 id: String,
179 detail: String,
180 },
181 Error(String),
182 ToolStart(ToolUse),
183 ToolUpdate(ToolUpdate),
184 ToolEnd(ToolEnd),
185 TaskEnd {
190 tool_use_id: String,
191 exit_code: Option<i32>,
192 },
193 CompactStart,
194 CompactEnd {
199 #[serde(default)]
200 summary: Option<String>,
201 },
202 TurnStart {
203 turn_id: Id,
204 },
205 TurnPause {
206 turn_id: Id,
207 reason: TurnPauseReason,
208 },
209 TurnResume {
213 turn_id: Id,
214 },
215 TurnEnd {
216 turn_id: Id,
217 status: TurnEndStatus,
218 #[serde(default)]
220 steps: usize,
221 },
222 UsageUpdate {
223 usage: Usage,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
228 context: Option<ContextWindow>,
229 },
230 ContextReport(ContextBreakdown),
233 Ambient {
238 kind: AmbientKind,
239 req_id: u64,
240 text: String,
241 },
242 Goodbye,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub enum TurnPauseReason {
247 Approval {
248 tools: Vec<ToolUse>,
249 message: String,
250 },
251 Question {
260 tool_use_id: String,
261 questions: Vec<QuestionSpec>,
262 },
263}
264
265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
267pub struct QuestionSpec {
268 pub header: String,
270 pub question: String,
272 #[serde(default)]
274 pub multi_select: bool,
275 pub options: Vec<QuestionOption>,
278}
279
280#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
282pub struct QuestionOption {
283 pub label: String,
284 pub description: String,
286 #[serde(default, skip_serializing_if = "Option::is_none")]
289 pub preview: Option<String>,
290}
291
292#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
294pub struct QuestionAnswer {
295 pub selected: Vec<String>,
298 #[serde(default, skip_serializing_if = "Option::is_none")]
301 pub note: Option<String>,
302}
303
304#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
306pub enum QuestionReply {
307 Answered(Vec<QuestionAnswer>),
310 Dismissed,
312 Discuss {
316 #[serde(default, skip_serializing_if = "Option::is_none")]
317 message: Option<String>,
318 },
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
322pub enum SessionEndReason {
323 Replaced,
325 Shutdown,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub enum TurnEndStatus {
331 Completed,
332 Interrupted {
333 #[serde(skip_serializing_if = "Option::is_none")]
334 reason: Option<String>,
335 },
336 Error {
337 #[serde(default, skip_serializing_if = "Option::is_none")]
343 kind: Option<String>,
344 headline: String,
347 details: Vec<String>,
351 },
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct ToolUpdate {
356 pub tool_use_id: String,
357 pub seq: u64,
358 pub message: String,
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
362pub enum ToolEndStatus {
363 Completed,
364 Cancelled,
365 Denied,
366 Failed,
367}
368
369impl ToolEndStatus {
370 pub fn is_error(self) -> bool {
374 !matches!(self, ToolEndStatus::Completed)
375 }
376}
377
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct ToolEnd {
380 pub tool_use_id: String,
381 #[serde(default)]
383 pub tool_name: String,
384 pub status: ToolEndStatus,
385 pub result_json: serde_json::Value,
386}
387
388#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
389pub enum ReviewDecision {
390 Accept,
391 Deny,
392 AcceptForSession,
393 AcceptAlways,
396}
397
398#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
400pub struct ToolDecision {
401 pub tool_use_id: String,
402 pub decision: ReviewDecision,
403 #[serde(default, skip_serializing_if = "Option::is_none")]
405 pub message: Option<String>,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct SkillMetadata {
410 pub name: String,
411 pub description: Option<String>,
412 pub scope: Scope,
413 pub argument_hint: Option<String>,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct SubagentMetadata {
418 pub name: String,
419 pub description: String,
420 pub scope: Scope,
421}
422
423#[derive(Debug, Clone, Default, Serialize, Deserialize)]
432pub struct ProviderSpec {
433 #[serde(alias = "name")]
434 pub id: String,
435 pub display_name: String,
436 pub base_url: String,
437}
438
439#[derive(Debug, Clone, Default, Serialize, Deserialize)]
443pub struct SessionInfo {
444 pub model: ModelSpec,
445 pub provider: ProviderSpec,
446 pub session_id: Id,
447 pub cwd: PathBuf,
448 pub permission_mode: PermissionMode,
449 #[serde(default)]
451 pub skills: Vec<SkillMetadata>,
452 #[serde(default)]
454 pub subagents: Vec<SubagentMetadata>,
455 #[serde(default, skip_serializing_if = "Option::is_none")]
458 pub title: Option<String>,
459}
460
461#[derive(Debug, Clone, Default, Serialize, Deserialize)]
465pub struct SessionUpdate {
466 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub provider: Option<String>,
475 #[serde(default, skip_serializing_if = "Option::is_none")]
480 pub model: Option<ModelSpec>,
481 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub permission_mode: Option<PermissionMode>,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
488 pub title: Option<String>,
489}
490
491#[derive(Debug, Clone, Serialize, Deserialize)]
495pub struct ExtensionRefreshed {
496 pub session_id: Id,
497 pub skills: Vec<SkillMetadata>,
498 pub subagents: Vec<SubagentMetadata>,
499 #[serde(default)]
500 pub mcp_servers: Vec<McpServerInfo>,
501}
502
503#[derive(Debug, Clone, Serialize, Deserialize)]
504pub struct McpServerInfo {
505 pub name: String,
506 pub command: String,
507 pub args: Vec<String>,
508 pub tools: Vec<McpToolInfo>,
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct McpToolInfo {
513 pub name: String,
514 pub qualified_name: String,
515 pub description: String,
516 pub parameters: Vec<McpToolParam>,
517}
518
519#[derive(Debug, Clone, Serialize, Deserialize)]
520pub struct McpToolParam {
521 pub name: String,
522 pub param_type: String,
523 pub required: bool,
524 pub description: String,
525}
526
527#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
534pub struct SessionRequest {
535 #[serde(default, skip_serializing_if = "Option::is_none")]
536 pub model: Option<String>,
537 #[serde(default, skip_serializing_if = "Option::is_none")]
538 pub provider: Option<String>,
539 #[serde(default, skip_serializing_if = "Option::is_none")]
540 pub permission_mode: Option<PermissionMode>,
541 #[serde(default, skip_serializing_if = "Option::is_none")]
542 pub system_prompt: Option<String>,
543 #[serde(default, skip_serializing_if = "Option::is_none")]
544 pub append_system_prompt: Option<String>,
545 #[serde(default, skip_serializing_if = "Option::is_none")]
547 pub tools: Option<Vec<String>>,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
550 pub include_tools: Option<Vec<String>>,
551 #[serde(default, skip_serializing_if = "Option::is_none")]
553 pub exclude_tools: Option<Vec<String>>,
554 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub cwd: Option<PathBuf>,
556 #[serde(default, skip_serializing_if = "Option::is_none")]
557 pub effort: Option<Effort>,
558 #[serde(default, skip_serializing_if = "Option::is_none")]
559 pub enable_auto_memory: Option<bool>,
560 #[serde(default, skip_serializing_if = "Option::is_none")]
561 pub short_prompt: Option<bool>,
562 #[serde(default, skip_serializing_if = "Option::is_none")]
565 pub no_skills: Option<bool>,
566 #[serde(default, skip_serializing_if = "Option::is_none")]
569 pub include_skills: Option<Vec<String>>,
570 #[serde(default, skip_serializing_if = "Option::is_none")]
573 pub exclude_skills: Option<Vec<String>>,
574 #[serde(default, skip_serializing_if = "Option::is_none")]
576 pub save_session: Option<bool>,
577 #[serde(default, skip_serializing_if = "Option::is_none")]
581 pub unattended: Option<bool>,
582 #[serde(default, skip_serializing_if = "Option::is_none")]
584 pub title: Option<String>,
585}
586
587impl SessionRequest {
588 pub fn patched(self, patch: SessionRequest) -> SessionRequest {
595 let SessionRequest {
596 model,
597 provider,
598 permission_mode,
599 system_prompt,
600 append_system_prompt,
601 tools,
602 include_tools,
603 exclude_tools,
604 cwd,
605 effort,
606 enable_auto_memory,
607 short_prompt,
608 no_skills,
609 include_skills,
610 exclude_skills,
611 save_session,
612 unattended,
613 title,
614 } = patch;
615 SessionRequest {
616 model: model.or(self.model),
617 provider: provider.or(self.provider),
618 permission_mode: permission_mode.or(self.permission_mode),
619 system_prompt: system_prompt.or(self.system_prompt),
620 append_system_prompt: append_system_prompt.or(self.append_system_prompt),
621 tools: tools.or(self.tools),
622 include_tools: include_tools.or(self.include_tools),
623 exclude_tools: exclude_tools.or(self.exclude_tools),
624 cwd: cwd.or(self.cwd),
625 effort: effort.or(self.effort),
626 enable_auto_memory: enable_auto_memory.or(self.enable_auto_memory),
627 short_prompt: short_prompt.or(self.short_prompt),
628 no_skills: no_skills.or(self.no_skills),
629 include_skills: include_skills.or(self.include_skills),
630 exclude_skills: exclude_skills.or(self.exclude_skills),
631 save_session: save_session.or(self.save_session),
632 unattended: unattended.or(self.unattended),
633 title: title.or(self.title),
634 }
635 }
636}
637
638#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
639pub struct MalformedToolArgs {
640 pub raw: String,
642 pub error: String,
644}
645
646pub const MISSING_TOOL_NAME: &str = "missing_function_name";
649
650#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
651pub struct ToolUse {
652 pub id: String,
653 pub name: String,
654 pub args: serde_json::Value,
655 #[serde(default, skip_serializing_if = "Option::is_none")]
661 pub malformed_args: Option<MalformedToolArgs>,
662 #[serde(skip_serializing_if = "Option::is_none")]
663 pub signature: Option<String>,
664}
665
666impl ToolUse {
667 pub fn new(id: impl Into<String>, name: impl Into<String>, args: serde_json::Value) -> Self {
669 Self { id: id.into(), name: name.into(), args, malformed_args: None, signature: None }
670 }
671}
672
673#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
674pub struct ModelSpec {
675 #[serde(alias = "name")]
676 pub id: String,
677 #[serde(skip_serializing_if = "Option::is_none")]
678 pub display_name: Option<String>,
679 #[serde(skip_serializing_if = "Option::is_none")]
680 pub description: Option<String>,
681 #[serde(skip_serializing_if = "Option::is_none")]
682 pub temperature: Option<f32>,
683 #[serde(skip_serializing_if = "Option::is_none")]
684 pub top_p: Option<f32>,
685 #[serde(skip_serializing_if = "Option::is_none")]
686 pub top_k: Option<u32>,
687 #[serde(skip_serializing_if = "Option::is_none")]
688 pub max_tokens: Option<u32>,
689 #[serde(skip_serializing_if = "Option::is_none")]
690 pub stop_sequences: Option<Vec<String>>,
691 #[serde(skip_serializing_if = "Option::is_none")]
692 pub context_limit: Option<u32>,
693 #[serde(skip_serializing_if = "Option::is_none")]
694 pub effort: Option<Effort>,
695 #[serde(skip_serializing_if = "Option::is_none")]
699 pub supported_efforts: Option<Vec<Effort>>,
700 #[serde(skip_serializing_if = "Option::is_none")]
701 pub support_vision: Option<bool>,
702 #[serde(skip_serializing_if = "Option::is_none")]
703 pub weight_class: Option<WeightClass>,
704}
705
706#[derive(Debug, Clone, Serialize, Deserialize, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
714#[serde(rename_all = "lowercase")]
715pub enum Effort {
716 Min,
717 Low,
718 Medium,
719 High,
720 XHigh,
721 Max,
722}
723
724impl Effort {
725 pub const ALL: [Effort; 6] =
727 [Effort::Min, Effort::Low, Effort::Medium, Effort::High, Effort::XHigh, Effort::Max];
728
729 pub fn as_str(self) -> &'static str {
731 match self {
732 Effort::Min => "min",
733 Effort::Low => "low",
734 Effort::Medium => "medium",
735 Effort::High => "high",
736 Effort::XHigh => "xhigh",
737 Effort::Max => "max",
738 }
739 }
740}
741
742impl std::fmt::Display for Effort {
743 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
744 f.write_str(self.as_str())
745 }
746}
747
748impl std::str::FromStr for Effort {
749 type Err = String;
750
751 fn from_str(s: &str) -> Result<Self, Self::Err> {
752 Effort::ALL.into_iter().find(|e| e.as_str() == s).ok_or_else(|| {
753 format!("unknown effort `{s}` (expected min, low, medium, high, xhigh, max)")
754 })
755 }
756}
757
758#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
765#[serde(rename_all = "lowercase")]
766pub enum WeightClass {
767 Feather,
769 Middle,
771 Heavy,
773}
774
775impl<'de> Deserialize<'de> for WeightClass {
776 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
777 where
778 D: Deserializer<'de>,
779 {
780 let value = String::deserialize(deserializer)?;
781 match value.to_ascii_lowercase().as_str() {
782 "feather" => Ok(Self::Feather),
783 "middle" => Ok(Self::Middle),
784 "heavy" => Ok(Self::Heavy),
785 _ => Err(serde::de::Error::unknown_variant(&value, &["feather", "middle", "heavy"])),
786 }
787 }
788}
789
790#[derive(Debug, Clone, Deserialize, Serialize, Default, Copy)]
807#[serde(default)]
808pub struct Usage {
809 pub input_tokens: u32,
811 pub output_tokens: u32,
813 #[serde(skip_serializing_if = "Option::is_none")]
815 pub cache_read_tokens: Option<u32>,
816 #[serde(skip_serializing_if = "Option::is_none")]
818 pub cache_creation_tokens: Option<u32>,
819}
820
821#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
825pub struct ContextWindow {
826 pub used_tokens: u32,
829 pub limit_tokens: u32,
831}
832
833#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
841#[serde(default)]
842pub struct ContextBreakdown {
843 pub system_prompt_tokens: u32,
845 pub system_tools_tokens: u32,
847 pub mcp_tools_tokens: u32,
849 pub memory_tokens: u32,
851 pub skills_tokens: u32,
853 pub messages_tokens: u32,
855 pub used_tokens: u32,
857 pub limit_tokens: Option<u32>,
860 pub compact_buffer_tokens: u32,
863}
864
865impl Usage {
866 pub fn new(input_tokens: u32, output_tokens: u32) -> Self {
867 Self { input_tokens, output_tokens, cache_read_tokens: None, cache_creation_tokens: None }
868 }
869
870 pub fn total(&self) -> u32 {
872 self.input_tokens.saturating_add(self.output_tokens)
873 }
874}
875
876impl std::ops::Add<Usage> for Usage {
877 type Output = Usage;
878
879 fn add(self, other: Usage) -> Usage {
880 Usage {
881 input_tokens: self.input_tokens.saturating_add(other.input_tokens),
882 output_tokens: self.output_tokens.saturating_add(other.output_tokens),
883 cache_read_tokens: add_optional_u32(self.cache_read_tokens, other.cache_read_tokens),
884 cache_creation_tokens: add_optional_u32(
885 self.cache_creation_tokens,
886 other.cache_creation_tokens,
887 ),
888 }
889 }
890}
891
892fn add_optional_u32(a: Option<u32>, b: Option<u32>) -> Option<u32> {
893 match (a, b) {
894 (None, None) => None,
895 _ => Some(a.unwrap_or(0).saturating_add(b.unwrap_or(0))),
896 }
897}
898
899impl std::ops::AddAssign<Usage> for Usage {
900 fn add_assign(&mut self, other: Usage) {
901 *self = *self + other;
902 }
903}
904
905#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, Eq, PartialEq)]
906#[serde(rename_all = "snake_case")]
907pub enum PermissionMode {
908 #[default]
910 Strict,
911 Auto,
914 Yolo,
916}
917
918#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
919#[serde(rename_all = "kebab-case")]
920pub enum Scope {
921 Project,
922 User,
923 System,
924}
925
926#[cfg(test)]
927mod tests {
928 use super::{
929 Effort, Evt, ExtensionRefreshed, Id, ModelSpec, Op, PermissionMode, ProviderSpec,
930 ReviewDecision, SessionInfo, SessionRequest, SessionUpdate, ToolDecision, ToolEnd,
931 ToolEndStatus, ToolUse, Usage, WeightClass, event_msg, op_msg,
932 };
933 use std::path::PathBuf;
934
935 #[test]
936 fn op_msg_assigns_runtime_id() {
937 let msg = op_msg(Op::Interrupt);
938
939 assert!(msg.id.to_string().starts_with("op_"));
940 assert!(matches!(msg.op, Op::Interrupt));
941 }
942
943 #[test]
944 fn event_msg_assigns_runtime_metadata() {
945 let event = event_msg(Evt::Info("hello".to_string()), None);
946
947 assert!(event.id.to_string().starts_with("evt_"));
948 assert!(matches!(event.event, Evt::Info(message) if message == "hello"));
949 assert!(event.parent.is_none());
950 }
951
952 fn model_spec(name: &str) -> ModelSpec {
953 ModelSpec {
954 id: name.to_string(),
955 display_name: None,
956 description: None,
957 temperature: None,
958 top_p: None,
959 top_k: None,
960 max_tokens: None,
961 stop_sequences: None,
962 context_limit: None,
963 effort: None,
964 supported_efforts: None,
965 support_vision: None,
966 weight_class: None,
967 }
968 }
969
970 #[test]
971 fn tool_use_without_malformed_args_remains_backward_compatible() {
972 let tool_use: ToolUse = serde_json::from_value(serde_json::json!({
973 "id": "call-1",
974 "name": "Read",
975 "args": { "file_path": "README.md" }
976 }))
977 .unwrap();
978
979 assert!(tool_use.malformed_args.is_none());
980 let encoded = serde_json::to_value(tool_use).unwrap();
981 assert!(encoded.get("malformed_args").is_none());
982 }
983
984 #[test]
985 fn tool_end_without_name_remains_backward_compatible() {
986 let legacy = serde_json::json!({
987 "tool_use_id": "call-1",
988 "status": "Denied",
989 "result_json": "Tool call denied by policy and was not executed."
990 });
991 let mut end: ToolEnd = serde_json::from_value(legacy.clone()).unwrap();
992 assert!(end.tool_name.is_empty());
993 assert_eq!(end.status, ToolEndStatus::Denied);
994
995 end.tool_name = "Bash".to_string();
996 let mut expected = legacy;
997 expected["tool_name"] = serde_json::json!("Bash");
998 assert_eq!(serde_json::to_value(end).unwrap(), expected);
999 }
1000
1001 #[test]
1002 fn task_end_round_trips_with_observed_or_unobserved_exit() {
1003 for exit_code in [Some(2), None] {
1004 let json = serde_json::json!({
1005 "TaskEnd": { "tool_use_id": "bash-1", "exit_code": exit_code }
1006 });
1007 let event: super::Evt = serde_json::from_value(json.clone()).unwrap();
1008 assert!(matches!(&event, super::Evt::TaskEnd { tool_use_id, exit_code: code }
1009 if tool_use_id == "bash-1" && *code == exit_code));
1010 assert_eq!(serde_json::to_value(event).unwrap(), json);
1011 }
1012 }
1013
1014 #[test]
1015 fn turn_end_error_kind_is_optional_on_the_wire() {
1016 let old: super::TurnEndStatus = serde_json::from_value(serde_json::json!({
1018 "Error": { "headline": "authentication error", "details": [] }
1019 }))
1020 .unwrap();
1021 let super::TurnEndStatus::Error { kind, .. } = &old else {
1022 panic!("expected Error variant");
1023 };
1024 assert!(kind.is_none());
1025
1026 let json = serde_json::to_value(&old).unwrap();
1028 assert!(json["Error"].get("kind").is_none());
1029
1030 let with = super::TurnEndStatus::Error {
1032 kind: Some("oauth".to_string()),
1033 headline: "OAuth sign-in required".to_string(),
1034 details: vec![],
1035 };
1036 let json = serde_json::to_value(&with).unwrap();
1037 assert_eq!(json["Error"]["kind"], "oauth");
1038 }
1039
1040 #[test]
1041 fn turn_end_steps_default_for_older_events() {
1042 let event = super::Evt::TurnEnd {
1043 turn_id: super::Id::new("turn"),
1044 status: super::TurnEndStatus::Completed,
1045 steps: 3,
1046 };
1047 let mut json = serde_json::to_value(&event).unwrap();
1048 json["TurnEnd"].as_object_mut().unwrap().remove("steps");
1049 let old: super::Evt = serde_json::from_value(json).unwrap();
1050
1051 assert!(matches!(old, super::Evt::TurnEnd { steps: 0, .. }));
1052
1053 let json = serde_json::to_value(event).unwrap();
1054 assert_eq!(json["TurnEnd"]["steps"], 3);
1055 }
1056
1057 #[test]
1058 fn effort_serializes_as_lowercase_tokens() {
1059 let mut spec = model_spec("m");
1060 spec.effort = Some(super::Effort::XHigh);
1061 let json = serde_json::to_value(&spec).unwrap();
1062 assert_eq!(json["effort"], "xhigh");
1063
1064 let json_none = serde_json::to_value(model_spec("m")).unwrap();
1066 assert!(json_none.get("effort").is_none());
1067
1068 for level in super::Effort::ALL {
1070 let parsed: ModelSpec =
1071 serde_json::from_value(serde_json::json!({"id": "m", "effort": level.as_str()}))
1072 .unwrap();
1073 assert_eq!(parsed.effort, Some(level), "round-trip {level}");
1074 }
1075 }
1076
1077 #[test]
1078 fn effort_orders_min_lowest_to_max_highest() {
1079 let mut sorted = super::Effort::ALL;
1080 sorted.sort();
1081 assert_eq!(sorted, super::Effort::ALL);
1082 assert!(super::Effort::Min < super::Effort::Low);
1083 assert!(super::Effort::XHigh < super::Effort::Max);
1084 }
1085
1086 #[test]
1087 fn session_overrides_effort_round_trips() {
1088 let parsed: super::SessionRequest =
1089 serde_json::from_value(serde_json::json!({"effort": "max"})).unwrap();
1090 assert_eq!(parsed.effort, Some(super::Effort::Max));
1091
1092 let parsed: super::SessionRequest = serde_json::from_value(serde_json::json!({})).unwrap();
1093 assert_eq!(parsed.effort, None);
1094 }
1095
1096 #[test]
1097 fn short_prompt_round_trips_and_defaults_to_unset() {
1098 let parsed: super::SessionRequest =
1099 serde_json::from_value(serde_json::json!({"short_prompt": true})).unwrap();
1100 assert_eq!(parsed.short_prompt, Some(true));
1101
1102 let parsed: super::SessionRequest = serde_json::from_value(serde_json::json!({})).unwrap();
1103 assert_eq!(parsed.short_prompt, None);
1104 }
1105
1106 #[test]
1107 fn skill_filters_are_optional_on_the_wire_and_preserve_empty_overrides() {
1108 let absent: SessionRequest = serde_json::from_value(serde_json::json!({})).unwrap();
1109 assert_eq!(absent.include_skills, None);
1110 assert_eq!(absent.exclude_skills, None);
1111 let encoded = serde_json::to_value(absent).unwrap();
1112 assert!(encoded.get("include_skills").is_none());
1113 assert!(encoded.get("exclude_skills").is_none());
1114
1115 let json = serde_json::json!({"include_skills": ["Review"], "exclude_skills": []});
1116 let parsed: SessionRequest = serde_json::from_value(json.clone()).unwrap();
1117 assert_eq!(parsed.include_skills, Some(vec!["Review".to_string()]));
1118 assert_eq!(parsed.exclude_skills, Some(Vec::new()));
1119 assert_eq!(serde_json::to_value(parsed).unwrap(), json);
1120 }
1121
1122 fn pinned_request() -> SessionRequest {
1123 SessionRequest {
1124 model: Some("base-model".to_string()),
1125 provider: Some("anthropic".to_string()),
1126 permission_mode: Some(PermissionMode::Strict),
1127 system_prompt: Some("base prompt".to_string()),
1128 cwd: Some(std::path::PathBuf::from("/base")),
1129 effort: Some(Effort::Medium),
1130 enable_auto_memory: Some(true),
1131 short_prompt: Some(true),
1132 no_skills: Some(true),
1133 include_skills: Some(vec!["review".to_string()]),
1134 exclude_skills: Some(vec!["noisy".to_string()]),
1135 save_session: Some(true),
1136 unattended: Some(true),
1137 title: Some("pinned title".to_string()),
1138 ..Default::default()
1139 }
1140 }
1141
1142 #[test]
1143 fn patched_with_an_empty_patch_keeps_every_field() {
1144 assert_eq!(pinned_request().patched(SessionRequest::default()), pinned_request());
1145 }
1146
1147 #[test]
1148 fn patched_overwrites_only_the_patch_set_fields() {
1149 let patched = pinned_request().patched(SessionRequest {
1150 model: Some("new-model".to_string()),
1151 permission_mode: Some(PermissionMode::Yolo),
1152 enable_auto_memory: Some(false),
1153 short_prompt: Some(false),
1154 include_skills: Some(Vec::new()),
1155 ..Default::default()
1156 });
1157 assert_eq!(patched.model.as_deref(), Some("new-model"));
1159 assert_eq!(patched.permission_mode, Some(PermissionMode::Yolo));
1160 assert_eq!(patched.enable_auto_memory, Some(false));
1161 assert_eq!(patched.short_prompt, Some(false));
1162 assert_eq!(patched.include_skills, Some(Vec::new()));
1163 assert_eq!(patched.provider.as_deref(), Some("anthropic"));
1165 assert_eq!(patched.system_prompt.as_deref(), Some("base prompt"));
1166 assert_eq!(patched.effort, Some(Effort::Medium));
1167 assert_eq!(patched.save_session, Some(true));
1168 assert_eq!(patched.unattended, Some(true));
1169 assert_eq!(patched.title.as_deref(), Some("pinned title"));
1170 assert_eq!(patched.exclude_skills, Some(vec!["noisy".to_string()]));
1171 }
1172
1173 #[test]
1174 fn unattended_is_optional_on_the_wire() {
1175 let request: SessionRequest = serde_json::from_value(serde_json::json!({})).unwrap();
1178 assert_eq!(request.unattended, None);
1179
1180 let op = Op::ResumeSession { session_id: Id::new("ses"), unattended: true };
1181 let mut json = serde_json::to_value(&op).unwrap();
1182 json["ResumeSession"].as_object_mut().unwrap().remove("unattended");
1183 let decoded: Op = serde_json::from_value(json).unwrap();
1184 assert!(matches!(decoded, Op::ResumeSession { unattended: false, .. }));
1185 }
1186
1187 #[test]
1188 fn session_initialized_title_is_optional_on_the_wire() {
1189 let mut payload = SessionInfo {
1190 model: model_spec("m"),
1191 provider: provider_spec("p"),
1192 session_id: Id::new("ses"),
1193 cwd: PathBuf::from("/tmp"),
1194 permission_mode: PermissionMode::default(),
1195 skills: vec![],
1196 subagents: vec![],
1197 title: None,
1198 };
1199
1200 let json = serde_json::to_value(&payload).unwrap();
1201 assert!(json.get("title").is_none(), "an unset title is omitted: {json}");
1202 let decoded: SessionInfo = serde_json::from_value(json).unwrap();
1203 assert_eq!(decoded.title, None);
1204
1205 payload.title = Some("fix".to_string());
1206 let json = serde_json::to_value(&payload).unwrap();
1207 assert_eq!(json["title"], "fix");
1208 }
1209
1210 #[test]
1211 fn weight_class_serializes_lowercase_and_is_omitted_when_none() {
1212 let mut spec = model_spec("m");
1213 spec.weight_class = Some(WeightClass::Heavy);
1214 let json = serde_json::to_value(&spec).unwrap();
1215 assert_eq!(json["weight_class"], "heavy");
1216
1217 let json_none = serde_json::to_value(model_spec("m")).unwrap();
1219 assert!(json_none.get("weight_class").is_none());
1220
1221 let parsed: ModelSpec =
1223 serde_json::from_value(serde_json::json!({"id": "m", "weight_class": "feather"}))
1224 .unwrap();
1225 assert_eq!(parsed.weight_class, Some(WeightClass::Feather));
1226 }
1227
1228 #[test]
1229 fn weight_class_deserializes_case_insensitively() {
1230 for (value, expected) in [
1231 ("Feather", WeightClass::Feather),
1232 ("MIDDLE", WeightClass::Middle),
1233 ("hEaVy", WeightClass::Heavy),
1234 ] {
1235 let parsed: ModelSpec =
1236 serde_json::from_value(serde_json::json!({"id": "m", "weight_class": value}))
1237 .unwrap();
1238 assert_eq!(parsed.weight_class, Some(expected));
1239 }
1240 }
1241
1242 #[test]
1243 fn weight_class_orders_feather_lightest_to_heavy_heaviest() {
1244 assert!(WeightClass::Feather < WeightClass::Middle);
1245 assert!(WeightClass::Middle < WeightClass::Heavy);
1246 }
1247
1248 fn provider_spec(name: &str) -> ProviderSpec {
1249 ProviderSpec {
1250 id: name.to_string(),
1251 display_name: name.to_string(),
1252 base_url: format!("https://api.{name}.test/v1"),
1253 }
1254 }
1255
1256 #[test]
1257 fn compact_events_serde_roundtrip() {
1258 let compact_start =
1259 serde_json::to_string(&Evt::CompactStart).expect("serialize CompactStart");
1260 let compact_end =
1261 serde_json::to_string(&Evt::CompactEnd { summary: Some("the summary".to_string()) })
1262 .expect("serialize CompactEnd");
1263
1264 assert_eq!(compact_start, "\"CompactStart\"");
1265 assert_eq!(compact_end, r#"{"CompactEnd":{"summary":"the summary"}}"#);
1266
1267 assert!(matches!(
1268 serde_json::from_str::<Evt>(&compact_start).expect("deserialize CompactStart"),
1269 Evt::CompactStart
1270 ));
1271 assert!(matches!(
1272 serde_json::from_str::<Evt>(&compact_end).expect("deserialize CompactEnd"),
1273 Evt::CompactEnd { summary: Some(s) } if s == "the summary"
1274 ));
1275 assert!(matches!(
1276 serde_json::from_str::<Evt>(r#"{"CompactEnd":{}}"#)
1277 .expect("deserialize CompactEnd without summary"),
1278 Evt::CompactEnd { summary: None }
1279 ));
1280 }
1281
1282 #[test]
1283 fn compact_op_serde_roundtrip() {
1284 let plain = serde_json::to_string(&Op::Compact { instructions: None })
1285 .expect("serialize bare Compact");
1286 assert_eq!(plain, r#"{"Compact":{}}"#);
1287 assert!(matches!(
1288 serde_json::from_str::<Op>(&plain).expect("deserialize bare Compact"),
1289 Op::Compact { instructions: None }
1290 ));
1291
1292 let steered =
1293 serde_json::to_string(&Op::Compact { instructions: Some("keep dates".to_string()) })
1294 .expect("serialize steered Compact");
1295 assert_eq!(steered, r#"{"Compact":{"instructions":"keep dates"}}"#);
1296 assert!(matches!(
1297 serde_json::from_str::<Op>(&steered).expect("deserialize steered Compact"),
1298 Op::Compact { instructions: Some(text) } if text == "keep dates"
1299 ));
1300 }
1301
1302 #[test]
1303 fn session_end_and_turn_resume_serde_roundtrip() {
1304 let session_id = Id::new("ses");
1305 let end = Evt::SessionEnd {
1306 session_id,
1307 reason: super::SessionEndReason::Shutdown,
1308 usage: Usage::new(10, 5),
1309 };
1310 let json = serde_json::to_string(&end).expect("serialize SessionEnd");
1311 let decoded = serde_json::from_str::<Evt>(&json).expect("deserialize SessionEnd");
1312 assert!(matches!(
1313 decoded,
1314 Evt::SessionEnd { session_id: id, reason: super::SessionEndReason::Shutdown, usage }
1315 if id == session_id && usage.total() == 15
1316 ));
1317
1318 let turn_id = Id::new("op");
1319 let resume = Evt::TurnResume { turn_id };
1320 let json = serde_json::to_string(&resume).expect("serialize TurnResume");
1321 let decoded = serde_json::from_str::<Evt>(&json).expect("deserialize TurnResume");
1322 assert!(matches!(decoded, Evt::TurnResume { turn_id: id } if id == turn_id));
1323 }
1324
1325 #[test]
1326 fn extension_refreshed_serde_roundtrip() {
1327 let event = Evt::ExtensionRefreshed(Box::new(ExtensionRefreshed {
1328 session_id: Id::new("ses"),
1329 skills: Vec::new(),
1330 subagents: Vec::new(),
1331 mcp_servers: Vec::new(),
1332 }));
1333
1334 let json = serde_json::to_string(&event).expect("serialize ExtensionRefreshed");
1335 let decoded = serde_json::from_str::<Evt>(&json).expect("deserialize ExtensionRefreshed");
1336
1337 assert!(matches!(
1338 decoded,
1339 Evt::ExtensionRefreshed(payload)
1340 if payload.skills.is_empty() && payload.subagents.is_empty()
1341 ));
1342 }
1343
1344 #[test]
1345 fn session_update_op_serde_roundtrip() {
1346 let op = Op::UpdateSession(SessionUpdate {
1347 provider: Some("openai".to_string()),
1348 model: Some(ModelSpec {
1349 temperature: Some(0.2),
1350 effort: Some(super::Effort::High),
1351 ..model_spec("gpt-5.4")
1352 }),
1353 permission_mode: Some(PermissionMode::Yolo),
1354 title: Some("renamed".to_string()),
1355 });
1356
1357 let json = serde_json::to_string(&op).expect("serialize UpdateSession");
1358 let decoded = serde_json::from_str::<Op>(&json).expect("deserialize UpdateSession");
1359
1360 assert!(matches!(
1361 decoded,
1362 Op::UpdateSession(SessionUpdate {
1363 provider: Some(provider),
1364 model: Some(model),
1365 permission_mode: Some(PermissionMode::Yolo),
1366 title: Some(title),
1367 })
1368 if provider == "openai"
1369 && model.id == "gpt-5.4"
1370 && model.temperature == Some(0.2)
1371 && model.effort == Some(super::Effort::High)
1372 && title == "renamed"
1373 ));
1374 }
1375
1376 #[test]
1377 fn approval_response_uses_named_tool_decisions() {
1378 let turn_id = Id::new("turn");
1379 let op = Op::ApprovalResponse {
1380 turn_id,
1381 responses: vec![ToolDecision {
1382 tool_use_id: "call-1".to_string(),
1383 decision: ReviewDecision::Deny,
1384 message: Some("use the read-only endpoint".to_string()),
1385 }],
1386 };
1387
1388 let json = serde_json::to_value(&op).expect("serialize ApprovalResponse");
1389 assert_eq!(
1390 json["ApprovalResponse"]["responses"],
1391 serde_json::json!([{
1392 "tool_use_id": "call-1",
1393 "decision": "Deny",
1394 "message": "use the read-only endpoint"
1395 }])
1396 );
1397
1398 let decoded = serde_json::from_value::<Op>(json).expect("deserialize ApprovalResponse");
1399 assert!(matches!(
1400 decoded,
1401 Op::ApprovalResponse { turn_id: id, responses }
1402 if id == turn_id
1403 && responses == vec![ToolDecision {
1404 tool_use_id: "call-1".to_string(),
1405 decision: ReviewDecision::Deny,
1406 message: Some("use the read-only endpoint".to_string()),
1407 }]
1408 ));
1409 }
1410
1411 #[test]
1412 fn session_updated_event_serde_roundtrip() {
1413 let session_id = Id::new("ses");
1414 let event = Evt::SessionUpdated(Box::new(SessionInfo {
1415 model: model_spec("claude-sonnet-4-6"),
1416 provider: provider_spec("anthropic"),
1417 session_id,
1418 cwd: PathBuf::from("/tmp/session-updated"),
1419 permission_mode: PermissionMode::default(),
1420 skills: vec![],
1421 subagents: vec![],
1422 title: None,
1423 }));
1424
1425 let json = serde_json::to_string(&event).expect("serialize SessionUpdated");
1426 let decoded = serde_json::from_str::<Evt>(&json).expect("deserialize SessionUpdated");
1427
1428 assert!(matches!(
1429 decoded,
1430 Evt::SessionUpdated(payload)
1431 if payload.model.id == "claude-sonnet-4-6"
1432 && payload.provider.id == "anthropic"
1433 && payload.provider.base_url == "https://api.anthropic.test/v1"
1434 && payload.session_id == session_id
1435 && payload.cwd == std::path::Path::new("/tmp/session-updated")
1436 ));
1437 }
1438
1439 #[test]
1440 fn provider_spec_ignores_the_dropped_model_list() {
1441 let spec: ProviderSpec = serde_json::from_value(serde_json::json!({
1444 "id": "anthropic",
1445 "display_name": "Anthropic",
1446 "base_url": "https://api.anthropic.test/v1",
1447 "preferred_models": [{ "id": "claude-sonnet-4-6" }],
1448 }))
1449 .unwrap();
1450
1451 assert_eq!(spec.id, "anthropic");
1452 assert_eq!(spec.display_name, "Anthropic");
1453 assert_eq!(spec.base_url, "https://api.anthropic.test/v1");
1454
1455 let encoded = serde_json::to_value(&spec).unwrap();
1457 assert!(encoded.get("preferred_models").is_none());
1458 }
1459
1460 #[test]
1461 fn context_report_serde_roundtrip() {
1462 let breakdown = super::ContextBreakdown {
1463 system_prompt_tokens: 1200,
1464 system_tools_tokens: 3400,
1465 mcp_tools_tokens: 0,
1466 memory_tokens: 800,
1467 skills_tokens: 150,
1468 messages_tokens: 42_000,
1469 used_tokens: 47_550,
1470 limit_tokens: Some(200_000),
1471 compact_buffer_tokens: 20_000,
1472 };
1473 let json = serde_json::to_value(Evt::ContextReport(breakdown)).expect("serialize");
1474 assert_eq!(
1475 json,
1476 serde_json::json!({
1477 "ContextReport": {
1478 "system_prompt_tokens": 1200,
1479 "system_tools_tokens": 3400,
1480 "mcp_tools_tokens": 0,
1481 "memory_tokens": 800,
1482 "skills_tokens": 150,
1483 "messages_tokens": 42000,
1484 "used_tokens": 47550,
1485 "limit_tokens": 200000,
1486 "compact_buffer_tokens": 20000
1487 }
1488 })
1489 );
1490 let decoded = serde_json::from_value::<Evt>(json).expect("deserialize");
1491 assert!(matches!(decoded, Evt::ContextReport(b) if b == breakdown));
1492
1493 let sparse: super::ContextBreakdown =
1495 serde_json::from_value(serde_json::json!({"used_tokens": 10})).unwrap();
1496 assert_eq!(sparse.used_tokens, 10);
1497 assert_eq!(sparse.limit_tokens, None);
1498
1499 let op = serde_json::to_value(Op::ContextReport).expect("serialize op");
1500 assert_eq!(op, serde_json::json!("ContextReport"));
1501 assert!(matches!(serde_json::from_value::<Op>(op).unwrap(), Op::ContextReport));
1502 }
1503
1504 #[test]
1505 fn question_pause_serde_roundtrip() {
1506 let turn_id = Id::new("op");
1507 let pause = Evt::TurnPause {
1508 turn_id,
1509 reason: super::TurnPauseReason::Question {
1510 tool_use_id: "toolu_1".to_string(),
1511 questions: vec![super::QuestionSpec {
1512 header: "Auth method".to_string(),
1513 question: "Which auth method should we use?".to_string(),
1514 multi_select: false,
1515 options: vec![
1516 super::QuestionOption {
1517 label: "JWT (Recommended)".to_string(),
1518 description: "Stateless tokens".to_string(),
1519 preview: None,
1520 },
1521 super::QuestionOption {
1522 label: "Sessions".to_string(),
1523 description: "Server-side sessions".to_string(),
1524 preview: Some("fn login() {}".to_string()),
1525 },
1526 ],
1527 }],
1528 },
1529 };
1530
1531 let json = serde_json::to_value(&pause).expect("serialize question pause");
1532 let spec = &json["TurnPause"]["reason"]["Question"]["questions"][0];
1534 assert!(spec["options"][0].get("preview").is_none());
1535 assert_eq!(spec["options"][1]["preview"], "fn login() {}");
1536
1537 let decoded: Evt = serde_json::from_value(json).expect("deserialize question pause");
1538 let Evt::TurnPause {
1539 reason: super::TurnPauseReason::Question { tool_use_id, questions },
1540 ..
1541 } = decoded
1542 else {
1543 panic!("expected Question pause");
1544 };
1545 assert_eq!(tool_use_id, "toolu_1");
1546 assert_eq!(questions.len(), 1);
1547 assert!(!questions[0].multi_select);
1548
1549 let sparse: super::QuestionSpec = serde_json::from_value(serde_json::json!({
1551 "header": "Scope",
1552 "question": "How broad?",
1553 "options": [],
1554 }))
1555 .unwrap();
1556 assert!(!sparse.multi_select);
1557 }
1558
1559 #[test]
1560 fn question_response_serde_roundtrip() {
1561 let turn_id = Id::new("op");
1562 let op = Op::QuestionResponse {
1563 turn_id,
1564 tool_use_id: "toolu_1".to_string(),
1565 reply: super::QuestionReply::Answered(vec![super::QuestionAnswer {
1566 selected: vec!["JWT".to_string(), "Sessions".to_string()],
1567 note: None,
1568 }]),
1569 };
1570 let json = serde_json::to_string(&op).expect("serialize QuestionResponse");
1571 let decoded: Op = serde_json::from_str(&json).expect("deserialize QuestionResponse");
1572 assert!(matches!(
1573 decoded,
1574 Op::QuestionResponse { tool_use_id, reply: super::QuestionReply::Answered(answers), .. }
1575 if tool_use_id == "toolu_1" && answers[0].selected.len() == 2
1576 ));
1577
1578 for (reply, expected) in [
1581 (super::QuestionReply::Dismissed, serde_json::json!("Dismissed")),
1582 (super::QuestionReply::Discuss { message: None }, serde_json::json!({ "Discuss": {} })),
1583 (
1584 super::QuestionReply::Discuss { message: Some("later".to_string()) },
1585 serde_json::json!({ "Discuss": { "message": "later" } }),
1586 ),
1587 ] {
1588 let json = serde_json::to_value(&reply).expect("serialize reply");
1589 assert_eq!(json, expected);
1590 let decoded: super::QuestionReply =
1591 serde_json::from_value(json).expect("deserialize reply");
1592 assert_eq!(decoded, reply);
1593 }
1594 }
1595
1596 #[test]
1597 fn usage_adds_cache_fields_without_overflowing() {
1598 let mut usage = Usage {
1599 input_tokens: 10,
1600 output_tokens: 20,
1601 cache_read_tokens: Some(3),
1602 cache_creation_tokens: None,
1603 };
1604 usage += Usage {
1605 input_tokens: 5,
1606 output_tokens: 6,
1607 cache_read_tokens: Some(4),
1608 cache_creation_tokens: Some(8),
1609 };
1610
1611 assert_eq!(usage.input_tokens, 15);
1612 assert_eq!(usage.output_tokens, 26);
1613 assert_eq!(usage.total(), 41);
1614 assert_eq!(usage.cache_read_tokens, Some(7));
1615 assert_eq!(usage.cache_creation_tokens, Some(8));
1616 }
1617}