1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15use crate::handler::SessionHandler;
16use crate::hooks::SessionHooks;
17pub use crate::session_fs::{
18 DirEntry, DirEntryKind, FileInfo, FsError, SessionFsConfig, SessionFsConventions,
19 SessionFsProvider,
20};
21pub use crate::trace_context::{TraceContext, TraceContextProvider};
22use crate::transforms::SystemMessageTransform;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32#[serde(rename_all = "lowercase")]
33#[non_exhaustive]
34pub enum ConnectionState {
35 Disconnected,
37 Connecting,
39 Connected,
41 Error,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
50#[non_exhaustive]
51pub enum SessionLifecycleEventType {
52 #[serde(rename = "session.created")]
54 Created,
55 #[serde(rename = "session.deleted")]
57 Deleted,
58 #[serde(rename = "session.updated")]
60 Updated,
61 #[serde(rename = "session.foreground")]
63 Foreground,
64 #[serde(rename = "session.background")]
66 Background,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct SessionLifecycleEventMetadata {
72 #[serde(rename = "startTime")]
74 pub start_time: String,
75 #[serde(rename = "modifiedTime")]
77 pub modified_time: String,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub summary: Option<String>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct SessionLifecycleEvent {
87 #[serde(rename = "type")]
89 pub event_type: SessionLifecycleEventType,
90 #[serde(rename = "sessionId")]
92 pub session_id: SessionId,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub metadata: Option<SessionLifecycleEventMetadata>,
96}
97
98#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
104#[serde(transparent)]
105pub struct SessionId(String);
106
107impl SessionId {
108 pub fn new(id: impl Into<String>) -> Self {
110 Self(id.into())
111 }
112
113 pub fn as_str(&self) -> &str {
115 &self.0
116 }
117
118 pub fn into_inner(self) -> String {
120 self.0
121 }
122}
123
124impl std::ops::Deref for SessionId {
125 type Target = str;
126
127 fn deref(&self) -> &str {
128 &self.0
129 }
130}
131
132impl std::fmt::Display for SessionId {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 f.write_str(&self.0)
135 }
136}
137
138impl From<String> for SessionId {
139 fn from(s: String) -> Self {
140 Self(s)
141 }
142}
143
144impl From<&str> for SessionId {
145 fn from(s: &str) -> Self {
146 Self(s.to_owned())
147 }
148}
149
150impl AsRef<str> for SessionId {
151 fn as_ref(&self) -> &str {
152 &self.0
153 }
154}
155
156impl std::borrow::Borrow<str> for SessionId {
157 fn borrow(&self) -> &str {
158 &self.0
159 }
160}
161
162impl From<SessionId> for String {
163 fn from(id: SessionId) -> String {
164 id.0
165 }
166}
167
168impl PartialEq<str> for SessionId {
169 fn eq(&self, other: &str) -> bool {
170 self.0 == other
171 }
172}
173
174impl PartialEq<String> for SessionId {
175 fn eq(&self, other: &String) -> bool {
176 &self.0 == other
177 }
178}
179
180impl PartialEq<SessionId> for String {
181 fn eq(&self, other: &SessionId) -> bool {
182 self == &other.0
183 }
184}
185
186impl PartialEq<&str> for SessionId {
187 fn eq(&self, other: &&str) -> bool {
188 self.0 == *other
189 }
190}
191
192impl PartialEq<&SessionId> for SessionId {
193 fn eq(&self, other: &&SessionId) -> bool {
194 self.0 == other.0
195 }
196}
197
198impl PartialEq<SessionId> for &SessionId {
199 fn eq(&self, other: &SessionId) -> bool {
200 self.0 == other.0
201 }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
210#[serde(transparent)]
211pub struct RequestId(String);
212
213impl RequestId {
214 pub fn new(id: impl Into<String>) -> Self {
216 Self(id.into())
217 }
218
219 pub fn into_inner(self) -> String {
221 self.0
222 }
223}
224
225impl std::ops::Deref for RequestId {
226 type Target = str;
227
228 fn deref(&self) -> &str {
229 &self.0
230 }
231}
232
233impl std::fmt::Display for RequestId {
234 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235 f.write_str(&self.0)
236 }
237}
238
239impl From<String> for RequestId {
240 fn from(s: String) -> Self {
241 Self(s)
242 }
243}
244
245impl From<&str> for RequestId {
246 fn from(s: &str) -> Self {
247 Self(s.to_owned())
248 }
249}
250
251impl AsRef<str> for RequestId {
252 fn as_ref(&self) -> &str {
253 &self.0
254 }
255}
256
257impl std::borrow::Borrow<str> for RequestId {
258 fn borrow(&self) -> &str {
259 &self.0
260 }
261}
262
263impl From<RequestId> for String {
264 fn from(id: RequestId) -> String {
265 id.0
266 }
267}
268
269impl PartialEq<str> for RequestId {
270 fn eq(&self, other: &str) -> bool {
271 self.0 == other
272 }
273}
274
275impl PartialEq<String> for RequestId {
276 fn eq(&self, other: &String) -> bool {
277 &self.0 == other
278 }
279}
280
281impl PartialEq<RequestId> for String {
282 fn eq(&self, other: &RequestId) -> bool {
283 self == &other.0
284 }
285}
286
287impl PartialEq<&str> for RequestId {
288 fn eq(&self, other: &&str) -> bool {
289 self.0 == *other
290 }
291}
292
293#[derive(Debug, Clone, Default, Serialize, Deserialize)]
301#[serde(rename_all = "camelCase")]
302#[non_exhaustive]
303pub struct Tool {
304 pub name: String,
306 #[serde(default, skip_serializing_if = "Option::is_none")]
309 pub namespaced_name: Option<String>,
310 #[serde(default)]
312 pub description: String,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub instructions: Option<String>,
316 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
318 pub parameters: HashMap<String, Value>,
319 #[serde(default, skip_serializing_if = "is_false")]
323 pub overrides_built_in_tool: bool,
324 #[serde(default, skip_serializing_if = "is_false")]
328 pub skip_permission: bool,
329}
330
331#[inline]
332fn is_false(b: &bool) -> bool {
333 !*b
334}
335
336impl Tool {
337 pub fn new(name: impl Into<String>) -> Self {
357 Self {
358 name: name.into(),
359 ..Default::default()
360 }
361 }
362
363 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
366 self.namespaced_name = Some(namespaced_name.into());
367 self
368 }
369
370 pub fn with_description(mut self, description: impl Into<String>) -> Self {
372 self.description = description.into();
373 self
374 }
375
376 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
378 self.instructions = Some(instructions.into());
379 self
380 }
381
382 pub fn with_parameters(mut self, parameters: Value) -> Self {
390 self.parameters = parameters
391 .as_object()
392 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
393 .unwrap_or_default();
394 self
395 }
396
397 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
401 self.overrides_built_in_tool = overrides;
402 self
403 }
404
405 pub fn with_skip_permission(mut self, skip: bool) -> Self {
409 self.skip_permission = skip;
410 self
411 }
412}
413
414#[non_exhaustive]
417#[derive(Debug, Clone)]
418pub struct CommandContext {
419 pub session_id: SessionId,
421 pub command: String,
423 pub command_name: String,
425 pub args: String,
427}
428
429#[async_trait::async_trait]
435pub trait CommandHandler: Send + Sync {
436 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
438}
439
440#[non_exhaustive]
446#[derive(Clone)]
447pub struct CommandDefinition {
448 pub name: String,
450 pub description: Option<String>,
452 pub handler: Arc<dyn CommandHandler>,
454}
455
456impl CommandDefinition {
457 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
460 Self {
461 name: name.into(),
462 description: None,
463 handler,
464 }
465 }
466
467 pub fn with_description(mut self, description: impl Into<String>) -> Self {
469 self.description = Some(description.into());
470 self
471 }
472}
473
474impl std::fmt::Debug for CommandDefinition {
475 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476 f.debug_struct("CommandDefinition")
477 .field("name", &self.name)
478 .field("description", &self.description)
479 .field("handler", &"<set>")
480 .finish()
481 }
482}
483
484impl Serialize for CommandDefinition {
485 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
486 use serde::ser::SerializeStruct;
487 let len = if self.description.is_some() { 2 } else { 1 };
488 let mut state = serializer.serialize_struct("CommandDefinition", len)?;
489 state.serialize_field("name", &self.name)?;
490 if let Some(description) = &self.description {
491 state.serialize_field("description", description)?;
492 }
493 state.end()
494 }
495}
496
497#[derive(Debug, Clone, Default, Serialize, Deserialize)]
504#[serde(rename_all = "camelCase")]
505#[non_exhaustive]
506pub struct CustomAgentConfig {
507 pub name: String,
509 #[serde(default, skip_serializing_if = "Option::is_none")]
511 pub display_name: Option<String>,
512 #[serde(default, skip_serializing_if = "Option::is_none")]
514 pub description: Option<String>,
515 #[serde(default, skip_serializing_if = "Option::is_none")]
517 pub tools: Option<Vec<String>>,
518 pub prompt: String,
520 #[serde(default, skip_serializing_if = "Option::is_none")]
522 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
523 #[serde(default, skip_serializing_if = "Option::is_none")]
525 pub infer: Option<bool>,
526 #[serde(default, skip_serializing_if = "Option::is_none")]
528 pub skills: Option<Vec<String>>,
529}
530
531impl CustomAgentConfig {
532 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
539 Self {
540 name: name.into(),
541 prompt: prompt.into(),
542 ..Self::default()
543 }
544 }
545
546 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
548 self.display_name = Some(display_name.into());
549 self
550 }
551
552 pub fn with_description(mut self, description: impl Into<String>) -> Self {
554 self.description = Some(description.into());
555 self
556 }
557
558 pub fn with_tools<I, S>(mut self, tools: I) -> Self
561 where
562 I: IntoIterator<Item = S>,
563 S: Into<String>,
564 {
565 self.tools = Some(tools.into_iter().map(Into::into).collect());
566 self
567 }
568
569 pub fn with_mcp_servers(mut self, mcp_servers: HashMap<String, McpServerConfig>) -> Self {
571 self.mcp_servers = Some(mcp_servers);
572 self
573 }
574
575 pub fn with_infer(mut self, infer: bool) -> Self {
577 self.infer = Some(infer);
578 self
579 }
580
581 pub fn with_skills<I, S>(mut self, skills: I) -> Self
583 where
584 I: IntoIterator<Item = S>,
585 S: Into<String>,
586 {
587 self.skills = Some(skills.into_iter().map(Into::into).collect());
588 self
589 }
590}
591
592#[derive(Debug, Clone, Default, Serialize, Deserialize)]
599#[serde(rename_all = "camelCase")]
600pub struct DefaultAgentConfig {
601 #[serde(default, skip_serializing_if = "Option::is_none")]
603 pub excluded_tools: Option<Vec<String>>,
604}
605
606#[derive(Debug, Clone, Default, Serialize, Deserialize)]
613#[serde(rename_all = "camelCase")]
614#[non_exhaustive]
615pub struct InfiniteSessionConfig {
616 #[serde(default, skip_serializing_if = "Option::is_none")]
618 pub enabled: Option<bool>,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
622 pub background_compaction_threshold: Option<f64>,
623 #[serde(default, skip_serializing_if = "Option::is_none")]
626 pub buffer_exhaustion_threshold: Option<f64>,
627}
628
629impl InfiniteSessionConfig {
630 pub fn new() -> Self {
633 Self::default()
634 }
635
636 pub fn with_enabled(mut self, enabled: bool) -> Self {
639 self.enabled = Some(enabled);
640 self
641 }
642
643 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
646 self.background_compaction_threshold = Some(threshold);
647 self
648 }
649
650 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
653 self.buffer_exhaustion_threshold = Some(threshold);
654 self
655 }
656}
657
658#[derive(Debug, Clone, Serialize, Deserialize)]
692#[serde(tag = "type", rename_all = "lowercase")]
693#[non_exhaustive]
694pub enum McpServerConfig {
695 #[serde(alias = "local")]
699 Stdio(McpStdioServerConfig),
700 Http(McpHttpServerConfig),
702 Sse(McpHttpServerConfig),
704}
705
706#[derive(Debug, Clone, Default, Serialize, Deserialize)]
710#[serde(rename_all = "camelCase")]
711pub struct McpStdioServerConfig {
712 #[serde(default)]
714 pub tools: Vec<String>,
715 #[serde(default, skip_serializing_if = "Option::is_none")]
717 pub timeout: Option<i64>,
718 pub command: String,
720 #[serde(default)]
722 pub args: Vec<String>,
723 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
729 pub env: HashMap<String, String>,
730 #[serde(default, skip_serializing_if = "Option::is_none")]
732 pub cwd: Option<String>,
733}
734
735#[derive(Debug, Clone, Default, Serialize, Deserialize)]
739#[serde(rename_all = "camelCase")]
740pub struct McpHttpServerConfig {
741 #[serde(default)]
743 pub tools: Vec<String>,
744 #[serde(default, skip_serializing_if = "Option::is_none")]
746 pub timeout: Option<i64>,
747 pub url: String,
749 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
751 pub headers: HashMap<String, String>,
752}
753
754#[derive(Debug, Clone, Default, Serialize, Deserialize)]
760#[serde(rename_all = "camelCase")]
761#[non_exhaustive]
762pub struct ProviderConfig {
763 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
766 pub provider_type: Option<String>,
767 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub wire_api: Option<String>,
771 pub base_url: String,
773 #[serde(default, skip_serializing_if = "Option::is_none")]
775 pub api_key: Option<String>,
776 #[serde(default, skip_serializing_if = "Option::is_none")]
780 pub bearer_token: Option<String>,
781 #[serde(default, skip_serializing_if = "Option::is_none")]
783 pub azure: Option<AzureProviderOptions>,
784 #[serde(default, skip_serializing_if = "Option::is_none")]
786 pub headers: Option<HashMap<String, String>>,
787 #[serde(default, skip_serializing_if = "Option::is_none")]
791 pub model_id: Option<String>,
792 #[serde(default, skip_serializing_if = "Option::is_none")]
799 pub wire_model: Option<String>,
800 #[serde(default, skip_serializing_if = "Option::is_none")]
805 pub max_prompt_tokens: Option<i64>,
806 #[serde(default, skip_serializing_if = "Option::is_none")]
809 pub max_output_tokens: Option<i64>,
810}
811
812impl ProviderConfig {
813 pub fn new(base_url: impl Into<String>) -> Self {
816 Self {
817 base_url: base_url.into(),
818 ..Self::default()
819 }
820 }
821
822 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
824 self.provider_type = Some(provider_type.into());
825 self
826 }
827
828 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
830 self.wire_api = Some(wire_api.into());
831 self
832 }
833
834 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
836 self.api_key = Some(api_key.into());
837 self
838 }
839
840 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
843 self.bearer_token = Some(bearer_token.into());
844 self
845 }
846
847 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
849 self.azure = Some(azure);
850 self
851 }
852
853 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
855 self.headers = Some(headers);
856 self
857 }
858
859 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
862 self.model_id = Some(model_id.into());
863 self
864 }
865
866 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
871 self.wire_model = Some(wire_model.into());
872 self
873 }
874
875 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
879 self.max_prompt_tokens = Some(max);
880 self
881 }
882
883 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
886 self.max_output_tokens = Some(max);
887 self
888 }
889}
890
891#[derive(Debug, Clone, Default, Serialize, Deserialize)]
893#[serde(rename_all = "camelCase")]
894pub struct AzureProviderOptions {
895 #[serde(default, skip_serializing_if = "Option::is_none")]
897 pub api_version: Option<String>,
898}
899
900#[derive(Clone, Serialize, Deserialize)]
947#[serde(rename_all = "camelCase")]
948#[non_exhaustive]
949pub struct SessionConfig {
950 #[serde(skip_serializing_if = "Option::is_none")]
952 pub session_id: Option<SessionId>,
953 #[serde(skip_serializing_if = "Option::is_none")]
955 pub model: Option<String>,
956 #[serde(skip_serializing_if = "Option::is_none")]
958 pub client_name: Option<String>,
959 #[serde(skip_serializing_if = "Option::is_none")]
961 pub reasoning_effort: Option<String>,
962 #[serde(skip_serializing_if = "Option::is_none")]
964 pub streaming: Option<bool>,
965 #[serde(skip_serializing_if = "Option::is_none")]
967 pub system_message: Option<SystemMessageConfig>,
968 #[serde(skip_serializing_if = "Option::is_none")]
970 pub tools: Option<Vec<Tool>>,
971 #[serde(skip_serializing_if = "Option::is_none")]
973 pub available_tools: Option<Vec<String>>,
974 #[serde(skip_serializing_if = "Option::is_none")]
976 pub excluded_tools: Option<Vec<String>>,
977 #[serde(skip_serializing_if = "Option::is_none")]
979 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
980 #[serde(skip_serializing_if = "Option::is_none")]
983 pub env_value_mode: Option<String>,
984 #[serde(skip_serializing_if = "Option::is_none")]
986 pub enable_config_discovery: Option<bool>,
987 #[serde(skip_serializing_if = "Option::is_none")]
990 pub request_user_input: Option<bool>,
991 #[serde(skip_serializing_if = "Option::is_none")]
996 pub request_permission: Option<bool>,
997 #[serde(skip_serializing_if = "Option::is_none")]
1000 pub request_exit_plan_mode: Option<bool>,
1001 #[serde(skip_serializing_if = "Option::is_none")]
1013 pub request_auto_mode_switch: Option<bool>,
1014 #[serde(skip_serializing_if = "Option::is_none")]
1018 pub request_elicitation: Option<bool>,
1019 #[serde(skip_serializing_if = "Option::is_none")]
1021 pub skill_directories: Option<Vec<PathBuf>>,
1022 #[serde(skip_serializing_if = "Option::is_none")]
1025 pub instruction_directories: Option<Vec<PathBuf>>,
1026 #[serde(skip_serializing_if = "Option::is_none")]
1029 pub disabled_skills: Option<Vec<String>>,
1030 #[serde(skip_serializing_if = "Option::is_none")]
1033 pub disabled_mcp_servers: Option<Vec<String>>,
1034 #[serde(skip_serializing_if = "Option::is_none")]
1038 pub hooks: Option<bool>,
1039 #[serde(skip_serializing_if = "Option::is_none")]
1041 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1042 #[serde(skip_serializing_if = "Option::is_none")]
1046 pub default_agent: Option<DefaultAgentConfig>,
1047 #[serde(skip_serializing_if = "Option::is_none")]
1050 pub agent: Option<String>,
1051 #[serde(skip_serializing_if = "Option::is_none")]
1054 pub infinite_sessions: Option<InfiniteSessionConfig>,
1055 #[serde(skip_serializing_if = "Option::is_none")]
1059 pub provider: Option<ProviderConfig>,
1060 #[serde(skip_serializing_if = "Option::is_none")]
1063 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1064 #[serde(skip_serializing_if = "Option::is_none")]
1067 pub config_dir: Option<PathBuf>,
1068 #[serde(skip_serializing_if = "Option::is_none")]
1071 pub working_directory: Option<PathBuf>,
1072 #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")]
1078 pub github_token: Option<String>,
1079 #[serde(skip_serializing_if = "Option::is_none")]
1083 pub include_sub_agent_streaming_events: Option<bool>,
1084 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
1088 pub commands: Option<Vec<CommandDefinition>>,
1089 #[serde(skip)]
1094 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1095 #[serde(skip)]
1100 pub handler: Option<Arc<dyn SessionHandler>>,
1101 #[serde(skip)]
1105 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1106 #[serde(skip)]
1111 pub transform: Option<Arc<dyn SystemMessageTransform>>,
1112}
1113
1114impl std::fmt::Debug for SessionConfig {
1115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1116 f.debug_struct("SessionConfig")
1117 .field("session_id", &self.session_id)
1118 .field("model", &self.model)
1119 .field("client_name", &self.client_name)
1120 .field("reasoning_effort", &self.reasoning_effort)
1121 .field("streaming", &self.streaming)
1122 .field("system_message", &self.system_message)
1123 .field("tools", &self.tools)
1124 .field("available_tools", &self.available_tools)
1125 .field("excluded_tools", &self.excluded_tools)
1126 .field("mcp_servers", &self.mcp_servers)
1127 .field("env_value_mode", &self.env_value_mode)
1128 .field("enable_config_discovery", &self.enable_config_discovery)
1129 .field("request_user_input", &self.request_user_input)
1130 .field("request_permission", &self.request_permission)
1131 .field("request_exit_plan_mode", &self.request_exit_plan_mode)
1132 .field("request_auto_mode_switch", &self.request_auto_mode_switch)
1133 .field("request_elicitation", &self.request_elicitation)
1134 .field("skill_directories", &self.skill_directories)
1135 .field("instruction_directories", &self.instruction_directories)
1136 .field("disabled_skills", &self.disabled_skills)
1137 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
1138 .field("hooks", &self.hooks)
1139 .field("custom_agents", &self.custom_agents)
1140 .field("default_agent", &self.default_agent)
1141 .field("agent", &self.agent)
1142 .field("infinite_sessions", &self.infinite_sessions)
1143 .field("provider", &self.provider)
1144 .field("model_capabilities", &self.model_capabilities)
1145 .field("config_dir", &self.config_dir)
1146 .field("working_directory", &self.working_directory)
1147 .field(
1148 "github_token",
1149 &self.github_token.as_ref().map(|_| "<redacted>"),
1150 )
1151 .field(
1152 "include_sub_agent_streaming_events",
1153 &self.include_sub_agent_streaming_events,
1154 )
1155 .field("commands", &self.commands)
1156 .field(
1157 "session_fs_provider",
1158 &self.session_fs_provider.as_ref().map(|_| "<set>"),
1159 )
1160 .field("handler", &self.handler.as_ref().map(|_| "<set>"))
1161 .field(
1162 "hooks_handler",
1163 &self.hooks_handler.as_ref().map(|_| "<set>"),
1164 )
1165 .field("transform", &self.transform.as_ref().map(|_| "<set>"))
1166 .finish()
1167 }
1168}
1169
1170impl Default for SessionConfig {
1171 fn default() -> Self {
1178 Self {
1179 session_id: None,
1180 model: None,
1181 client_name: None,
1182 reasoning_effort: None,
1183 streaming: None,
1184 system_message: None,
1185 tools: None,
1186 available_tools: None,
1187 excluded_tools: None,
1188 mcp_servers: None,
1189 env_value_mode: None,
1190 enable_config_discovery: None,
1191 request_user_input: Some(true),
1192 request_permission: Some(true),
1193 request_exit_plan_mode: Some(true),
1194 request_auto_mode_switch: Some(true),
1195 request_elicitation: Some(true),
1196 skill_directories: None,
1197 instruction_directories: None,
1198 disabled_skills: None,
1199 disabled_mcp_servers: None,
1200 hooks: None,
1201 custom_agents: None,
1202 default_agent: None,
1203 agent: None,
1204 infinite_sessions: None,
1205 provider: None,
1206 model_capabilities: None,
1207 config_dir: None,
1208 working_directory: None,
1209 github_token: None,
1210 include_sub_agent_streaming_events: None,
1211 commands: None,
1212 session_fs_provider: None,
1213 handler: None,
1214 hooks_handler: None,
1215 transform: None,
1216 }
1217 }
1218}
1219
1220impl SessionConfig {
1221 pub fn with_handler(mut self, handler: Arc<dyn SessionHandler>) -> Self {
1223 self.handler = Some(handler);
1224 self
1225 }
1226
1227 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
1232 self.commands = Some(commands);
1233 self
1234 }
1235
1236 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
1240 self.session_fs_provider = Some(provider);
1241 self
1242 }
1243
1244 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
1247 self.hooks_handler = Some(hooks);
1248 self
1249 }
1250
1251 pub fn with_transform(mut self, transform: Arc<dyn SystemMessageTransform>) -> Self {
1255 self.transform = Some(transform);
1256 self
1257 }
1258
1259 pub fn approve_all_permissions(mut self) -> Self {
1274 let inner = self
1275 .handler
1276 .take()
1277 .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1278 self.handler = Some(crate::permission::approve_all(inner));
1279 self
1280 }
1281
1282 pub fn deny_all_permissions(mut self) -> Self {
1286 let inner = self
1287 .handler
1288 .take()
1289 .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1290 self.handler = Some(crate::permission::deny_all(inner));
1291 self
1292 }
1293
1294 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
1300 where
1301 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
1302 {
1303 let inner = self
1304 .handler
1305 .take()
1306 .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1307 self.handler = Some(crate::permission::approve_if(inner, predicate));
1308 self
1309 }
1310
1311 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
1313 self.session_id = Some(id.into());
1314 self
1315 }
1316
1317 pub fn with_model(mut self, model: impl Into<String>) -> Self {
1319 self.model = Some(model.into());
1320 self
1321 }
1322
1323 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
1325 self.client_name = Some(name.into());
1326 self
1327 }
1328
1329 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
1331 self.reasoning_effort = Some(effort.into());
1332 self
1333 }
1334
1335 pub fn with_streaming(mut self, streaming: bool) -> Self {
1337 self.streaming = Some(streaming);
1338 self
1339 }
1340
1341 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
1343 self.system_message = Some(system_message);
1344 self
1345 }
1346
1347 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
1349 self.tools = Some(tools.into_iter().collect());
1350 self
1351 }
1352
1353 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
1355 where
1356 I: IntoIterator<Item = S>,
1357 S: Into<String>,
1358 {
1359 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
1360 self
1361 }
1362
1363 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
1365 where
1366 I: IntoIterator<Item = S>,
1367 S: Into<String>,
1368 {
1369 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
1370 self
1371 }
1372
1373 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
1375 self.mcp_servers = Some(servers);
1376 self
1377 }
1378
1379 pub fn with_env_value_mode(mut self, mode: impl Into<String>) -> Self {
1382 self.env_value_mode = Some(mode.into());
1383 self
1384 }
1385
1386 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
1388 self.enable_config_discovery = Some(enable);
1389 self
1390 }
1391
1392 pub fn with_request_user_input(mut self, enable: bool) -> Self {
1394 self.request_user_input = Some(enable);
1395 self
1396 }
1397
1398 pub fn with_request_permission(mut self, enable: bool) -> Self {
1400 self.request_permission = Some(enable);
1401 self
1402 }
1403
1404 pub fn with_request_exit_plan_mode(mut self, enable: bool) -> Self {
1406 self.request_exit_plan_mode = Some(enable);
1407 self
1408 }
1409
1410 pub fn with_request_auto_mode_switch(mut self, enable: bool) -> Self {
1412 self.request_auto_mode_switch = Some(enable);
1413 self
1414 }
1415
1416 pub fn with_request_elicitation(mut self, enable: bool) -> Self {
1418 self.request_elicitation = Some(enable);
1419 self
1420 }
1421
1422 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
1424 where
1425 I: IntoIterator<Item = P>,
1426 P: Into<PathBuf>,
1427 {
1428 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
1429 self
1430 }
1431
1432 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
1436 where
1437 I: IntoIterator<Item = P>,
1438 P: Into<PathBuf>,
1439 {
1440 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
1441 self
1442 }
1443
1444 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
1446 where
1447 I: IntoIterator<Item = S>,
1448 S: Into<String>,
1449 {
1450 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
1451 self
1452 }
1453
1454 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
1456 where
1457 I: IntoIterator<Item = S>,
1458 S: Into<String>,
1459 {
1460 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
1461 self
1462 }
1463
1464 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
1466 mut self,
1467 agents: I,
1468 ) -> Self {
1469 self.custom_agents = Some(agents.into_iter().collect());
1470 self
1471 }
1472
1473 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
1475 self.default_agent = Some(agent);
1476 self
1477 }
1478
1479 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
1482 self.agent = Some(name.into());
1483 self
1484 }
1485
1486 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
1489 self.infinite_sessions = Some(config);
1490 self
1491 }
1492
1493 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
1495 self.provider = Some(provider);
1496 self
1497 }
1498
1499 pub fn with_model_capabilities(
1501 mut self,
1502 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1503 ) -> Self {
1504 self.model_capabilities = Some(capabilities);
1505 self
1506 }
1507
1508 pub fn with_config_dir(mut self, dir: impl Into<PathBuf>) -> Self {
1510 self.config_dir = Some(dir.into());
1511 self
1512 }
1513
1514 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
1517 self.working_directory = Some(dir.into());
1518 self
1519 }
1520
1521 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
1526 self.github_token = Some(token.into());
1527 self
1528 }
1529
1530 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
1533 self.include_sub_agent_streaming_events = Some(include);
1534 self
1535 }
1536}
1537
1538#[derive(Clone, Serialize, Deserialize)]
1544#[serde(rename_all = "camelCase")]
1545#[non_exhaustive]
1546pub struct ResumeSessionConfig {
1547 pub session_id: SessionId,
1549 #[serde(skip_serializing_if = "Option::is_none")]
1551 pub client_name: Option<String>,
1552 #[serde(skip_serializing_if = "Option::is_none")]
1554 pub streaming: Option<bool>,
1555 #[serde(skip_serializing_if = "Option::is_none")]
1558 pub system_message: Option<SystemMessageConfig>,
1559 #[serde(skip_serializing_if = "Option::is_none")]
1561 pub tools: Option<Vec<Tool>>,
1562 #[serde(skip_serializing_if = "Option::is_none")]
1564 pub excluded_tools: Option<Vec<String>>,
1565 #[serde(skip_serializing_if = "Option::is_none")]
1567 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
1568 #[serde(skip_serializing_if = "Option::is_none")]
1570 pub env_value_mode: Option<String>,
1571 #[serde(skip_serializing_if = "Option::is_none")]
1573 pub enable_config_discovery: Option<bool>,
1574 #[serde(skip_serializing_if = "Option::is_none")]
1576 pub request_user_input: Option<bool>,
1577 #[serde(skip_serializing_if = "Option::is_none")]
1579 pub request_permission: Option<bool>,
1580 #[serde(skip_serializing_if = "Option::is_none")]
1582 pub request_exit_plan_mode: Option<bool>,
1583 #[serde(skip_serializing_if = "Option::is_none")]
1587 pub request_auto_mode_switch: Option<bool>,
1588 #[serde(skip_serializing_if = "Option::is_none")]
1590 pub request_elicitation: Option<bool>,
1591 #[serde(skip_serializing_if = "Option::is_none")]
1593 pub skill_directories: Option<Vec<PathBuf>>,
1594 #[serde(skip_serializing_if = "Option::is_none")]
1597 pub instruction_directories: Option<Vec<PathBuf>>,
1598 #[serde(skip_serializing_if = "Option::is_none")]
1600 pub hooks: Option<bool>,
1601 #[serde(skip_serializing_if = "Option::is_none")]
1603 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1604 #[serde(skip_serializing_if = "Option::is_none")]
1606 pub default_agent: Option<DefaultAgentConfig>,
1607 #[serde(skip_serializing_if = "Option::is_none")]
1609 pub agent: Option<String>,
1610 #[serde(skip_serializing_if = "Option::is_none")]
1612 pub infinite_sessions: Option<InfiniteSessionConfig>,
1613 #[serde(skip_serializing_if = "Option::is_none")]
1615 pub provider: Option<ProviderConfig>,
1616 #[serde(skip_serializing_if = "Option::is_none")]
1618 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1619 #[serde(skip_serializing_if = "Option::is_none")]
1621 pub config_dir: Option<PathBuf>,
1622 #[serde(skip_serializing_if = "Option::is_none")]
1624 pub working_directory: Option<PathBuf>,
1625 #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")]
1628 pub github_token: Option<String>,
1629 #[serde(skip_serializing_if = "Option::is_none")]
1631 pub include_sub_agent_streaming_events: Option<bool>,
1632 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
1636 pub commands: Option<Vec<CommandDefinition>>,
1637 #[serde(skip)]
1642 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1643 #[serde(skip_serializing_if = "Option::is_none")]
1646 pub disable_resume: Option<bool>,
1647 #[serde(skip_serializing_if = "Option::is_none")]
1655 pub continue_pending_work: Option<bool>,
1656 #[serde(skip)]
1658 pub handler: Option<Arc<dyn SessionHandler>>,
1659 #[serde(skip)]
1661 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1662 #[serde(skip)]
1664 pub transform: Option<Arc<dyn SystemMessageTransform>>,
1665}
1666
1667impl std::fmt::Debug for ResumeSessionConfig {
1668 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1669 f.debug_struct("ResumeSessionConfig")
1670 .field("session_id", &self.session_id)
1671 .field("client_name", &self.client_name)
1672 .field("streaming", &self.streaming)
1673 .field("system_message", &self.system_message)
1674 .field("tools", &self.tools)
1675 .field("excluded_tools", &self.excluded_tools)
1676 .field("mcp_servers", &self.mcp_servers)
1677 .field("env_value_mode", &self.env_value_mode)
1678 .field("enable_config_discovery", &self.enable_config_discovery)
1679 .field("request_user_input", &self.request_user_input)
1680 .field("request_permission", &self.request_permission)
1681 .field("request_exit_plan_mode", &self.request_exit_plan_mode)
1682 .field("request_auto_mode_switch", &self.request_auto_mode_switch)
1683 .field("request_elicitation", &self.request_elicitation)
1684 .field("skill_directories", &self.skill_directories)
1685 .field("instruction_directories", &self.instruction_directories)
1686 .field("hooks", &self.hooks)
1687 .field("custom_agents", &self.custom_agents)
1688 .field("default_agent", &self.default_agent)
1689 .field("agent", &self.agent)
1690 .field("infinite_sessions", &self.infinite_sessions)
1691 .field("provider", &self.provider)
1692 .field("model_capabilities", &self.model_capabilities)
1693 .field("config_dir", &self.config_dir)
1694 .field("working_directory", &self.working_directory)
1695 .field(
1696 "github_token",
1697 &self.github_token.as_ref().map(|_| "<redacted>"),
1698 )
1699 .field(
1700 "include_sub_agent_streaming_events",
1701 &self.include_sub_agent_streaming_events,
1702 )
1703 .field("commands", &self.commands)
1704 .field(
1705 "session_fs_provider",
1706 &self.session_fs_provider.as_ref().map(|_| "<set>"),
1707 )
1708 .field("handler", &self.handler.as_ref().map(|_| "<set>"))
1709 .field(
1710 "hooks_handler",
1711 &self.hooks_handler.as_ref().map(|_| "<set>"),
1712 )
1713 .field("transform", &self.transform.as_ref().map(|_| "<set>"))
1714 .field("disable_resume", &self.disable_resume)
1715 .field("continue_pending_work", &self.continue_pending_work)
1716 .finish()
1717 }
1718}
1719
1720impl ResumeSessionConfig {
1721 pub fn new(session_id: SessionId) -> Self {
1726 Self {
1727 session_id,
1728 client_name: None,
1729 streaming: None,
1730 system_message: None,
1731 tools: None,
1732 excluded_tools: None,
1733 mcp_servers: None,
1734 env_value_mode: None,
1735 enable_config_discovery: None,
1736 request_user_input: Some(true),
1737 request_permission: Some(true),
1738 request_exit_plan_mode: Some(true),
1739 request_auto_mode_switch: Some(true),
1740 request_elicitation: Some(true),
1741 skill_directories: None,
1742 instruction_directories: None,
1743 hooks: None,
1744 custom_agents: None,
1745 default_agent: None,
1746 agent: None,
1747 infinite_sessions: None,
1748 provider: None,
1749 model_capabilities: None,
1750 config_dir: None,
1751 working_directory: None,
1752 github_token: None,
1753 include_sub_agent_streaming_events: None,
1754 commands: None,
1755 session_fs_provider: None,
1756 disable_resume: None,
1757 continue_pending_work: None,
1758 handler: None,
1759 hooks_handler: None,
1760 transform: None,
1761 }
1762 }
1763
1764 pub fn with_handler(mut self, handler: Arc<dyn SessionHandler>) -> Self {
1766 self.handler = Some(handler);
1767 self
1768 }
1769
1770 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
1773 self.hooks_handler = Some(hooks);
1774 self
1775 }
1776
1777 pub fn with_transform(mut self, transform: Arc<dyn SystemMessageTransform>) -> Self {
1779 self.transform = Some(transform);
1780 self
1781 }
1782
1783 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
1787 self.commands = Some(commands);
1788 self
1789 }
1790
1791 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
1794 self.session_fs_provider = Some(provider);
1795 self
1796 }
1797
1798 pub fn approve_all_permissions(mut self) -> Self {
1802 let inner = self
1803 .handler
1804 .take()
1805 .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1806 self.handler = Some(crate::permission::approve_all(inner));
1807 self
1808 }
1809
1810 pub fn deny_all_permissions(mut self) -> Self {
1814 let inner = self
1815 .handler
1816 .take()
1817 .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1818 self.handler = Some(crate::permission::deny_all(inner));
1819 self
1820 }
1821
1822 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
1825 where
1826 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
1827 {
1828 let inner = self
1829 .handler
1830 .take()
1831 .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1832 self.handler = Some(crate::permission::approve_if(inner, predicate));
1833 self
1834 }
1835
1836 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
1838 self.client_name = Some(name.into());
1839 self
1840 }
1841
1842 pub fn with_streaming(mut self, streaming: bool) -> Self {
1844 self.streaming = Some(streaming);
1845 self
1846 }
1847
1848 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
1851 self.system_message = Some(system_message);
1852 self
1853 }
1854
1855 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
1857 self.tools = Some(tools.into_iter().collect());
1858 self
1859 }
1860
1861 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
1863 where
1864 I: IntoIterator<Item = S>,
1865 S: Into<String>,
1866 {
1867 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
1868 self
1869 }
1870
1871 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
1873 self.mcp_servers = Some(servers);
1874 self
1875 }
1876
1877 pub fn with_env_value_mode(mut self, mode: impl Into<String>) -> Self {
1880 self.env_value_mode = Some(mode.into());
1881 self
1882 }
1883
1884 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
1886 self.enable_config_discovery = Some(enable);
1887 self
1888 }
1889
1890 pub fn with_request_user_input(mut self, enable: bool) -> Self {
1892 self.request_user_input = Some(enable);
1893 self
1894 }
1895
1896 pub fn with_request_permission(mut self, enable: bool) -> Self {
1898 self.request_permission = Some(enable);
1899 self
1900 }
1901
1902 pub fn with_request_exit_plan_mode(mut self, enable: bool) -> Self {
1904 self.request_exit_plan_mode = Some(enable);
1905 self
1906 }
1907
1908 pub fn with_request_auto_mode_switch(mut self, enable: bool) -> Self {
1910 self.request_auto_mode_switch = Some(enable);
1911 self
1912 }
1913
1914 pub fn with_request_elicitation(mut self, enable: bool) -> Self {
1916 self.request_elicitation = Some(enable);
1917 self
1918 }
1919
1920 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
1922 where
1923 I: IntoIterator<Item = P>,
1924 P: Into<PathBuf>,
1925 {
1926 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
1927 self
1928 }
1929
1930 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
1934 where
1935 I: IntoIterator<Item = P>,
1936 P: Into<PathBuf>,
1937 {
1938 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
1939 self
1940 }
1941
1942 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
1944 mut self,
1945 agents: I,
1946 ) -> Self {
1947 self.custom_agents = Some(agents.into_iter().collect());
1948 self
1949 }
1950
1951 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
1953 self.default_agent = Some(agent);
1954 self
1955 }
1956
1957 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
1959 self.agent = Some(name.into());
1960 self
1961 }
1962
1963 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
1965 self.infinite_sessions = Some(config);
1966 self
1967 }
1968
1969 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
1971 self.provider = Some(provider);
1972 self
1973 }
1974
1975 pub fn with_model_capabilities(
1977 mut self,
1978 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1979 ) -> Self {
1980 self.model_capabilities = Some(capabilities);
1981 self
1982 }
1983
1984 pub fn with_config_dir(mut self, dir: impl Into<PathBuf>) -> Self {
1986 self.config_dir = Some(dir.into());
1987 self
1988 }
1989
1990 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
1992 self.working_directory = Some(dir.into());
1993 self
1994 }
1995
1996 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2000 self.github_token = Some(token.into());
2001 self
2002 }
2003
2004 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2006 self.include_sub_agent_streaming_events = Some(include);
2007 self
2008 }
2009
2010 pub fn with_disable_resume(mut self, disable: bool) -> Self {
2013 self.disable_resume = Some(disable);
2014 self
2015 }
2016
2017 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
2023 self.continue_pending_work = Some(continue_pending);
2024 self
2025 }
2026}
2027
2028#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2034#[serde(rename_all = "camelCase")]
2035#[non_exhaustive]
2036pub struct SystemMessageConfig {
2037 #[serde(skip_serializing_if = "Option::is_none")]
2039 pub mode: Option<String>,
2040 #[serde(skip_serializing_if = "Option::is_none")]
2042 pub content: Option<String>,
2043 #[serde(skip_serializing_if = "Option::is_none")]
2045 pub sections: Option<HashMap<String, SectionOverride>>,
2046}
2047
2048impl SystemMessageConfig {
2049 pub fn new() -> Self {
2052 Self::default()
2053 }
2054
2055 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
2058 self.mode = Some(mode.into());
2059 self
2060 }
2061
2062 pub fn with_content(mut self, content: impl Into<String>) -> Self {
2065 self.content = Some(content.into());
2066 self
2067 }
2068
2069 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
2071 self.sections = Some(sections);
2072 self
2073 }
2074}
2075
2076#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2082#[serde(rename_all = "camelCase")]
2083pub struct SectionOverride {
2084 #[serde(skip_serializing_if = "Option::is_none")]
2086 pub action: Option<String>,
2087 #[serde(skip_serializing_if = "Option::is_none")]
2089 pub content: Option<String>,
2090}
2091
2092#[derive(Debug, Clone, Serialize, Deserialize)]
2094#[serde(rename_all = "camelCase")]
2095pub struct CreateSessionResult {
2096 pub session_id: SessionId,
2098 #[serde(skip_serializing_if = "Option::is_none")]
2100 pub workspace_path: Option<PathBuf>,
2101 #[serde(default, alias = "remote_url")]
2103 pub remote_url: Option<String>,
2104 #[serde(skip_serializing_if = "Option::is_none")]
2106 pub capabilities: Option<SessionCapabilities>,
2107}
2108
2109#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
2111#[serde(rename_all = "lowercase")]
2112pub enum LogLevel {
2113 #[default]
2115 Info,
2116 Warning,
2118 Error,
2120}
2121
2122#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
2127#[serde(rename_all = "camelCase")]
2128pub struct LogOptions {
2129 #[serde(skip_serializing_if = "Option::is_none")]
2131 pub level: Option<LogLevel>,
2132 #[serde(skip_serializing_if = "Option::is_none")]
2135 pub ephemeral: Option<bool>,
2136}
2137
2138impl LogOptions {
2139 pub fn with_level(mut self, level: LogLevel) -> Self {
2141 self.level = Some(level);
2142 self
2143 }
2144
2145 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
2147 self.ephemeral = Some(ephemeral);
2148 self
2149 }
2150}
2151
2152#[derive(Debug, Clone, Default)]
2156pub struct SetModelOptions {
2157 pub reasoning_effort: Option<String>,
2160 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2164}
2165
2166impl SetModelOptions {
2167 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2169 self.reasoning_effort = Some(effort.into());
2170 self
2171 }
2172
2173 pub fn with_model_capabilities(
2175 mut self,
2176 caps: crate::generated::api_types::ModelCapabilitiesOverride,
2177 ) -> Self {
2178 self.model_capabilities = Some(caps);
2179 self
2180 }
2181}
2182
2183#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2190#[serde(rename_all = "camelCase")]
2191pub struct PingResponse {
2192 #[serde(default)]
2194 pub message: String,
2195 #[serde(default)]
2197 pub timestamp: i64,
2198 #[serde(skip_serializing_if = "Option::is_none")]
2200 pub protocol_version: Option<u32>,
2201}
2202
2203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2205#[serde(rename_all = "camelCase")]
2206pub struct AttachmentLineRange {
2207 pub start: u32,
2209 pub end: u32,
2211}
2212
2213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2215#[serde(rename_all = "camelCase")]
2216pub struct AttachmentSelectionPosition {
2217 pub line: u32,
2219 pub character: u32,
2221}
2222
2223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2225#[serde(rename_all = "camelCase")]
2226pub struct AttachmentSelectionRange {
2227 pub start: AttachmentSelectionPosition,
2229 pub end: AttachmentSelectionPosition,
2231}
2232
2233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2235#[serde(rename_all = "snake_case")]
2236#[non_exhaustive]
2237pub enum GitHubReferenceType {
2238 Issue,
2240 Pr,
2242 Discussion,
2244}
2245
2246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2248#[serde(
2249 tag = "type",
2250 rename_all = "camelCase",
2251 rename_all_fields = "camelCase"
2252)]
2253#[non_exhaustive]
2254pub enum Attachment {
2255 File {
2257 path: PathBuf,
2259 #[serde(skip_serializing_if = "Option::is_none")]
2261 display_name: Option<String>,
2262 #[serde(skip_serializing_if = "Option::is_none")]
2264 line_range: Option<AttachmentLineRange>,
2265 },
2266 Directory {
2268 path: PathBuf,
2270 #[serde(skip_serializing_if = "Option::is_none")]
2272 display_name: Option<String>,
2273 },
2274 Selection {
2276 file_path: PathBuf,
2278 text: String,
2280 #[serde(skip_serializing_if = "Option::is_none")]
2282 display_name: Option<String>,
2283 selection: AttachmentSelectionRange,
2285 },
2286 Blob {
2288 data: String,
2290 mime_type: String,
2292 #[serde(skip_serializing_if = "Option::is_none")]
2294 display_name: Option<String>,
2295 },
2296 #[serde(rename = "github_reference")]
2298 GitHubReference {
2299 number: u64,
2301 title: String,
2303 reference_type: GitHubReferenceType,
2305 state: String,
2307 url: String,
2309 },
2310}
2311
2312impl Attachment {
2313 pub fn display_name(&self) -> Option<&str> {
2315 match self {
2316 Self::File { display_name, .. }
2317 | Self::Directory { display_name, .. }
2318 | Self::Selection { display_name, .. }
2319 | Self::Blob { display_name, .. } => display_name.as_deref(),
2320 Self::GitHubReference { .. } => None,
2321 }
2322 }
2323
2324 pub fn label(&self) -> Option<String> {
2326 if let Some(display_name) = self
2327 .display_name()
2328 .map(str::trim)
2329 .filter(|name| !name.is_empty())
2330 {
2331 return Some(display_name.to_string());
2332 }
2333
2334 match self {
2335 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
2336 format!("#{}", number)
2337 } else {
2338 title.trim().to_string()
2339 }),
2340 _ => self.derived_display_name(),
2341 }
2342 }
2343
2344 pub fn ensure_display_name(&mut self) {
2346 if self
2347 .display_name()
2348 .map(str::trim)
2349 .is_some_and(|name| !name.is_empty())
2350 {
2351 return;
2352 }
2353
2354 let Some(derived_display_name) = self.derived_display_name() else {
2355 return;
2356 };
2357
2358 match self {
2359 Self::File { display_name, .. }
2360 | Self::Directory { display_name, .. }
2361 | Self::Selection { display_name, .. }
2362 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
2363 Self::GitHubReference { .. } => {}
2364 }
2365 }
2366
2367 fn derived_display_name(&self) -> Option<String> {
2368 match self {
2369 Self::File { path, .. } | Self::Directory { path, .. } => {
2370 Some(attachment_name_from_path(path))
2371 }
2372 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
2373 Self::Blob { .. } => Some("attachment".to_string()),
2374 Self::GitHubReference { .. } => None,
2375 }
2376 }
2377}
2378
2379fn attachment_name_from_path(path: &Path) -> String {
2380 path.file_name()
2381 .map(|name| name.to_string_lossy().into_owned())
2382 .filter(|name| !name.is_empty())
2383 .unwrap_or_else(|| {
2384 let full = path.to_string_lossy();
2385 if full.is_empty() {
2386 "attachment".to_string()
2387 } else {
2388 full.into_owned()
2389 }
2390 })
2391}
2392
2393pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
2395 for attachment in attachments {
2396 attachment.ensure_display_name();
2397 }
2398}
2399
2400#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2405#[serde(rename_all = "lowercase")]
2406#[non_exhaustive]
2407pub enum DeliveryMode {
2408 Enqueue,
2410 Immediate,
2412}
2413
2414#[derive(Debug, Clone)]
2443#[non_exhaustive]
2444pub struct MessageOptions {
2445 pub prompt: String,
2447 pub mode: Option<DeliveryMode>,
2453 pub attachments: Option<Vec<Attachment>>,
2455 pub wait_timeout: Option<Duration>,
2458 pub request_headers: Option<HashMap<String, String>>,
2462 pub traceparent: Option<String>,
2469 pub tracestate: Option<String>,
2473}
2474
2475impl MessageOptions {
2476 pub fn new(prompt: impl Into<String>) -> Self {
2478 Self {
2479 prompt: prompt.into(),
2480 mode: None,
2481 attachments: None,
2482 wait_timeout: None,
2483 request_headers: None,
2484 traceparent: None,
2485 tracestate: None,
2486 }
2487 }
2488
2489 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
2495 self.mode = Some(mode);
2496 self
2497 }
2498
2499 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
2501 self.attachments = Some(attachments);
2502 self
2503 }
2504
2505 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
2507 self.wait_timeout = Some(timeout);
2508 self
2509 }
2510
2511 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
2513 self.request_headers = Some(headers);
2514 self
2515 }
2516
2517 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
2522 self.traceparent = ctx.traceparent;
2523 self.tracestate = ctx.tracestate;
2524 self
2525 }
2526
2527 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
2529 self.traceparent = Some(traceparent.into());
2530 self
2531 }
2532
2533 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
2535 self.tracestate = Some(tracestate.into());
2536 self
2537 }
2538}
2539
2540impl From<&str> for MessageOptions {
2541 fn from(prompt: &str) -> Self {
2542 Self::new(prompt)
2543 }
2544}
2545
2546impl From<String> for MessageOptions {
2547 fn from(prompt: String) -> Self {
2548 Self::new(prompt)
2549 }
2550}
2551
2552impl From<&String> for MessageOptions {
2553 fn from(prompt: &String) -> Self {
2554 Self::new(prompt.clone())
2555 }
2556}
2557
2558#[derive(Debug, Clone, Serialize, Deserialize)]
2560#[serde(rename_all = "camelCase")]
2561#[non_exhaustive]
2562pub struct GetStatusResponse {
2563 pub version: String,
2565 pub protocol_version: u32,
2567}
2568
2569#[derive(Debug, Clone, Serialize, Deserialize)]
2571#[serde(rename_all = "camelCase")]
2572#[non_exhaustive]
2573pub struct GetAuthStatusResponse {
2574 pub is_authenticated: bool,
2576 #[serde(skip_serializing_if = "Option::is_none")]
2579 pub auth_type: Option<String>,
2580 #[serde(skip_serializing_if = "Option::is_none")]
2582 pub host: Option<String>,
2583 #[serde(skip_serializing_if = "Option::is_none")]
2585 pub login: Option<String>,
2586 #[serde(skip_serializing_if = "Option::is_none")]
2588 pub status_message: Option<String>,
2589}
2590
2591#[derive(Debug, Clone, Serialize, Deserialize)]
2595#[serde(rename_all = "camelCase")]
2596pub struct SessionEventNotification {
2597 pub session_id: SessionId,
2599 pub event: SessionEvent,
2601}
2602
2603#[derive(Debug, Clone, Serialize, Deserialize)]
2610#[serde(rename_all = "camelCase")]
2611pub struct SessionEvent {
2612 pub id: String,
2614 pub timestamp: String,
2616 pub parent_id: Option<String>,
2618 #[serde(skip_serializing_if = "Option::is_none")]
2620 pub ephemeral: Option<bool>,
2621 #[serde(skip_serializing_if = "Option::is_none")]
2624 pub agent_id: Option<String>,
2625 #[serde(skip_serializing_if = "Option::is_none")]
2627 pub debug_cli_received_at_ms: Option<i64>,
2628 #[serde(skip_serializing_if = "Option::is_none")]
2630 pub debug_ws_forwarded_at_ms: Option<i64>,
2631 #[serde(rename = "type")]
2633 pub event_type: String,
2634 pub data: Value,
2636}
2637
2638impl SessionEvent {
2639 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
2644 use serde::de::IntoDeserializer;
2645 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
2646 self.event_type.as_str().into_deserializer();
2647 crate::generated::SessionEventType::deserialize(deserializer)
2648 .unwrap_or(crate::generated::SessionEventType::Unknown)
2649 }
2650
2651 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
2657 serde_json::from_value(self.data.clone()).ok()
2658 }
2659
2660 pub fn is_transient_error(&self) -> bool {
2664 self.event_type == "session.error"
2665 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
2666 }
2667}
2668
2669#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2674#[serde(rename_all = "camelCase")]
2675#[non_exhaustive]
2676pub struct ToolInvocation {
2677 pub session_id: SessionId,
2679 pub tool_call_id: String,
2681 pub tool_name: String,
2683 pub arguments: Value,
2685 #[serde(default, skip_serializing_if = "Option::is_none")]
2690 pub traceparent: Option<String>,
2691 #[serde(default, skip_serializing_if = "Option::is_none")]
2694 pub tracestate: Option<String>,
2695}
2696
2697impl ToolInvocation {
2698 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
2719 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
2720 }
2721
2722 pub fn trace_context(&self) -> TraceContext {
2725 TraceContext {
2726 traceparent: self.traceparent.clone(),
2727 tracestate: self.tracestate.clone(),
2728 }
2729 }
2730}
2731
2732#[derive(Debug, Clone, Serialize, Deserialize)]
2734#[serde(rename_all = "camelCase")]
2735pub struct ToolResultExpanded {
2736 pub text_result_for_llm: String,
2738 pub result_type: String,
2740 #[serde(skip_serializing_if = "Option::is_none")]
2742 pub session_log: Option<String>,
2743 #[serde(skip_serializing_if = "Option::is_none")]
2745 pub error: Option<String>,
2746}
2747
2748#[derive(Debug, Clone, Serialize, Deserialize)]
2750#[serde(untagged)]
2751#[non_exhaustive]
2752pub enum ToolResult {
2753 Text(String),
2755 Expanded(ToolResultExpanded),
2757}
2758
2759#[derive(Debug, Clone, Serialize, Deserialize)]
2761#[serde(rename_all = "camelCase")]
2762pub struct ToolResultResponse {
2763 pub result: ToolResult,
2765}
2766
2767#[derive(Debug, Clone, Serialize, Deserialize)]
2769#[serde(rename_all = "camelCase")]
2770pub struct SessionMetadata {
2771 pub session_id: SessionId,
2773 pub start_time: String,
2775 pub modified_time: String,
2777 #[serde(skip_serializing_if = "Option::is_none")]
2779 pub summary: Option<String>,
2780 pub is_remote: bool,
2782}
2783
2784#[derive(Debug, Clone, Serialize, Deserialize)]
2786#[serde(rename_all = "camelCase")]
2787pub struct ListSessionsResponse {
2788 pub sessions: Vec<SessionMetadata>,
2790}
2791
2792#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2796#[serde(rename_all = "camelCase")]
2797pub struct SessionListFilter {
2798 #[serde(default, skip_serializing_if = "Option::is_none")]
2800 pub cwd: Option<String>,
2801 #[serde(default, skip_serializing_if = "Option::is_none")]
2803 pub git_root: Option<String>,
2804 #[serde(default, skip_serializing_if = "Option::is_none")]
2806 pub repository: Option<String>,
2807 #[serde(default, skip_serializing_if = "Option::is_none")]
2809 pub branch: Option<String>,
2810}
2811
2812#[derive(Debug, Clone, Serialize, Deserialize)]
2814#[serde(rename_all = "camelCase")]
2815pub struct GetSessionMetadataResponse {
2816 #[serde(skip_serializing_if = "Option::is_none")]
2818 pub session: Option<SessionMetadata>,
2819}
2820
2821#[derive(Debug, Clone, Serialize, Deserialize)]
2823#[serde(rename_all = "camelCase")]
2824pub struct GetLastSessionIdResponse {
2825 #[serde(skip_serializing_if = "Option::is_none")]
2827 pub session_id: Option<SessionId>,
2828}
2829
2830#[derive(Debug, Clone, Serialize, Deserialize)]
2832#[serde(rename_all = "camelCase")]
2833pub struct GetForegroundSessionResponse {
2834 #[serde(skip_serializing_if = "Option::is_none")]
2836 pub session_id: Option<SessionId>,
2837}
2838
2839#[derive(Debug, Clone, Serialize, Deserialize)]
2841#[serde(rename_all = "camelCase")]
2842pub struct GetMessagesResponse {
2843 pub events: Vec<SessionEvent>,
2845}
2846
2847#[derive(Debug, Clone, Serialize, Deserialize)]
2849#[serde(rename_all = "camelCase")]
2850pub struct ElicitationResult {
2851 pub action: String,
2853 #[serde(skip_serializing_if = "Option::is_none")]
2855 pub content: Option<Value>,
2856}
2857
2858#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2864#[serde(rename_all = "camelCase")]
2865#[non_exhaustive]
2866pub enum ElicitationMode {
2867 Form,
2869 Url,
2871 #[serde(other)]
2873 Unknown,
2874}
2875
2876#[derive(Debug, Clone, Serialize, Deserialize)]
2882#[serde(rename_all = "camelCase")]
2883pub struct ElicitationRequest {
2884 pub message: String,
2886 #[serde(skip_serializing_if = "Option::is_none")]
2888 pub requested_schema: Option<Value>,
2889 #[serde(skip_serializing_if = "Option::is_none")]
2891 pub mode: Option<ElicitationMode>,
2892 #[serde(skip_serializing_if = "Option::is_none")]
2894 pub elicitation_source: Option<String>,
2895 #[serde(skip_serializing_if = "Option::is_none")]
2897 pub url: Option<String>,
2898}
2899
2900#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2905#[serde(rename_all = "camelCase")]
2906pub struct SessionCapabilities {
2907 #[serde(skip_serializing_if = "Option::is_none")]
2909 pub ui: Option<UiCapabilities>,
2910}
2911
2912#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2914#[serde(rename_all = "camelCase")]
2915pub struct UiCapabilities {
2916 #[serde(skip_serializing_if = "Option::is_none")]
2918 pub elicitation: Option<bool>,
2919}
2920
2921#[derive(Debug, Clone, Default)]
2923pub struct InputOptions<'a> {
2924 pub title: Option<&'a str>,
2926 pub description: Option<&'a str>,
2928 pub min_length: Option<u64>,
2930 pub max_length: Option<u64>,
2932 pub format: Option<InputFormat>,
2934 pub default: Option<&'a str>,
2936}
2937
2938#[derive(Debug, Clone, Copy)]
2940#[non_exhaustive]
2941pub enum InputFormat {
2942 Email,
2944 Uri,
2946 Date,
2948 DateTime,
2950}
2951
2952impl InputFormat {
2953 pub fn as_str(&self) -> &'static str {
2955 match self {
2956 Self::Email => "email",
2957 Self::Uri => "uri",
2958 Self::Date => "date",
2959 Self::DateTime => "date-time",
2960 }
2961 }
2962}
2963
2964pub use crate::generated::api_types::{
2969 Model, ModelBilling, ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
2970 ModelCapabilitiesSupports, ModelList, ModelPolicy,
2971};
2972
2973#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2979#[serde(rename_all = "kebab-case")]
2980#[non_exhaustive]
2981pub enum PermissionRequestKind {
2982 Shell,
2984 Write,
2986 Read,
2988 Url,
2990 Mcp,
2992 CustomTool,
2994 Memory,
2996 Hook,
2998 #[serde(other)]
3001 Unknown,
3002}
3003
3004#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3011#[serde(rename_all = "camelCase")]
3012pub struct PermissionRequestData {
3013 #[serde(default, skip_serializing_if = "Option::is_none")]
3017 pub kind: Option<PermissionRequestKind>,
3018 #[serde(default, skip_serializing_if = "Option::is_none")]
3021 pub tool_call_id: Option<String>,
3022 #[serde(flatten)]
3025 pub extra: Value,
3026}
3027
3028#[derive(Debug, Clone, Serialize, Deserialize)]
3030#[serde(rename_all = "camelCase")]
3031pub struct ExitPlanModeData {
3032 #[serde(default)]
3034 pub summary: String,
3035 #[serde(default, skip_serializing_if = "Option::is_none")]
3037 pub plan_content: Option<String>,
3038 #[serde(default)]
3040 pub actions: Vec<String>,
3041 #[serde(default = "default_recommended_action")]
3043 pub recommended_action: String,
3044}
3045
3046fn default_recommended_action() -> String {
3047 "autopilot".to_string()
3048}
3049
3050impl Default for ExitPlanModeData {
3051 fn default() -> Self {
3052 Self {
3053 summary: String::new(),
3054 plan_content: None,
3055 actions: Vec::new(),
3056 recommended_action: default_recommended_action(),
3057 }
3058 }
3059}
3060
3061#[cfg(test)]
3062mod tests {
3063 use std::path::PathBuf;
3064
3065 use serde_json::json;
3066
3067 use super::{
3068 Attachment, AttachmentLineRange, AttachmentSelectionPosition, AttachmentSelectionRange,
3069 ConnectionState, CustomAgentConfig, DeliveryMode, GitHubReferenceType,
3070 InfiniteSessionConfig, ProviderConfig, ResumeSessionConfig, SessionConfig, SessionEvent,
3071 SessionId, SystemMessageConfig, Tool, ensure_attachment_display_names,
3072 };
3073 use crate::generated::session_events::TypedSessionEvent;
3074
3075 #[test]
3076 fn tool_builder_composes() {
3077 let tool = Tool::new("greet")
3078 .with_description("Say hello")
3079 .with_namespaced_name("hello/greet")
3080 .with_instructions("Pass the user's name")
3081 .with_parameters(json!({
3082 "type": "object",
3083 "properties": { "name": { "type": "string" } },
3084 "required": ["name"]
3085 }))
3086 .with_overrides_built_in_tool(true)
3087 .with_skip_permission(true);
3088 assert_eq!(tool.name, "greet");
3089 assert_eq!(tool.description, "Say hello");
3090 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
3091 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
3092 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
3093 assert!(tool.overrides_built_in_tool);
3094 assert!(tool.skip_permission);
3095 }
3096
3097 #[test]
3098 fn tool_with_parameters_handles_non_object_value() {
3099 let tool = Tool::new("noop").with_parameters(json!(null));
3100 assert!(tool.parameters.is_empty());
3101 }
3102
3103 #[test]
3104 fn session_config_default_enables_permission_flow_flags() {
3105 let cfg = SessionConfig::default();
3106 assert_eq!(cfg.request_user_input, Some(true));
3107 assert_eq!(cfg.request_permission, Some(true));
3108 assert_eq!(cfg.request_exit_plan_mode, Some(true));
3109 assert_eq!(cfg.request_auto_mode_switch, Some(true));
3110 assert_eq!(cfg.request_elicitation, Some(true));
3111 }
3112
3113 #[test]
3114 fn resume_session_config_new_enables_permission_flow_flags() {
3115 let cfg = ResumeSessionConfig::new(SessionId::from("test-id"));
3116 assert_eq!(cfg.request_user_input, Some(true));
3117 assert_eq!(cfg.request_permission, Some(true));
3118 assert_eq!(cfg.request_exit_plan_mode, Some(true));
3119 assert_eq!(cfg.request_auto_mode_switch, Some(true));
3120 assert_eq!(cfg.request_elicitation, Some(true));
3121 }
3122
3123 #[test]
3124 fn session_config_builder_composes() {
3125 use std::collections::HashMap;
3126
3127 let cfg = SessionConfig::default()
3128 .with_session_id(SessionId::from("sess-1"))
3129 .with_model("claude-sonnet-4")
3130 .with_client_name("test-app")
3131 .with_reasoning_effort("medium")
3132 .with_streaming(true)
3133 .with_tools([Tool::new("greet")])
3134 .with_available_tools(["bash", "view"])
3135 .with_excluded_tools(["dangerous"])
3136 .with_mcp_servers(HashMap::new())
3137 .with_env_value_mode("direct")
3138 .with_enable_config_discovery(true)
3139 .with_request_user_input(false)
3140 .with_skill_directories([PathBuf::from("/tmp/skills")])
3141 .with_disabled_skills(["broken-skill"])
3142 .with_disabled_mcp_servers(["broken-server"])
3143 .with_agent("researcher")
3144 .with_config_dir(PathBuf::from("/tmp/config"))
3145 .with_working_directory(PathBuf::from("/tmp/work"))
3146 .with_github_token("ghp_test")
3147 .with_include_sub_agent_streaming_events(false);
3148
3149 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
3150 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
3151 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
3152 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
3153 assert_eq!(cfg.streaming, Some(true));
3154 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
3155 assert_eq!(
3156 cfg.available_tools.as_deref(),
3157 Some(&["bash".to_string(), "view".to_string()][..])
3158 );
3159 assert_eq!(
3160 cfg.excluded_tools.as_deref(),
3161 Some(&["dangerous".to_string()][..])
3162 );
3163 assert!(cfg.mcp_servers.is_some());
3164 assert_eq!(cfg.env_value_mode.as_deref(), Some("direct"));
3165 assert_eq!(cfg.enable_config_discovery, Some(true));
3166 assert_eq!(cfg.request_user_input, Some(false)); assert_eq!(cfg.request_permission, Some(true)); assert_eq!(
3169 cfg.skill_directories.as_deref(),
3170 Some(&[PathBuf::from("/tmp/skills")][..])
3171 );
3172 assert_eq!(
3173 cfg.disabled_skills.as_deref(),
3174 Some(&["broken-skill".to_string()][..])
3175 );
3176 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
3177 assert_eq!(cfg.config_dir, Some(PathBuf::from("/tmp/config")));
3178 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
3179 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
3180 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
3181 }
3182
3183 #[test]
3184 fn resume_session_config_builder_composes() {
3185 use std::collections::HashMap;
3186
3187 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
3188 .with_client_name("test-app")
3189 .with_streaming(true)
3190 .with_tools([Tool::new("greet")])
3191 .with_excluded_tools(["dangerous"])
3192 .with_mcp_servers(HashMap::new())
3193 .with_env_value_mode("indirect")
3194 .with_enable_config_discovery(true)
3195 .with_request_user_input(false)
3196 .with_skill_directories([PathBuf::from("/tmp/skills")])
3197 .with_agent("researcher")
3198 .with_config_dir(PathBuf::from("/tmp/config"))
3199 .with_working_directory(PathBuf::from("/tmp/work"))
3200 .with_github_token("ghp_test")
3201 .with_include_sub_agent_streaming_events(true)
3202 .with_disable_resume(true)
3203 .with_continue_pending_work(true);
3204
3205 assert_eq!(cfg.session_id.as_str(), "sess-2");
3206 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
3207 assert_eq!(cfg.streaming, Some(true));
3208 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
3209 assert_eq!(
3210 cfg.excluded_tools.as_deref(),
3211 Some(&["dangerous".to_string()][..])
3212 );
3213 assert!(cfg.mcp_servers.is_some());
3214 assert_eq!(cfg.env_value_mode.as_deref(), Some("indirect"));
3215 assert_eq!(cfg.enable_config_discovery, Some(true));
3216 assert_eq!(cfg.request_user_input, Some(false)); assert_eq!(cfg.request_permission, Some(true)); assert_eq!(
3219 cfg.skill_directories.as_deref(),
3220 Some(&[PathBuf::from("/tmp/skills")][..])
3221 );
3222 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
3223 assert_eq!(cfg.config_dir, Some(PathBuf::from("/tmp/config")));
3224 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
3225 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
3226 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
3227 assert_eq!(cfg.disable_resume, Some(true));
3228 assert_eq!(cfg.continue_pending_work, Some(true));
3229 }
3230
3231 #[test]
3235 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
3236 let cfg =
3237 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
3238 let wire = serde_json::to_value(&cfg).unwrap();
3239 assert_eq!(wire["continuePendingWork"], true);
3240
3241 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"));
3243 let wire = serde_json::to_value(&cfg).unwrap();
3244 assert!(wire.get("continuePendingWork").is_none());
3245 }
3246
3247 #[test]
3251 fn session_config_serializes_instruction_directories_to_camel_case() {
3252 let cfg =
3253 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
3254 let wire = serde_json::to_value(&cfg).unwrap();
3255 assert_eq!(
3256 wire["instructionDirectories"],
3257 serde_json::json!(["/tmp/instr"])
3258 );
3259
3260 let cfg = SessionConfig::default();
3262 let wire = serde_json::to_value(&cfg).unwrap();
3263 assert!(wire.get("instructionDirectories").is_none());
3264 }
3265
3266 #[test]
3269 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
3270 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
3271 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
3272 let wire = serde_json::to_value(&cfg).unwrap();
3273 assert_eq!(
3274 wire["instructionDirectories"],
3275 serde_json::json!(["/tmp/instr"])
3276 );
3277
3278 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"));
3279 let wire = serde_json::to_value(&cfg).unwrap();
3280 assert!(wire.get("instructionDirectories").is_none());
3281 }
3282
3283 #[test]
3284 fn custom_agent_config_builder_composes() {
3285 use std::collections::HashMap;
3286
3287 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
3288 .with_display_name("Research Assistant")
3289 .with_description("Investigates technical questions.")
3290 .with_tools(["bash", "view"])
3291 .with_mcp_servers(HashMap::new())
3292 .with_infer(true)
3293 .with_skills(["rust-coding-skill"]);
3294
3295 assert_eq!(cfg.name, "researcher");
3296 assert_eq!(cfg.prompt, "You are a research assistant.");
3297 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
3298 assert_eq!(
3299 cfg.description.as_deref(),
3300 Some("Investigates technical questions.")
3301 );
3302 assert_eq!(
3303 cfg.tools.as_deref(),
3304 Some(&["bash".to_string(), "view".to_string()][..])
3305 );
3306 assert!(cfg.mcp_servers.is_some());
3307 assert_eq!(cfg.infer, Some(true));
3308 assert_eq!(
3309 cfg.skills.as_deref(),
3310 Some(&["rust-coding-skill".to_string()][..])
3311 );
3312 }
3313
3314 #[test]
3315 fn infinite_session_config_builder_composes() {
3316 let cfg = InfiniteSessionConfig::new()
3317 .with_enabled(true)
3318 .with_background_compaction_threshold(0.75)
3319 .with_buffer_exhaustion_threshold(0.92);
3320
3321 assert_eq!(cfg.enabled, Some(true));
3322 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
3323 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
3324 }
3325
3326 #[test]
3327 fn provider_config_builder_composes() {
3328 use std::collections::HashMap;
3329
3330 let mut headers = HashMap::new();
3331 headers.insert("X-Custom".to_string(), "value".to_string());
3332
3333 let cfg = ProviderConfig::new("https://api.example.com")
3334 .with_provider_type("openai")
3335 .with_wire_api("completions")
3336 .with_api_key("sk-test")
3337 .with_bearer_token("bearer-test")
3338 .with_headers(headers)
3339 .with_model_id("gpt-4")
3340 .with_wire_model("azure-gpt-4-deployment")
3341 .with_max_prompt_tokens(8192)
3342 .with_max_output_tokens(2048);
3343
3344 assert_eq!(cfg.base_url, "https://api.example.com");
3345 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
3346 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
3347 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
3348 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
3349 assert_eq!(
3350 cfg.headers
3351 .as_ref()
3352 .and_then(|h| h.get("X-Custom"))
3353 .map(String::as_str),
3354 Some("value"),
3355 );
3356 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
3357 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
3358 assert_eq!(cfg.max_prompt_tokens, Some(8192));
3359 assert_eq!(cfg.max_output_tokens, Some(2048));
3360
3361 let wire = serde_json::to_value(&cfg).unwrap();
3363 assert_eq!(wire["modelId"], "gpt-4");
3364 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
3365 assert_eq!(wire["maxPromptTokens"], 8192);
3366 assert_eq!(wire["maxOutputTokens"], 2048);
3367
3368 let unset = ProviderConfig::new("https://api.example.com");
3369 let wire_unset = serde_json::to_value(&unset).unwrap();
3370 assert!(wire_unset.get("modelId").is_none());
3371 assert!(wire_unset.get("wireModel").is_none());
3372 assert!(wire_unset.get("maxPromptTokens").is_none());
3373 assert!(wire_unset.get("maxOutputTokens").is_none());
3374 }
3375
3376 #[test]
3377 fn system_message_config_builder_composes() {
3378 use std::collections::HashMap;
3379
3380 let cfg = SystemMessageConfig::new()
3381 .with_mode("replace")
3382 .with_content("Custom system message.")
3383 .with_sections(HashMap::new());
3384
3385 assert_eq!(cfg.mode.as_deref(), Some("replace"));
3386 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
3387 assert!(cfg.sections.is_some());
3388 }
3389
3390 #[test]
3391 fn delivery_mode_serializes_to_kebab_case_strings() {
3392 assert_eq!(
3393 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
3394 "\"enqueue\""
3395 );
3396 assert_eq!(
3397 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
3398 "\"immediate\""
3399 );
3400 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
3401 assert_eq!(parsed, DeliveryMode::Immediate);
3402 }
3403
3404 #[test]
3405 fn connection_state_error_serializes_to_match_go() {
3406 let json = serde_json::to_string(&ConnectionState::Error).unwrap();
3407 assert_eq!(json, "\"error\"");
3408 let parsed: ConnectionState = serde_json::from_str("\"error\"").unwrap();
3409 assert_eq!(parsed, ConnectionState::Error);
3410 }
3411
3412 #[test]
3418 fn session_event_round_trips_agent_id_on_envelope() {
3419 let wire = json!({
3420 "id": "evt-1",
3421 "timestamp": "2026-04-30T12:00:00Z",
3422 "parentId": null,
3423 "agentId": "sub-agent-42",
3424 "type": "assistant.message",
3425 "data": { "message": "hi" }
3426 });
3427
3428 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
3429 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
3430
3431 let roundtripped = serde_json::to_value(&event).unwrap();
3433 assert_eq!(roundtripped["agentId"], "sub-agent-42");
3434
3435 let main_agent_event: SessionEvent = serde_json::from_value(json!({
3437 "id": "evt-2",
3438 "timestamp": "2026-04-30T12:00:01Z",
3439 "parentId": null,
3440 "type": "session.idle",
3441 "data": {}
3442 }))
3443 .unwrap();
3444 assert!(main_agent_event.agent_id.is_none());
3445 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
3446 assert!(roundtripped.get("agentId").is_none());
3447 }
3448
3449 #[test]
3451 fn typed_session_event_round_trips_agent_id_on_envelope() {
3452 let wire = json!({
3453 "id": "evt-1",
3454 "timestamp": "2026-04-30T12:00:00Z",
3455 "parentId": null,
3456 "agentId": "sub-agent-42",
3457 "type": "session.idle",
3458 "data": {}
3459 });
3460
3461 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
3462 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
3463
3464 let roundtripped = serde_json::to_value(&event).unwrap();
3465 assert_eq!(roundtripped["agentId"], "sub-agent-42");
3466 }
3467
3468 #[test]
3469 fn connection_state_other_variants_serialize_as_lowercase() {
3470 assert_eq!(
3471 serde_json::to_string(&ConnectionState::Disconnected).unwrap(),
3472 "\"disconnected\""
3473 );
3474 assert_eq!(
3475 serde_json::to_string(&ConnectionState::Connecting).unwrap(),
3476 "\"connecting\""
3477 );
3478 assert_eq!(
3479 serde_json::to_string(&ConnectionState::Connected).unwrap(),
3480 "\"connected\""
3481 );
3482 }
3483
3484 #[test]
3485 fn deserializes_runtime_attachment_variants() {
3486 let attachments: Vec<Attachment> = serde_json::from_value(json!([
3487 {
3488 "type": "file",
3489 "path": "/tmp/file.rs",
3490 "displayName": "file.rs",
3491 "lineRange": { "start": 7, "end": 12 }
3492 },
3493 {
3494 "type": "directory",
3495 "path": "/tmp/project",
3496 "displayName": "project"
3497 },
3498 {
3499 "type": "selection",
3500 "filePath": "/tmp/lib.rs",
3501 "displayName": "lib.rs",
3502 "text": "fn main() {}",
3503 "selection": {
3504 "start": { "line": 1, "character": 2 },
3505 "end": { "line": 3, "character": 4 }
3506 }
3507 },
3508 {
3509 "type": "blob",
3510 "data": "Zm9v",
3511 "mimeType": "image/png",
3512 "displayName": "image.png"
3513 },
3514 {
3515 "type": "github_reference",
3516 "number": 42,
3517 "title": "Fix rendering",
3518 "referenceType": "issue",
3519 "state": "open",
3520 "url": "https://github.com/example/repo/issues/42"
3521 }
3522 ]))
3523 .expect("attachments should deserialize");
3524
3525 assert_eq!(attachments.len(), 5);
3526 assert!(matches!(
3527 &attachments[0],
3528 Attachment::File {
3529 path,
3530 display_name,
3531 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
3532 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
3533 ));
3534 assert!(matches!(
3535 &attachments[1],
3536 Attachment::Directory { path, display_name }
3537 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
3538 ));
3539 assert!(matches!(
3540 &attachments[2],
3541 Attachment::Selection {
3542 file_path,
3543 display_name,
3544 selection:
3545 AttachmentSelectionRange {
3546 start: AttachmentSelectionPosition { line: 1, character: 2 },
3547 end: AttachmentSelectionPosition { line: 3, character: 4 },
3548 },
3549 ..
3550 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
3551 ));
3552 assert!(matches!(
3553 &attachments[3],
3554 Attachment::Blob {
3555 data,
3556 mime_type,
3557 display_name,
3558 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
3559 ));
3560 assert!(matches!(
3561 &attachments[4],
3562 Attachment::GitHubReference {
3563 number: 42,
3564 title,
3565 reference_type: GitHubReferenceType::Issue,
3566 state,
3567 url,
3568 } if title == "Fix rendering"
3569 && state == "open"
3570 && url == "https://github.com/example/repo/issues/42"
3571 ));
3572 }
3573
3574 #[test]
3575 fn ensures_display_names_for_variants_that_support_them() {
3576 let mut attachments = vec![
3577 Attachment::File {
3578 path: PathBuf::from("/tmp/file.rs"),
3579 display_name: None,
3580 line_range: None,
3581 },
3582 Attachment::Selection {
3583 file_path: PathBuf::from("/tmp/src/lib.rs"),
3584 display_name: None,
3585 text: "fn main() {}".to_string(),
3586 selection: AttachmentSelectionRange {
3587 start: AttachmentSelectionPosition {
3588 line: 0,
3589 character: 0,
3590 },
3591 end: AttachmentSelectionPosition {
3592 line: 0,
3593 character: 10,
3594 },
3595 },
3596 },
3597 Attachment::Blob {
3598 data: "Zm9v".to_string(),
3599 mime_type: "image/png".to_string(),
3600 display_name: None,
3601 },
3602 Attachment::GitHubReference {
3603 number: 7,
3604 title: "Track regressions".to_string(),
3605 reference_type: GitHubReferenceType::Issue,
3606 state: "open".to_string(),
3607 url: "https://example.com/issues/7".to_string(),
3608 },
3609 ];
3610
3611 ensure_attachment_display_names(&mut attachments);
3612
3613 assert_eq!(attachments[0].display_name(), Some("file.rs"));
3614 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
3615 assert_eq!(attachments[2].display_name(), Some("attachment"));
3616 assert_eq!(attachments[3].display_name(), None);
3617 assert_eq!(
3618 attachments[3].label(),
3619 Some("Track regressions".to_string())
3620 );
3621 }
3622}
3623
3624#[cfg(test)]
3625mod permission_builder_tests {
3626 use std::sync::Arc;
3627
3628 use crate::handler::{
3629 ApproveAllHandler, HandlerEvent, HandlerResponse, PermissionResult, SessionHandler,
3630 };
3631 use crate::types::{
3632 PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, SessionId,
3633 };
3634
3635 fn permission_event() -> HandlerEvent {
3636 HandlerEvent::PermissionRequest {
3637 session_id: SessionId::from("s1"),
3638 request_id: RequestId::new("1"),
3639 data: PermissionRequestData {
3640 extra: serde_json::json!({"tool": "shell"}),
3641 ..Default::default()
3642 },
3643 }
3644 }
3645
3646 async fn dispatch(handler: &Arc<dyn SessionHandler>) -> HandlerResponse {
3647 handler.on_event(permission_event()).await
3648 }
3649
3650 #[tokio::test]
3651 async fn session_config_approve_all_wraps_existing_handler() {
3652 let cfg = SessionConfig::default()
3653 .with_handler(Arc::new(ApproveAllHandler))
3654 .approve_all_permissions();
3655 let handler = cfg.handler.expect("handler should be set");
3656 match dispatch(&handler).await {
3657 HandlerResponse::Permission(PermissionResult::Approved) => {}
3658 other => panic!("expected Approved, got {other:?}"),
3659 }
3660 }
3661
3662 #[tokio::test]
3663 async fn session_config_approve_all_defaults_to_deny_inner() {
3664 let cfg = SessionConfig::default().approve_all_permissions();
3668 let handler = cfg.handler.expect("handler should be set");
3669 match dispatch(&handler).await {
3670 HandlerResponse::Permission(PermissionResult::Approved) => {}
3671 other => panic!("expected Approved, got {other:?}"),
3672 }
3673 }
3674
3675 #[tokio::test]
3676 async fn session_config_deny_all_denies() {
3677 let cfg = SessionConfig::default()
3678 .with_handler(Arc::new(ApproveAllHandler))
3679 .deny_all_permissions();
3680 let handler = cfg.handler.expect("handler should be set");
3681 match dispatch(&handler).await {
3682 HandlerResponse::Permission(PermissionResult::Denied) => {}
3683 other => panic!("expected Denied, got {other:?}"),
3684 }
3685 }
3686
3687 #[tokio::test]
3688 async fn session_config_approve_permissions_if_consults_predicate() {
3689 let cfg = SessionConfig::default()
3690 .with_handler(Arc::new(ApproveAllHandler))
3691 .approve_permissions_if(|data| {
3692 data.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
3693 });
3694 let handler = cfg.handler.expect("handler should be set");
3695 match dispatch(&handler).await {
3696 HandlerResponse::Permission(PermissionResult::Denied) => {}
3697 other => panic!("expected Denied for shell, got {other:?}"),
3698 }
3699 }
3700
3701 #[tokio::test]
3702 async fn resume_session_config_approve_all_wraps_existing_handler() {
3703 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
3704 .with_handler(Arc::new(ApproveAllHandler))
3705 .approve_all_permissions();
3706 let handler = cfg.handler.expect("handler should be set");
3707 match dispatch(&handler).await {
3708 HandlerResponse::Permission(PermissionResult::Approved) => {}
3709 other => panic!("expected Approved, got {other:?}"),
3710 }
3711 }
3712}