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")]
726 pub env: HashMap<String, String>,
727 #[serde(default, skip_serializing_if = "Option::is_none")]
729 pub cwd: Option<String>,
730}
731
732#[derive(Debug, Clone, Default, Serialize, Deserialize)]
736#[serde(rename_all = "camelCase")]
737pub struct McpHttpServerConfig {
738 #[serde(default)]
740 pub tools: Vec<String>,
741 #[serde(default, skip_serializing_if = "Option::is_none")]
743 pub timeout: Option<i64>,
744 pub url: String,
746 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
748 pub headers: HashMap<String, String>,
749}
750
751#[derive(Debug, Clone, Default, Serialize, Deserialize)]
757#[serde(rename_all = "camelCase")]
758#[non_exhaustive]
759pub struct ProviderConfig {
760 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
763 pub provider_type: Option<String>,
764 #[serde(default, skip_serializing_if = "Option::is_none")]
767 pub wire_api: Option<String>,
768 pub base_url: String,
770 #[serde(default, skip_serializing_if = "Option::is_none")]
772 pub api_key: Option<String>,
773 #[serde(default, skip_serializing_if = "Option::is_none")]
777 pub bearer_token: Option<String>,
778 #[serde(default, skip_serializing_if = "Option::is_none")]
780 pub azure: Option<AzureProviderOptions>,
781 #[serde(default, skip_serializing_if = "Option::is_none")]
783 pub headers: Option<HashMap<String, String>>,
784 #[serde(default, skip_serializing_if = "Option::is_none")]
788 pub model_id: Option<String>,
789 #[serde(default, skip_serializing_if = "Option::is_none")]
796 pub wire_model: Option<String>,
797 #[serde(default, skip_serializing_if = "Option::is_none")]
802 pub max_prompt_tokens: Option<i64>,
803 #[serde(default, skip_serializing_if = "Option::is_none")]
806 pub max_output_tokens: Option<i64>,
807}
808
809impl ProviderConfig {
810 pub fn new(base_url: impl Into<String>) -> Self {
813 Self {
814 base_url: base_url.into(),
815 ..Self::default()
816 }
817 }
818
819 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
821 self.provider_type = Some(provider_type.into());
822 self
823 }
824
825 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
827 self.wire_api = Some(wire_api.into());
828 self
829 }
830
831 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
833 self.api_key = Some(api_key.into());
834 self
835 }
836
837 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
840 self.bearer_token = Some(bearer_token.into());
841 self
842 }
843
844 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
846 self.azure = Some(azure);
847 self
848 }
849
850 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
852 self.headers = Some(headers);
853 self
854 }
855
856 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
859 self.model_id = Some(model_id.into());
860 self
861 }
862
863 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
868 self.wire_model = Some(wire_model.into());
869 self
870 }
871
872 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
876 self.max_prompt_tokens = Some(max);
877 self
878 }
879
880 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
883 self.max_output_tokens = Some(max);
884 self
885 }
886}
887
888#[derive(Debug, Clone, Default, Serialize, Deserialize)]
890#[serde(rename_all = "camelCase")]
891pub struct AzureProviderOptions {
892 #[serde(default, skip_serializing_if = "Option::is_none")]
894 pub api_version: Option<String>,
895}
896
897fn default_env_value_mode() -> String {
902 "direct".into()
903}
904
905#[derive(Clone, Serialize, Deserialize)]
952#[serde(rename_all = "camelCase")]
953#[non_exhaustive]
954pub struct SessionConfig {
955 #[serde(skip_serializing_if = "Option::is_none")]
957 pub session_id: Option<SessionId>,
958 #[serde(skip_serializing_if = "Option::is_none")]
960 pub model: Option<String>,
961 #[serde(skip_serializing_if = "Option::is_none")]
963 pub client_name: Option<String>,
964 #[serde(skip_serializing_if = "Option::is_none")]
966 pub reasoning_effort: Option<String>,
967 #[serde(skip_serializing_if = "Option::is_none")]
969 pub streaming: Option<bool>,
970 #[serde(skip_serializing_if = "Option::is_none")]
972 pub system_message: Option<SystemMessageConfig>,
973 #[serde(skip_serializing_if = "Option::is_none")]
975 pub tools: Option<Vec<Tool>>,
976 #[serde(skip_serializing_if = "Option::is_none")]
978 pub available_tools: Option<Vec<String>>,
979 #[serde(skip_serializing_if = "Option::is_none")]
981 pub excluded_tools: Option<Vec<String>>,
982 #[serde(skip_serializing_if = "Option::is_none")]
984 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
985 #[serde(default = "default_env_value_mode", skip_deserializing)]
989 pub(crate) env_value_mode: String,
990 #[serde(skip_serializing_if = "Option::is_none")]
992 pub enable_config_discovery: Option<bool>,
993 #[serde(skip_serializing_if = "Option::is_none")]
996 pub request_user_input: Option<bool>,
997 #[serde(skip_serializing_if = "Option::is_none")]
1002 pub request_permission: Option<bool>,
1003 #[serde(skip_serializing_if = "Option::is_none")]
1006 pub request_exit_plan_mode: Option<bool>,
1007 #[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")]
1034 pub hooks: Option<bool>,
1035 #[serde(skip_serializing_if = "Option::is_none")]
1037 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1038 #[serde(skip_serializing_if = "Option::is_none")]
1042 pub default_agent: Option<DefaultAgentConfig>,
1043 #[serde(skip_serializing_if = "Option::is_none")]
1046 pub agent: Option<String>,
1047 #[serde(skip_serializing_if = "Option::is_none")]
1050 pub infinite_sessions: Option<InfiniteSessionConfig>,
1051 #[serde(skip_serializing_if = "Option::is_none")]
1055 pub provider: Option<ProviderConfig>,
1056 #[serde(skip_serializing_if = "Option::is_none")]
1064 pub enable_session_telemetry: Option<bool>,
1065 #[serde(skip_serializing_if = "Option::is_none")]
1068 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1069 #[serde(skip_serializing_if = "Option::is_none")]
1072 pub config_dir: Option<PathBuf>,
1073 #[serde(skip_serializing_if = "Option::is_none")]
1076 pub working_directory: Option<PathBuf>,
1077 #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")]
1083 pub github_token: Option<String>,
1084 #[serde(skip_serializing_if = "Option::is_none")]
1088 pub include_sub_agent_streaming_events: Option<bool>,
1089 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
1093 pub commands: Option<Vec<CommandDefinition>>,
1094 #[serde(skip)]
1099 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1100 #[serde(skip)]
1105 pub handler: Option<Arc<dyn SessionHandler>>,
1106 #[serde(skip)]
1110 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1111 #[serde(skip)]
1116 pub transform: Option<Arc<dyn SystemMessageTransform>>,
1117}
1118
1119impl std::fmt::Debug for SessionConfig {
1120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1121 f.debug_struct("SessionConfig")
1122 .field("session_id", &self.session_id)
1123 .field("model", &self.model)
1124 .field("client_name", &self.client_name)
1125 .field("reasoning_effort", &self.reasoning_effort)
1126 .field("streaming", &self.streaming)
1127 .field("system_message", &self.system_message)
1128 .field("tools", &self.tools)
1129 .field("available_tools", &self.available_tools)
1130 .field("excluded_tools", &self.excluded_tools)
1131 .field("mcp_servers", &self.mcp_servers)
1132 .field("enable_config_discovery", &self.enable_config_discovery)
1133 .field("request_user_input", &self.request_user_input)
1134 .field("request_permission", &self.request_permission)
1135 .field("request_exit_plan_mode", &self.request_exit_plan_mode)
1136 .field("request_auto_mode_switch", &self.request_auto_mode_switch)
1137 .field("request_elicitation", &self.request_elicitation)
1138 .field("skill_directories", &self.skill_directories)
1139 .field("instruction_directories", &self.instruction_directories)
1140 .field("disabled_skills", &self.disabled_skills)
1141 .field("hooks", &self.hooks)
1142 .field("custom_agents", &self.custom_agents)
1143 .field("default_agent", &self.default_agent)
1144 .field("agent", &self.agent)
1145 .field("infinite_sessions", &self.infinite_sessions)
1146 .field("provider", &self.provider)
1147 .field("enable_session_telemetry", &self.enable_session_telemetry)
1148 .field("model_capabilities", &self.model_capabilities)
1149 .field("config_dir", &self.config_dir)
1150 .field("working_directory", &self.working_directory)
1151 .field(
1152 "github_token",
1153 &self.github_token.as_ref().map(|_| "<redacted>"),
1154 )
1155 .field(
1156 "include_sub_agent_streaming_events",
1157 &self.include_sub_agent_streaming_events,
1158 )
1159 .field("commands", &self.commands)
1160 .field(
1161 "session_fs_provider",
1162 &self.session_fs_provider.as_ref().map(|_| "<set>"),
1163 )
1164 .field("handler", &self.handler.as_ref().map(|_| "<set>"))
1165 .field(
1166 "hooks_handler",
1167 &self.hooks_handler.as_ref().map(|_| "<set>"),
1168 )
1169 .field("transform", &self.transform.as_ref().map(|_| "<set>"))
1170 .finish()
1171 }
1172}
1173
1174impl Default for SessionConfig {
1175 fn default() -> Self {
1182 Self {
1183 session_id: None,
1184 model: None,
1185 client_name: None,
1186 reasoning_effort: None,
1187 streaming: None,
1188 system_message: None,
1189 tools: None,
1190 available_tools: None,
1191 excluded_tools: None,
1192 mcp_servers: None,
1193 env_value_mode: default_env_value_mode(),
1194 enable_config_discovery: None,
1195 request_user_input: Some(true),
1196 request_permission: Some(true),
1197 request_exit_plan_mode: Some(true),
1198 request_auto_mode_switch: Some(true),
1199 request_elicitation: Some(true),
1200 skill_directories: None,
1201 instruction_directories: None,
1202 disabled_skills: None,
1203 hooks: None,
1204 custom_agents: None,
1205 default_agent: None,
1206 agent: None,
1207 infinite_sessions: None,
1208 provider: None,
1209 enable_session_telemetry: None,
1210 model_capabilities: None,
1211 config_dir: None,
1212 working_directory: None,
1213 github_token: None,
1214 include_sub_agent_streaming_events: None,
1215 commands: None,
1216 session_fs_provider: None,
1217 handler: None,
1218 hooks_handler: None,
1219 transform: None,
1220 }
1221 }
1222}
1223
1224impl SessionConfig {
1225 pub fn with_handler(mut self, handler: Arc<dyn SessionHandler>) -> Self {
1227 self.handler = Some(handler);
1228 self
1229 }
1230
1231 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
1236 self.commands = Some(commands);
1237 self
1238 }
1239
1240 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
1244 self.session_fs_provider = Some(provider);
1245 self
1246 }
1247
1248 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
1251 self.hooks_handler = Some(hooks);
1252 self
1253 }
1254
1255 pub fn with_transform(mut self, transform: Arc<dyn SystemMessageTransform>) -> Self {
1259 self.transform = Some(transform);
1260 self
1261 }
1262
1263 pub fn approve_all_permissions(mut self) -> Self {
1278 let inner = self
1279 .handler
1280 .take()
1281 .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1282 self.handler = Some(crate::permission::approve_all(inner));
1283 self
1284 }
1285
1286 pub fn deny_all_permissions(mut self) -> Self {
1290 let inner = self
1291 .handler
1292 .take()
1293 .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1294 self.handler = Some(crate::permission::deny_all(inner));
1295 self
1296 }
1297
1298 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
1304 where
1305 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
1306 {
1307 let inner = self
1308 .handler
1309 .take()
1310 .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1311 self.handler = Some(crate::permission::approve_if(inner, predicate));
1312 self
1313 }
1314
1315 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
1317 self.session_id = Some(id.into());
1318 self
1319 }
1320
1321 pub fn with_model(mut self, model: impl Into<String>) -> Self {
1323 self.model = Some(model.into());
1324 self
1325 }
1326
1327 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
1329 self.client_name = Some(name.into());
1330 self
1331 }
1332
1333 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
1335 self.reasoning_effort = Some(effort.into());
1336 self
1337 }
1338
1339 pub fn with_streaming(mut self, streaming: bool) -> Self {
1341 self.streaming = Some(streaming);
1342 self
1343 }
1344
1345 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
1347 self.system_message = Some(system_message);
1348 self
1349 }
1350
1351 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
1353 self.tools = Some(tools.into_iter().collect());
1354 self
1355 }
1356
1357 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
1359 where
1360 I: IntoIterator<Item = S>,
1361 S: Into<String>,
1362 {
1363 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
1364 self
1365 }
1366
1367 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
1369 where
1370 I: IntoIterator<Item = S>,
1371 S: Into<String>,
1372 {
1373 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
1374 self
1375 }
1376
1377 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
1379 self.mcp_servers = Some(servers);
1380 self
1381 }
1382
1383 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
1385 self.enable_config_discovery = Some(enable);
1386 self
1387 }
1388
1389 pub fn with_request_user_input(mut self, enable: bool) -> Self {
1391 self.request_user_input = Some(enable);
1392 self
1393 }
1394
1395 pub fn with_request_permission(mut self, enable: bool) -> Self {
1397 self.request_permission = Some(enable);
1398 self
1399 }
1400
1401 pub fn with_request_exit_plan_mode(mut self, enable: bool) -> Self {
1403 self.request_exit_plan_mode = Some(enable);
1404 self
1405 }
1406
1407 pub fn with_request_auto_mode_switch(mut self, enable: bool) -> Self {
1409 self.request_auto_mode_switch = Some(enable);
1410 self
1411 }
1412
1413 pub fn with_request_elicitation(mut self, enable: bool) -> Self {
1415 self.request_elicitation = Some(enable);
1416 self
1417 }
1418
1419 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
1421 where
1422 I: IntoIterator<Item = P>,
1423 P: Into<PathBuf>,
1424 {
1425 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
1426 self
1427 }
1428
1429 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
1433 where
1434 I: IntoIterator<Item = P>,
1435 P: Into<PathBuf>,
1436 {
1437 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
1438 self
1439 }
1440
1441 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
1443 where
1444 I: IntoIterator<Item = S>,
1445 S: Into<String>,
1446 {
1447 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
1448 self
1449 }
1450
1451 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
1453 mut self,
1454 agents: I,
1455 ) -> Self {
1456 self.custom_agents = Some(agents.into_iter().collect());
1457 self
1458 }
1459
1460 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
1462 self.default_agent = Some(agent);
1463 self
1464 }
1465
1466 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
1469 self.agent = Some(name.into());
1470 self
1471 }
1472
1473 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
1476 self.infinite_sessions = Some(config);
1477 self
1478 }
1479
1480 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
1482 self.provider = Some(provider);
1483 self
1484 }
1485
1486 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
1490 self.enable_session_telemetry = Some(enable);
1491 self
1492 }
1493
1494 pub fn with_model_capabilities(
1496 mut self,
1497 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1498 ) -> Self {
1499 self.model_capabilities = Some(capabilities);
1500 self
1501 }
1502
1503 pub fn with_config_dir(mut self, dir: impl Into<PathBuf>) -> Self {
1505 self.config_dir = Some(dir.into());
1506 self
1507 }
1508
1509 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
1512 self.working_directory = Some(dir.into());
1513 self
1514 }
1515
1516 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
1521 self.github_token = Some(token.into());
1522 self
1523 }
1524
1525 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
1528 self.include_sub_agent_streaming_events = Some(include);
1529 self
1530 }
1531}
1532
1533#[derive(Clone, Serialize, Deserialize)]
1539#[serde(rename_all = "camelCase")]
1540#[non_exhaustive]
1541pub struct ResumeSessionConfig {
1542 pub session_id: SessionId,
1544 #[serde(skip_serializing_if = "Option::is_none")]
1546 pub client_name: Option<String>,
1547 #[serde(skip_serializing_if = "Option::is_none")]
1549 pub reasoning_effort: Option<String>,
1550 #[serde(skip_serializing_if = "Option::is_none")]
1552 pub streaming: Option<bool>,
1553 #[serde(skip_serializing_if = "Option::is_none")]
1556 pub system_message: Option<SystemMessageConfig>,
1557 #[serde(skip_serializing_if = "Option::is_none")]
1559 pub tools: Option<Vec<Tool>>,
1560 #[serde(skip_serializing_if = "Option::is_none")]
1562 pub available_tools: Option<Vec<String>>,
1563 #[serde(skip_serializing_if = "Option::is_none")]
1565 pub excluded_tools: Option<Vec<String>>,
1566 #[serde(skip_serializing_if = "Option::is_none")]
1568 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
1569 #[serde(default = "default_env_value_mode", skip_deserializing)]
1571 pub(crate) env_value_mode: String,
1572 #[serde(skip_serializing_if = "Option::is_none")]
1574 pub enable_config_discovery: Option<bool>,
1575 #[serde(skip_serializing_if = "Option::is_none")]
1577 pub request_user_input: Option<bool>,
1578 #[serde(skip_serializing_if = "Option::is_none")]
1580 pub request_permission: Option<bool>,
1581 #[serde(skip_serializing_if = "Option::is_none")]
1583 pub request_exit_plan_mode: Option<bool>,
1584 #[serde(skip_serializing_if = "Option::is_none")]
1588 pub request_auto_mode_switch: Option<bool>,
1589 #[serde(skip_serializing_if = "Option::is_none")]
1591 pub request_elicitation: Option<bool>,
1592 #[serde(skip_serializing_if = "Option::is_none")]
1594 pub skill_directories: Option<Vec<PathBuf>>,
1595 #[serde(skip_serializing_if = "Option::is_none")]
1598 pub instruction_directories: Option<Vec<PathBuf>>,
1599 #[serde(skip_serializing_if = "Option::is_none")]
1601 pub disabled_skills: Option<Vec<String>>,
1602 #[serde(skip_serializing_if = "Option::is_none")]
1604 pub hooks: Option<bool>,
1605 #[serde(skip_serializing_if = "Option::is_none")]
1607 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1608 #[serde(skip_serializing_if = "Option::is_none")]
1610 pub default_agent: Option<DefaultAgentConfig>,
1611 #[serde(skip_serializing_if = "Option::is_none")]
1613 pub agent: Option<String>,
1614 #[serde(skip_serializing_if = "Option::is_none")]
1616 pub infinite_sessions: Option<InfiniteSessionConfig>,
1617 #[serde(skip_serializing_if = "Option::is_none")]
1619 pub provider: Option<ProviderConfig>,
1620 #[serde(skip_serializing_if = "Option::is_none")]
1628 pub enable_session_telemetry: Option<bool>,
1629 #[serde(skip_serializing_if = "Option::is_none")]
1631 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1632 #[serde(skip_serializing_if = "Option::is_none")]
1634 pub config_dir: Option<PathBuf>,
1635 #[serde(skip_serializing_if = "Option::is_none")]
1637 pub working_directory: Option<PathBuf>,
1638 #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")]
1641 pub github_token: Option<String>,
1642 #[serde(skip_serializing_if = "Option::is_none")]
1644 pub include_sub_agent_streaming_events: Option<bool>,
1645 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
1649 pub commands: Option<Vec<CommandDefinition>>,
1650 #[serde(skip)]
1655 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1656 #[serde(skip_serializing_if = "Option::is_none")]
1659 pub disable_resume: Option<bool>,
1660 #[serde(skip_serializing_if = "Option::is_none")]
1668 pub continue_pending_work: Option<bool>,
1669 #[serde(skip)]
1671 pub handler: Option<Arc<dyn SessionHandler>>,
1672 #[serde(skip)]
1674 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1675 #[serde(skip)]
1677 pub transform: Option<Arc<dyn SystemMessageTransform>>,
1678}
1679
1680impl std::fmt::Debug for ResumeSessionConfig {
1681 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1682 f.debug_struct("ResumeSessionConfig")
1683 .field("session_id", &self.session_id)
1684 .field("client_name", &self.client_name)
1685 .field("reasoning_effort", &self.reasoning_effort)
1686 .field("streaming", &self.streaming)
1687 .field("system_message", &self.system_message)
1688 .field("tools", &self.tools)
1689 .field("available_tools", &self.available_tools)
1690 .field("excluded_tools", &self.excluded_tools)
1691 .field("mcp_servers", &self.mcp_servers)
1692 .field("enable_config_discovery", &self.enable_config_discovery)
1693 .field("request_user_input", &self.request_user_input)
1694 .field("request_permission", &self.request_permission)
1695 .field("request_exit_plan_mode", &self.request_exit_plan_mode)
1696 .field("request_auto_mode_switch", &self.request_auto_mode_switch)
1697 .field("request_elicitation", &self.request_elicitation)
1698 .field("skill_directories", &self.skill_directories)
1699 .field("instruction_directories", &self.instruction_directories)
1700 .field("disabled_skills", &self.disabled_skills)
1701 .field("hooks", &self.hooks)
1702 .field("custom_agents", &self.custom_agents)
1703 .field("default_agent", &self.default_agent)
1704 .field("agent", &self.agent)
1705 .field("infinite_sessions", &self.infinite_sessions)
1706 .field("provider", &self.provider)
1707 .field("enable_session_telemetry", &self.enable_session_telemetry)
1708 .field("model_capabilities", &self.model_capabilities)
1709 .field("config_dir", &self.config_dir)
1710 .field("working_directory", &self.working_directory)
1711 .field(
1712 "github_token",
1713 &self.github_token.as_ref().map(|_| "<redacted>"),
1714 )
1715 .field(
1716 "include_sub_agent_streaming_events",
1717 &self.include_sub_agent_streaming_events,
1718 )
1719 .field("commands", &self.commands)
1720 .field(
1721 "session_fs_provider",
1722 &self.session_fs_provider.as_ref().map(|_| "<set>"),
1723 )
1724 .field("handler", &self.handler.as_ref().map(|_| "<set>"))
1725 .field(
1726 "hooks_handler",
1727 &self.hooks_handler.as_ref().map(|_| "<set>"),
1728 )
1729 .field("transform", &self.transform.as_ref().map(|_| "<set>"))
1730 .field("disable_resume", &self.disable_resume)
1731 .field("continue_pending_work", &self.continue_pending_work)
1732 .finish()
1733 }
1734}
1735
1736impl ResumeSessionConfig {
1737 pub fn new(session_id: SessionId) -> Self {
1742 Self {
1743 session_id,
1744 client_name: None,
1745 reasoning_effort: None,
1746 streaming: None,
1747 system_message: None,
1748 tools: None,
1749 available_tools: None,
1750 excluded_tools: None,
1751 mcp_servers: None,
1752 env_value_mode: default_env_value_mode(),
1753 enable_config_discovery: None,
1754 request_user_input: Some(true),
1755 request_permission: Some(true),
1756 request_exit_plan_mode: Some(true),
1757 request_auto_mode_switch: Some(true),
1758 request_elicitation: Some(true),
1759 skill_directories: None,
1760 instruction_directories: None,
1761 disabled_skills: None,
1762 hooks: None,
1763 custom_agents: None,
1764 default_agent: None,
1765 agent: None,
1766 infinite_sessions: None,
1767 provider: None,
1768 enable_session_telemetry: None,
1769 model_capabilities: None,
1770 config_dir: None,
1771 working_directory: None,
1772 github_token: None,
1773 include_sub_agent_streaming_events: None,
1774 commands: None,
1775 session_fs_provider: None,
1776 disable_resume: None,
1777 continue_pending_work: None,
1778 handler: None,
1779 hooks_handler: None,
1780 transform: None,
1781 }
1782 }
1783
1784 pub fn with_handler(mut self, handler: Arc<dyn SessionHandler>) -> Self {
1786 self.handler = Some(handler);
1787 self
1788 }
1789
1790 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
1793 self.hooks_handler = Some(hooks);
1794 self
1795 }
1796
1797 pub fn with_transform(mut self, transform: Arc<dyn SystemMessageTransform>) -> Self {
1799 self.transform = Some(transform);
1800 self
1801 }
1802
1803 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
1807 self.commands = Some(commands);
1808 self
1809 }
1810
1811 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
1814 self.session_fs_provider = Some(provider);
1815 self
1816 }
1817
1818 pub fn approve_all_permissions(mut self) -> Self {
1822 let inner = self
1823 .handler
1824 .take()
1825 .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1826 self.handler = Some(crate::permission::approve_all(inner));
1827 self
1828 }
1829
1830 pub fn deny_all_permissions(mut self) -> Self {
1834 let inner = self
1835 .handler
1836 .take()
1837 .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1838 self.handler = Some(crate::permission::deny_all(inner));
1839 self
1840 }
1841
1842 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
1845 where
1846 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
1847 {
1848 let inner = self
1849 .handler
1850 .take()
1851 .unwrap_or_else(|| Arc::new(crate::handler::DenyAllHandler));
1852 self.handler = Some(crate::permission::approve_if(inner, predicate));
1853 self
1854 }
1855
1856 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
1858 self.client_name = Some(name.into());
1859 self
1860 }
1861
1862 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
1864 self.reasoning_effort = Some(effort.into());
1865 self
1866 }
1867
1868 pub fn with_streaming(mut self, streaming: bool) -> Self {
1870 self.streaming = Some(streaming);
1871 self
1872 }
1873
1874 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
1877 self.system_message = Some(system_message);
1878 self
1879 }
1880
1881 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
1883 self.tools = Some(tools.into_iter().collect());
1884 self
1885 }
1886
1887 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
1889 where
1890 I: IntoIterator<Item = S>,
1891 S: Into<String>,
1892 {
1893 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
1894 self
1895 }
1896
1897 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
1899 where
1900 I: IntoIterator<Item = S>,
1901 S: Into<String>,
1902 {
1903 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
1904 self
1905 }
1906
1907 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
1909 self.mcp_servers = Some(servers);
1910 self
1911 }
1912
1913 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
1915 self.enable_config_discovery = Some(enable);
1916 self
1917 }
1918
1919 pub fn with_request_user_input(mut self, enable: bool) -> Self {
1921 self.request_user_input = Some(enable);
1922 self
1923 }
1924
1925 pub fn with_request_permission(mut self, enable: bool) -> Self {
1927 self.request_permission = Some(enable);
1928 self
1929 }
1930
1931 pub fn with_request_exit_plan_mode(mut self, enable: bool) -> Self {
1933 self.request_exit_plan_mode = Some(enable);
1934 self
1935 }
1936
1937 pub fn with_request_auto_mode_switch(mut self, enable: bool) -> Self {
1939 self.request_auto_mode_switch = Some(enable);
1940 self
1941 }
1942
1943 pub fn with_request_elicitation(mut self, enable: bool) -> Self {
1945 self.request_elicitation = Some(enable);
1946 self
1947 }
1948
1949 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
1951 where
1952 I: IntoIterator<Item = P>,
1953 P: Into<PathBuf>,
1954 {
1955 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
1956 self
1957 }
1958
1959 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
1963 where
1964 I: IntoIterator<Item = P>,
1965 P: Into<PathBuf>,
1966 {
1967 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
1968 self
1969 }
1970
1971 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
1973 where
1974 I: IntoIterator<Item = S>,
1975 S: Into<String>,
1976 {
1977 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
1978 self
1979 }
1980
1981 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
1983 mut self,
1984 agents: I,
1985 ) -> Self {
1986 self.custom_agents = Some(agents.into_iter().collect());
1987 self
1988 }
1989
1990 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
1992 self.default_agent = Some(agent);
1993 self
1994 }
1995
1996 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
1998 self.agent = Some(name.into());
1999 self
2000 }
2001
2002 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2004 self.infinite_sessions = Some(config);
2005 self
2006 }
2007
2008 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2010 self.provider = Some(provider);
2011 self
2012 }
2013
2014 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2018 self.enable_session_telemetry = Some(enable);
2019 self
2020 }
2021
2022 pub fn with_model_capabilities(
2024 mut self,
2025 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2026 ) -> Self {
2027 self.model_capabilities = Some(capabilities);
2028 self
2029 }
2030
2031 pub fn with_config_dir(mut self, dir: impl Into<PathBuf>) -> Self {
2033 self.config_dir = Some(dir.into());
2034 self
2035 }
2036
2037 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2039 self.working_directory = Some(dir.into());
2040 self
2041 }
2042
2043 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2047 self.github_token = Some(token.into());
2048 self
2049 }
2050
2051 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2053 self.include_sub_agent_streaming_events = Some(include);
2054 self
2055 }
2056
2057 pub fn with_disable_resume(mut self, disable: bool) -> Self {
2060 self.disable_resume = Some(disable);
2061 self
2062 }
2063
2064 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
2070 self.continue_pending_work = Some(continue_pending);
2071 self
2072 }
2073}
2074
2075#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2081#[serde(rename_all = "camelCase")]
2082#[non_exhaustive]
2083pub struct SystemMessageConfig {
2084 #[serde(skip_serializing_if = "Option::is_none")]
2086 pub mode: Option<String>,
2087 #[serde(skip_serializing_if = "Option::is_none")]
2089 pub content: Option<String>,
2090 #[serde(skip_serializing_if = "Option::is_none")]
2092 pub sections: Option<HashMap<String, SectionOverride>>,
2093}
2094
2095impl SystemMessageConfig {
2096 pub fn new() -> Self {
2099 Self::default()
2100 }
2101
2102 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
2105 self.mode = Some(mode.into());
2106 self
2107 }
2108
2109 pub fn with_content(mut self, content: impl Into<String>) -> Self {
2112 self.content = Some(content.into());
2113 self
2114 }
2115
2116 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
2118 self.sections = Some(sections);
2119 self
2120 }
2121}
2122
2123#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2129#[serde(rename_all = "camelCase")]
2130pub struct SectionOverride {
2131 #[serde(skip_serializing_if = "Option::is_none")]
2133 pub action: Option<String>,
2134 #[serde(skip_serializing_if = "Option::is_none")]
2136 pub content: Option<String>,
2137}
2138
2139#[derive(Debug, Clone, Serialize, Deserialize)]
2141#[serde(rename_all = "camelCase")]
2142pub struct CreateSessionResult {
2143 pub session_id: SessionId,
2145 #[serde(skip_serializing_if = "Option::is_none")]
2147 pub workspace_path: Option<PathBuf>,
2148 #[serde(default, alias = "remote_url")]
2150 pub remote_url: Option<String>,
2151 #[serde(skip_serializing_if = "Option::is_none")]
2153 pub capabilities: Option<SessionCapabilities>,
2154}
2155
2156#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
2158#[serde(rename_all = "lowercase")]
2159pub enum LogLevel {
2160 #[default]
2162 Info,
2163 Warning,
2165 Error,
2167}
2168
2169#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
2174#[serde(rename_all = "camelCase")]
2175pub struct LogOptions {
2176 #[serde(skip_serializing_if = "Option::is_none")]
2178 pub level: Option<LogLevel>,
2179 #[serde(skip_serializing_if = "Option::is_none")]
2182 pub ephemeral: Option<bool>,
2183}
2184
2185impl LogOptions {
2186 pub fn with_level(mut self, level: LogLevel) -> Self {
2188 self.level = Some(level);
2189 self
2190 }
2191
2192 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
2194 self.ephemeral = Some(ephemeral);
2195 self
2196 }
2197}
2198
2199#[derive(Debug, Clone, Default)]
2203pub struct SetModelOptions {
2204 pub reasoning_effort: Option<String>,
2207 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2211}
2212
2213impl SetModelOptions {
2214 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2216 self.reasoning_effort = Some(effort.into());
2217 self
2218 }
2219
2220 pub fn with_model_capabilities(
2222 mut self,
2223 caps: crate::generated::api_types::ModelCapabilitiesOverride,
2224 ) -> Self {
2225 self.model_capabilities = Some(caps);
2226 self
2227 }
2228}
2229
2230#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2237#[serde(rename_all = "camelCase")]
2238pub struct PingResponse {
2239 #[serde(default)]
2241 pub message: String,
2242 #[serde(default)]
2244 pub timestamp: i64,
2245 #[serde(skip_serializing_if = "Option::is_none")]
2247 pub protocol_version: Option<u32>,
2248}
2249
2250#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2252#[serde(rename_all = "camelCase")]
2253pub struct AttachmentLineRange {
2254 pub start: u32,
2256 pub end: u32,
2258}
2259
2260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2262#[serde(rename_all = "camelCase")]
2263pub struct AttachmentSelectionPosition {
2264 pub line: u32,
2266 pub character: u32,
2268}
2269
2270#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2272#[serde(rename_all = "camelCase")]
2273pub struct AttachmentSelectionRange {
2274 pub start: AttachmentSelectionPosition,
2276 pub end: AttachmentSelectionPosition,
2278}
2279
2280#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2282#[serde(rename_all = "snake_case")]
2283#[non_exhaustive]
2284pub enum GitHubReferenceType {
2285 Issue,
2287 Pr,
2289 Discussion,
2291}
2292
2293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2295#[serde(
2296 tag = "type",
2297 rename_all = "camelCase",
2298 rename_all_fields = "camelCase"
2299)]
2300#[non_exhaustive]
2301pub enum Attachment {
2302 File {
2304 path: PathBuf,
2306 #[serde(skip_serializing_if = "Option::is_none")]
2308 display_name: Option<String>,
2309 #[serde(skip_serializing_if = "Option::is_none")]
2311 line_range: Option<AttachmentLineRange>,
2312 },
2313 Directory {
2315 path: PathBuf,
2317 #[serde(skip_serializing_if = "Option::is_none")]
2319 display_name: Option<String>,
2320 },
2321 Selection {
2323 file_path: PathBuf,
2325 text: String,
2327 #[serde(skip_serializing_if = "Option::is_none")]
2329 display_name: Option<String>,
2330 selection: AttachmentSelectionRange,
2332 },
2333 Blob {
2335 data: String,
2337 mime_type: String,
2339 #[serde(skip_serializing_if = "Option::is_none")]
2341 display_name: Option<String>,
2342 },
2343 #[serde(rename = "github_reference")]
2345 GitHubReference {
2346 number: u64,
2348 title: String,
2350 reference_type: GitHubReferenceType,
2352 state: String,
2354 url: String,
2356 },
2357}
2358
2359impl Attachment {
2360 pub fn display_name(&self) -> Option<&str> {
2362 match self {
2363 Self::File { display_name, .. }
2364 | Self::Directory { display_name, .. }
2365 | Self::Selection { display_name, .. }
2366 | Self::Blob { display_name, .. } => display_name.as_deref(),
2367 Self::GitHubReference { .. } => None,
2368 }
2369 }
2370
2371 pub fn label(&self) -> Option<String> {
2373 if let Some(display_name) = self
2374 .display_name()
2375 .map(str::trim)
2376 .filter(|name| !name.is_empty())
2377 {
2378 return Some(display_name.to_string());
2379 }
2380
2381 match self {
2382 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
2383 format!("#{}", number)
2384 } else {
2385 title.trim().to_string()
2386 }),
2387 _ => self.derived_display_name(),
2388 }
2389 }
2390
2391 pub fn ensure_display_name(&mut self) {
2393 if self
2394 .display_name()
2395 .map(str::trim)
2396 .is_some_and(|name| !name.is_empty())
2397 {
2398 return;
2399 }
2400
2401 let Some(derived_display_name) = self.derived_display_name() else {
2402 return;
2403 };
2404
2405 match self {
2406 Self::File { display_name, .. }
2407 | Self::Directory { display_name, .. }
2408 | Self::Selection { display_name, .. }
2409 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
2410 Self::GitHubReference { .. } => {}
2411 }
2412 }
2413
2414 fn derived_display_name(&self) -> Option<String> {
2415 match self {
2416 Self::File { path, .. } | Self::Directory { path, .. } => {
2417 Some(attachment_name_from_path(path))
2418 }
2419 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
2420 Self::Blob { .. } => Some("attachment".to_string()),
2421 Self::GitHubReference { .. } => None,
2422 }
2423 }
2424}
2425
2426fn attachment_name_from_path(path: &Path) -> String {
2427 path.file_name()
2428 .map(|name| name.to_string_lossy().into_owned())
2429 .filter(|name| !name.is_empty())
2430 .unwrap_or_else(|| {
2431 let full = path.to_string_lossy();
2432 if full.is_empty() {
2433 "attachment".to_string()
2434 } else {
2435 full.into_owned()
2436 }
2437 })
2438}
2439
2440pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
2442 for attachment in attachments {
2443 attachment.ensure_display_name();
2444 }
2445}
2446
2447#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2452#[serde(rename_all = "lowercase")]
2453#[non_exhaustive]
2454pub enum DeliveryMode {
2455 Enqueue,
2457 Immediate,
2459}
2460
2461#[derive(Debug, Clone)]
2490#[non_exhaustive]
2491pub struct MessageOptions {
2492 pub prompt: String,
2494 pub mode: Option<DeliveryMode>,
2500 pub attachments: Option<Vec<Attachment>>,
2502 pub wait_timeout: Option<Duration>,
2505 pub request_headers: Option<HashMap<String, String>>,
2509 pub traceparent: Option<String>,
2516 pub tracestate: Option<String>,
2520}
2521
2522impl MessageOptions {
2523 pub fn new(prompt: impl Into<String>) -> Self {
2525 Self {
2526 prompt: prompt.into(),
2527 mode: None,
2528 attachments: None,
2529 wait_timeout: None,
2530 request_headers: None,
2531 traceparent: None,
2532 tracestate: None,
2533 }
2534 }
2535
2536 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
2542 self.mode = Some(mode);
2543 self
2544 }
2545
2546 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
2548 self.attachments = Some(attachments);
2549 self
2550 }
2551
2552 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
2554 self.wait_timeout = Some(timeout);
2555 self
2556 }
2557
2558 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
2560 self.request_headers = Some(headers);
2561 self
2562 }
2563
2564 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
2569 self.traceparent = ctx.traceparent;
2570 self.tracestate = ctx.tracestate;
2571 self
2572 }
2573
2574 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
2576 self.traceparent = Some(traceparent.into());
2577 self
2578 }
2579
2580 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
2582 self.tracestate = Some(tracestate.into());
2583 self
2584 }
2585}
2586
2587impl From<&str> for MessageOptions {
2588 fn from(prompt: &str) -> Self {
2589 Self::new(prompt)
2590 }
2591}
2592
2593impl From<String> for MessageOptions {
2594 fn from(prompt: String) -> Self {
2595 Self::new(prompt)
2596 }
2597}
2598
2599impl From<&String> for MessageOptions {
2600 fn from(prompt: &String) -> Self {
2601 Self::new(prompt.clone())
2602 }
2603}
2604
2605#[derive(Debug, Clone, Serialize, Deserialize)]
2607#[serde(rename_all = "camelCase")]
2608#[non_exhaustive]
2609pub struct GetStatusResponse {
2610 pub version: String,
2612 pub protocol_version: u32,
2614}
2615
2616#[derive(Debug, Clone, Serialize, Deserialize)]
2618#[serde(rename_all = "camelCase")]
2619#[non_exhaustive]
2620pub struct GetAuthStatusResponse {
2621 pub is_authenticated: bool,
2623 #[serde(skip_serializing_if = "Option::is_none")]
2626 pub auth_type: Option<String>,
2627 #[serde(skip_serializing_if = "Option::is_none")]
2629 pub host: Option<String>,
2630 #[serde(skip_serializing_if = "Option::is_none")]
2632 pub login: Option<String>,
2633 #[serde(skip_serializing_if = "Option::is_none")]
2635 pub status_message: Option<String>,
2636}
2637
2638#[derive(Debug, Clone, Serialize, Deserialize)]
2642#[serde(rename_all = "camelCase")]
2643pub struct SessionEventNotification {
2644 pub session_id: SessionId,
2646 pub event: SessionEvent,
2648}
2649
2650#[derive(Debug, Clone, Serialize, Deserialize)]
2657#[serde(rename_all = "camelCase")]
2658pub struct SessionEvent {
2659 pub id: String,
2661 pub timestamp: String,
2663 pub parent_id: Option<String>,
2665 #[serde(skip_serializing_if = "Option::is_none")]
2667 pub ephemeral: Option<bool>,
2668 #[serde(skip_serializing_if = "Option::is_none")]
2671 pub agent_id: Option<String>,
2672 #[serde(skip_serializing_if = "Option::is_none")]
2674 pub debug_cli_received_at_ms: Option<i64>,
2675 #[serde(skip_serializing_if = "Option::is_none")]
2677 pub debug_ws_forwarded_at_ms: Option<i64>,
2678 #[serde(rename = "type")]
2680 pub event_type: String,
2681 pub data: Value,
2683}
2684
2685impl SessionEvent {
2686 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
2691 use serde::de::IntoDeserializer;
2692 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
2693 self.event_type.as_str().into_deserializer();
2694 crate::generated::SessionEventType::deserialize(deserializer)
2695 .unwrap_or(crate::generated::SessionEventType::Unknown)
2696 }
2697
2698 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
2704 serde_json::from_value(self.data.clone()).ok()
2705 }
2706
2707 pub fn is_transient_error(&self) -> bool {
2711 self.event_type == "session.error"
2712 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
2713 }
2714}
2715
2716#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2721#[serde(rename_all = "camelCase")]
2722#[non_exhaustive]
2723pub struct ToolInvocation {
2724 pub session_id: SessionId,
2726 pub tool_call_id: String,
2728 pub tool_name: String,
2730 pub arguments: Value,
2732 #[serde(default, skip_serializing_if = "Option::is_none")]
2737 pub traceparent: Option<String>,
2738 #[serde(default, skip_serializing_if = "Option::is_none")]
2741 pub tracestate: Option<String>,
2742}
2743
2744impl ToolInvocation {
2745 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
2766 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
2767 }
2768
2769 pub fn trace_context(&self) -> TraceContext {
2772 TraceContext {
2773 traceparent: self.traceparent.clone(),
2774 tracestate: self.tracestate.clone(),
2775 }
2776 }
2777}
2778
2779#[derive(Debug, Clone, Serialize, Deserialize)]
2781#[serde(rename_all = "camelCase")]
2782pub struct ToolBinaryResult {
2783 pub data: String,
2785 pub mime_type: String,
2787 pub r#type: String,
2789 #[serde(default, skip_serializing_if = "Option::is_none")]
2791 pub description: Option<String>,
2792}
2793
2794#[derive(Debug, Clone, Serialize, Deserialize)]
2796#[serde(rename_all = "camelCase")]
2797pub struct ToolResultExpanded {
2798 pub text_result_for_llm: String,
2800 pub result_type: String,
2802 #[serde(default, skip_serializing_if = "Option::is_none")]
2804 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
2805 #[serde(skip_serializing_if = "Option::is_none")]
2807 pub session_log: Option<String>,
2808 #[serde(skip_serializing_if = "Option::is_none")]
2810 pub error: Option<String>,
2811 #[serde(default, skip_serializing_if = "Option::is_none")]
2813 pub tool_telemetry: Option<HashMap<String, Value>>,
2814}
2815
2816#[derive(Debug, Clone, Serialize, Deserialize)]
2818#[serde(untagged)]
2819#[non_exhaustive]
2820pub enum ToolResult {
2821 Text(String),
2823 Expanded(ToolResultExpanded),
2825}
2826
2827#[derive(Debug, Clone, Serialize, Deserialize)]
2829#[serde(rename_all = "camelCase")]
2830pub struct ToolResultResponse {
2831 pub result: ToolResult,
2833}
2834
2835#[derive(Debug, Clone, Serialize, Deserialize)]
2837#[serde(rename_all = "camelCase")]
2838pub struct SessionMetadata {
2839 pub session_id: SessionId,
2841 pub start_time: String,
2843 pub modified_time: String,
2845 #[serde(skip_serializing_if = "Option::is_none")]
2847 pub summary: Option<String>,
2848 pub is_remote: bool,
2850}
2851
2852#[derive(Debug, Clone, Serialize, Deserialize)]
2854#[serde(rename_all = "camelCase")]
2855pub struct ListSessionsResponse {
2856 pub sessions: Vec<SessionMetadata>,
2858}
2859
2860#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2864#[serde(rename_all = "camelCase")]
2865pub struct SessionListFilter {
2866 #[serde(default, skip_serializing_if = "Option::is_none")]
2868 pub cwd: Option<String>,
2869 #[serde(default, skip_serializing_if = "Option::is_none")]
2871 pub git_root: Option<String>,
2872 #[serde(default, skip_serializing_if = "Option::is_none")]
2874 pub repository: Option<String>,
2875 #[serde(default, skip_serializing_if = "Option::is_none")]
2877 pub branch: Option<String>,
2878}
2879
2880#[derive(Debug, Clone, Serialize, Deserialize)]
2882#[serde(rename_all = "camelCase")]
2883pub struct GetSessionMetadataResponse {
2884 #[serde(skip_serializing_if = "Option::is_none")]
2886 pub session: Option<SessionMetadata>,
2887}
2888
2889#[derive(Debug, Clone, Serialize, Deserialize)]
2891#[serde(rename_all = "camelCase")]
2892pub struct GetLastSessionIdResponse {
2893 #[serde(skip_serializing_if = "Option::is_none")]
2895 pub session_id: Option<SessionId>,
2896}
2897
2898#[derive(Debug, Clone, Serialize, Deserialize)]
2900#[serde(rename_all = "camelCase")]
2901pub struct GetForegroundSessionResponse {
2902 #[serde(skip_serializing_if = "Option::is_none")]
2904 pub session_id: Option<SessionId>,
2905}
2906
2907#[derive(Debug, Clone, Serialize, Deserialize)]
2909#[serde(rename_all = "camelCase")]
2910pub struct GetMessagesResponse {
2911 pub events: Vec<SessionEvent>,
2913}
2914
2915#[derive(Debug, Clone, Serialize, Deserialize)]
2917#[serde(rename_all = "camelCase")]
2918pub struct ElicitationResult {
2919 pub action: String,
2921 #[serde(skip_serializing_if = "Option::is_none")]
2923 pub content: Option<Value>,
2924}
2925
2926#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2932#[serde(rename_all = "camelCase")]
2933#[non_exhaustive]
2934pub enum ElicitationMode {
2935 Form,
2937 Url,
2939 #[serde(other)]
2941 Unknown,
2942}
2943
2944#[derive(Debug, Clone, Serialize, Deserialize)]
2950#[serde(rename_all = "camelCase")]
2951pub struct ElicitationRequest {
2952 pub message: String,
2954 #[serde(skip_serializing_if = "Option::is_none")]
2956 pub requested_schema: Option<Value>,
2957 #[serde(skip_serializing_if = "Option::is_none")]
2959 pub mode: Option<ElicitationMode>,
2960 #[serde(skip_serializing_if = "Option::is_none")]
2962 pub elicitation_source: Option<String>,
2963 #[serde(skip_serializing_if = "Option::is_none")]
2965 pub url: Option<String>,
2966}
2967
2968#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2973#[serde(rename_all = "camelCase")]
2974pub struct SessionCapabilities {
2975 #[serde(skip_serializing_if = "Option::is_none")]
2977 pub ui: Option<UiCapabilities>,
2978}
2979
2980#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2982#[serde(rename_all = "camelCase")]
2983pub struct UiCapabilities {
2984 #[serde(skip_serializing_if = "Option::is_none")]
2986 pub elicitation: Option<bool>,
2987}
2988
2989#[derive(Debug, Clone, Default)]
2991pub struct InputOptions<'a> {
2992 pub title: Option<&'a str>,
2994 pub description: Option<&'a str>,
2996 pub min_length: Option<u64>,
2998 pub max_length: Option<u64>,
3000 pub format: Option<InputFormat>,
3002 pub default: Option<&'a str>,
3004}
3005
3006#[derive(Debug, Clone, Copy)]
3008#[non_exhaustive]
3009pub enum InputFormat {
3010 Email,
3012 Uri,
3014 Date,
3016 DateTime,
3018}
3019
3020impl InputFormat {
3021 pub fn as_str(&self) -> &'static str {
3023 match self {
3024 Self::Email => "email",
3025 Self::Uri => "uri",
3026 Self::Date => "date",
3027 Self::DateTime => "date-time",
3028 }
3029 }
3030}
3031
3032pub use crate::generated::api_types::{
3037 Model, ModelBilling, ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
3038 ModelCapabilitiesSupports, ModelList, ModelPolicy,
3039};
3040
3041#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3047#[serde(rename_all = "kebab-case")]
3048#[non_exhaustive]
3049pub enum PermissionRequestKind {
3050 Shell,
3052 Write,
3054 Read,
3056 Url,
3058 Mcp,
3060 CustomTool,
3062 Memory,
3064 Hook,
3066 #[serde(other)]
3069 Unknown,
3070}
3071
3072#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3079#[serde(rename_all = "camelCase")]
3080pub struct PermissionRequestData {
3081 #[serde(default, skip_serializing_if = "Option::is_none")]
3085 pub kind: Option<PermissionRequestKind>,
3086 #[serde(default, skip_serializing_if = "Option::is_none")]
3089 pub tool_call_id: Option<String>,
3090 #[serde(flatten)]
3093 pub extra: Value,
3094}
3095
3096#[derive(Debug, Clone, Serialize, Deserialize)]
3098#[serde(rename_all = "camelCase")]
3099pub struct ExitPlanModeData {
3100 #[serde(default)]
3102 pub summary: String,
3103 #[serde(default, skip_serializing_if = "Option::is_none")]
3105 pub plan_content: Option<String>,
3106 #[serde(default)]
3108 pub actions: Vec<String>,
3109 #[serde(default = "default_recommended_action")]
3111 pub recommended_action: String,
3112}
3113
3114fn default_recommended_action() -> String {
3115 "autopilot".to_string()
3116}
3117
3118impl Default for ExitPlanModeData {
3119 fn default() -> Self {
3120 Self {
3121 summary: String::new(),
3122 plan_content: None,
3123 actions: Vec::new(),
3124 recommended_action: default_recommended_action(),
3125 }
3126 }
3127}
3128
3129#[cfg(test)]
3130mod tests {
3131 use std::path::PathBuf;
3132
3133 use serde_json::json;
3134
3135 use super::{
3136 Attachment, AttachmentLineRange, AttachmentSelectionPosition, AttachmentSelectionRange,
3137 ConnectionState, CustomAgentConfig, DeliveryMode, GitHubReferenceType,
3138 InfiniteSessionConfig, ProviderConfig, ResumeSessionConfig, SessionConfig, SessionEvent,
3139 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
3140 ToolResultResponse, ensure_attachment_display_names,
3141 };
3142 use crate::generated::session_events::TypedSessionEvent;
3143
3144 #[test]
3145 fn tool_builder_composes() {
3146 let tool = Tool::new("greet")
3147 .with_description("Say hello")
3148 .with_namespaced_name("hello/greet")
3149 .with_instructions("Pass the user's name")
3150 .with_parameters(json!({
3151 "type": "object",
3152 "properties": { "name": { "type": "string" } },
3153 "required": ["name"]
3154 }))
3155 .with_overrides_built_in_tool(true)
3156 .with_skip_permission(true);
3157 assert_eq!(tool.name, "greet");
3158 assert_eq!(tool.description, "Say hello");
3159 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
3160 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
3161 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
3162 assert!(tool.overrides_built_in_tool);
3163 assert!(tool.skip_permission);
3164 }
3165
3166 #[test]
3167 fn tool_with_parameters_handles_non_object_value() {
3168 let tool = Tool::new("noop").with_parameters(json!(null));
3169 assert!(tool.parameters.is_empty());
3170 }
3171
3172 #[test]
3173 fn tool_result_expanded_serializes_binary_results_for_llm() {
3174 let response = ToolResultResponse {
3175 result: ToolResult::Expanded(ToolResultExpanded {
3176 text_result_for_llm: "rendered chart".to_string(),
3177 result_type: "success".to_string(),
3178 binary_results_for_llm: Some(vec![ToolBinaryResult {
3179 data: "aW1n".to_string(),
3180 mime_type: "image/png".to_string(),
3181 r#type: "image".to_string(),
3182 description: Some("chart preview".to_string()),
3183 }]),
3184 session_log: None,
3185 error: None,
3186 tool_telemetry: None,
3187 }),
3188 };
3189
3190 let wire = serde_json::to_value(&response).unwrap();
3191
3192 assert_eq!(
3193 wire,
3194 json!({
3195 "result": {
3196 "textResultForLlm": "rendered chart",
3197 "resultType": "success",
3198 "binaryResultsForLlm": [
3199 {
3200 "data": "aW1n",
3201 "mimeType": "image/png",
3202 "type": "image",
3203 "description": "chart preview"
3204 }
3205 ]
3206 }
3207 })
3208 );
3209 }
3210
3211 #[test]
3212 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
3213 let response = ToolResultResponse {
3214 result: ToolResult::Expanded(ToolResultExpanded {
3215 text_result_for_llm: "ok".to_string(),
3216 result_type: "success".to_string(),
3217 binary_results_for_llm: None,
3218 session_log: None,
3219 error: None,
3220 tool_telemetry: None,
3221 }),
3222 };
3223
3224 let wire = serde_json::to_value(&response).unwrap();
3225
3226 assert_eq!(wire["result"]["textResultForLlm"], "ok");
3227 assert!(wire["result"].get("binaryResultsForLlm").is_none());
3228 }
3229
3230 #[test]
3231 fn session_config_default_enables_permission_flow_flags() {
3232 let cfg = SessionConfig::default();
3233 assert_eq!(cfg.request_user_input, Some(true));
3234 assert_eq!(cfg.request_permission, Some(true));
3235 assert_eq!(cfg.request_elicitation, Some(true));
3236 assert_eq!(cfg.request_exit_plan_mode, Some(true));
3237 assert_eq!(cfg.request_auto_mode_switch, Some(true));
3238 }
3239
3240 #[test]
3241 fn resume_session_config_new_enables_permission_flow_flags() {
3242 let cfg = ResumeSessionConfig::new(SessionId::from("test-id"));
3243 assert_eq!(cfg.request_user_input, Some(true));
3244 assert_eq!(cfg.request_permission, Some(true));
3245 assert_eq!(cfg.request_elicitation, Some(true));
3246 assert_eq!(cfg.request_exit_plan_mode, Some(true));
3247 assert_eq!(cfg.request_auto_mode_switch, Some(true));
3248 }
3249
3250 #[test]
3251 fn session_config_builder_composes() {
3252 use std::collections::HashMap;
3253
3254 let cfg = SessionConfig::default()
3255 .with_session_id(SessionId::from("sess-1"))
3256 .with_model("claude-sonnet-4")
3257 .with_client_name("test-app")
3258 .with_reasoning_effort("medium")
3259 .with_streaming(true)
3260 .with_tools([Tool::new("greet")])
3261 .with_available_tools(["bash", "view"])
3262 .with_excluded_tools(["dangerous"])
3263 .with_mcp_servers(HashMap::new())
3264 .with_enable_config_discovery(true)
3265 .with_request_user_input(false)
3266 .with_request_exit_plan_mode(false)
3267 .with_request_auto_mode_switch(false)
3268 .with_skill_directories([PathBuf::from("/tmp/skills")])
3269 .with_disabled_skills(["broken-skill"])
3270 .with_agent("researcher")
3271 .with_config_dir(PathBuf::from("/tmp/config"))
3272 .with_working_directory(PathBuf::from("/tmp/work"))
3273 .with_github_token("ghp_test")
3274 .with_enable_session_telemetry(false)
3275 .with_include_sub_agent_streaming_events(false);
3276
3277 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
3278 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
3279 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
3280 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
3281 assert_eq!(cfg.streaming, Some(true));
3282 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
3283 assert_eq!(
3284 cfg.available_tools.as_deref(),
3285 Some(&["bash".to_string(), "view".to_string()][..])
3286 );
3287 assert_eq!(
3288 cfg.excluded_tools.as_deref(),
3289 Some(&["dangerous".to_string()][..])
3290 );
3291 assert!(cfg.mcp_servers.is_some());
3292 assert_eq!(cfg.enable_config_discovery, Some(true));
3293 assert_eq!(cfg.request_user_input, Some(false)); assert_eq!(cfg.request_permission, Some(true)); assert_eq!(cfg.request_exit_plan_mode, Some(false));
3296 assert_eq!(cfg.request_auto_mode_switch, Some(false));
3297 assert_eq!(
3298 cfg.skill_directories.as_deref(),
3299 Some(&[PathBuf::from("/tmp/skills")][..])
3300 );
3301 assert_eq!(
3302 cfg.disabled_skills.as_deref(),
3303 Some(&["broken-skill".to_string()][..])
3304 );
3305 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
3306 assert_eq!(cfg.config_dir, Some(PathBuf::from("/tmp/config")));
3307 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
3308 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
3309 assert_eq!(cfg.enable_session_telemetry, Some(false));
3310 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
3311 }
3312
3313 #[test]
3314 fn resume_session_config_builder_composes() {
3315 use std::collections::HashMap;
3316
3317 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
3318 .with_client_name("test-app")
3319 .with_streaming(true)
3320 .with_tools([Tool::new("greet")])
3321 .with_available_tools(["bash", "view"])
3322 .with_excluded_tools(["dangerous"])
3323 .with_mcp_servers(HashMap::new())
3324 .with_enable_config_discovery(true)
3325 .with_request_user_input(false)
3326 .with_request_exit_plan_mode(false)
3327 .with_request_auto_mode_switch(false)
3328 .with_skill_directories([PathBuf::from("/tmp/skills")])
3329 .with_disabled_skills(["broken-skill"])
3330 .with_agent("researcher")
3331 .with_config_dir(PathBuf::from("/tmp/config"))
3332 .with_working_directory(PathBuf::from("/tmp/work"))
3333 .with_github_token("ghp_test")
3334 .with_enable_session_telemetry(false)
3335 .with_include_sub_agent_streaming_events(true)
3336 .with_disable_resume(true)
3337 .with_continue_pending_work(true);
3338
3339 assert_eq!(cfg.session_id.as_str(), "sess-2");
3340 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
3341 assert_eq!(cfg.streaming, Some(true));
3342 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
3343 assert_eq!(
3344 cfg.available_tools.as_deref(),
3345 Some(&["bash".to_string(), "view".to_string()][..])
3346 );
3347 assert_eq!(
3348 cfg.excluded_tools.as_deref(),
3349 Some(&["dangerous".to_string()][..])
3350 );
3351 assert!(cfg.mcp_servers.is_some());
3352 assert_eq!(cfg.enable_config_discovery, Some(true));
3353 assert_eq!(cfg.request_user_input, Some(false)); assert_eq!(cfg.request_permission, Some(true)); assert_eq!(cfg.request_exit_plan_mode, Some(false));
3356 assert_eq!(cfg.request_auto_mode_switch, Some(false));
3357 assert_eq!(
3358 cfg.skill_directories.as_deref(),
3359 Some(&[PathBuf::from("/tmp/skills")][..])
3360 );
3361 assert_eq!(
3362 cfg.disabled_skills.as_deref(),
3363 Some(&["broken-skill".to_string()][..])
3364 );
3365 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
3366 assert_eq!(cfg.config_dir, Some(PathBuf::from("/tmp/config")));
3367 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
3368 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
3369 assert_eq!(cfg.enable_session_telemetry, Some(false));
3370 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
3371 assert_eq!(cfg.disable_resume, Some(true));
3372 assert_eq!(cfg.continue_pending_work, Some(true));
3373 }
3374
3375 #[test]
3379 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
3380 let cfg =
3381 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
3382 let wire = serde_json::to_value(&cfg).unwrap();
3383 assert_eq!(wire["continuePendingWork"], true);
3384
3385 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"));
3387 let wire = serde_json::to_value(&cfg).unwrap();
3388 assert!(wire.get("continuePendingWork").is_none());
3389 }
3390
3391 #[test]
3394 fn session_config_serializes_instruction_directories_to_camel_case() {
3395 let cfg =
3396 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
3397 let wire = serde_json::to_value(&cfg).unwrap();
3398 assert_eq!(
3399 wire["instructionDirectories"],
3400 serde_json::json!(["/tmp/instr"])
3401 );
3402
3403 let cfg = SessionConfig::default();
3405 let wire = serde_json::to_value(&cfg).unwrap();
3406 assert!(wire.get("instructionDirectories").is_none());
3407 }
3408
3409 #[test]
3412 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
3413 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
3414 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
3415 let wire = serde_json::to_value(&cfg).unwrap();
3416 assert_eq!(
3417 wire["instructionDirectories"],
3418 serde_json::json!(["/tmp/instr"])
3419 );
3420
3421 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"));
3422 let wire = serde_json::to_value(&cfg).unwrap();
3423 assert!(wire.get("instructionDirectories").is_none());
3424 }
3425
3426 #[test]
3427 fn custom_agent_config_builder_composes() {
3428 use std::collections::HashMap;
3429
3430 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
3431 .with_display_name("Research Assistant")
3432 .with_description("Investigates technical questions.")
3433 .with_tools(["bash", "view"])
3434 .with_mcp_servers(HashMap::new())
3435 .with_infer(true)
3436 .with_skills(["rust-coding-skill"]);
3437
3438 assert_eq!(cfg.name, "researcher");
3439 assert_eq!(cfg.prompt, "You are a research assistant.");
3440 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
3441 assert_eq!(
3442 cfg.description.as_deref(),
3443 Some("Investigates technical questions.")
3444 );
3445 assert_eq!(
3446 cfg.tools.as_deref(),
3447 Some(&["bash".to_string(), "view".to_string()][..])
3448 );
3449 assert!(cfg.mcp_servers.is_some());
3450 assert_eq!(cfg.infer, Some(true));
3451 assert_eq!(
3452 cfg.skills.as_deref(),
3453 Some(&["rust-coding-skill".to_string()][..])
3454 );
3455 }
3456
3457 #[test]
3458 fn infinite_session_config_builder_composes() {
3459 let cfg = InfiniteSessionConfig::new()
3460 .with_enabled(true)
3461 .with_background_compaction_threshold(0.75)
3462 .with_buffer_exhaustion_threshold(0.92);
3463
3464 assert_eq!(cfg.enabled, Some(true));
3465 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
3466 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
3467 }
3468
3469 #[test]
3470 fn provider_config_builder_composes() {
3471 use std::collections::HashMap;
3472
3473 let mut headers = HashMap::new();
3474 headers.insert("X-Custom".to_string(), "value".to_string());
3475
3476 let cfg = ProviderConfig::new("https://api.example.com")
3477 .with_provider_type("openai")
3478 .with_wire_api("completions")
3479 .with_api_key("sk-test")
3480 .with_bearer_token("bearer-test")
3481 .with_headers(headers)
3482 .with_model_id("gpt-4")
3483 .with_wire_model("azure-gpt-4-deployment")
3484 .with_max_prompt_tokens(8192)
3485 .with_max_output_tokens(2048);
3486
3487 assert_eq!(cfg.base_url, "https://api.example.com");
3488 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
3489 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
3490 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
3491 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
3492 assert_eq!(
3493 cfg.headers
3494 .as_ref()
3495 .and_then(|h| h.get("X-Custom"))
3496 .map(String::as_str),
3497 Some("value"),
3498 );
3499 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
3500 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
3501 assert_eq!(cfg.max_prompt_tokens, Some(8192));
3502 assert_eq!(cfg.max_output_tokens, Some(2048));
3503
3504 let wire = serde_json::to_value(&cfg).unwrap();
3506 assert_eq!(wire["modelId"], "gpt-4");
3507 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
3508 assert_eq!(wire["maxPromptTokens"], 8192);
3509 assert_eq!(wire["maxOutputTokens"], 2048);
3510
3511 let unset = ProviderConfig::new("https://api.example.com");
3512 let wire_unset = serde_json::to_value(&unset).unwrap();
3513 assert!(wire_unset.get("modelId").is_none());
3514 assert!(wire_unset.get("wireModel").is_none());
3515 assert!(wire_unset.get("maxPromptTokens").is_none());
3516 assert!(wire_unset.get("maxOutputTokens").is_none());
3517 }
3518
3519 #[test]
3520 fn system_message_config_builder_composes() {
3521 use std::collections::HashMap;
3522
3523 let cfg = SystemMessageConfig::new()
3524 .with_mode("replace")
3525 .with_content("Custom system message.")
3526 .with_sections(HashMap::new());
3527
3528 assert_eq!(cfg.mode.as_deref(), Some("replace"));
3529 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
3530 assert!(cfg.sections.is_some());
3531 }
3532
3533 #[test]
3534 fn delivery_mode_serializes_to_kebab_case_strings() {
3535 assert_eq!(
3536 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
3537 "\"enqueue\""
3538 );
3539 assert_eq!(
3540 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
3541 "\"immediate\""
3542 );
3543 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
3544 assert_eq!(parsed, DeliveryMode::Immediate);
3545 }
3546
3547 #[test]
3548 fn connection_state_error_serializes_to_match_go() {
3549 let json = serde_json::to_string(&ConnectionState::Error).unwrap();
3550 assert_eq!(json, "\"error\"");
3551 let parsed: ConnectionState = serde_json::from_str("\"error\"").unwrap();
3552 assert_eq!(parsed, ConnectionState::Error);
3553 }
3554
3555 #[test]
3561 fn session_event_round_trips_agent_id_on_envelope() {
3562 let wire = json!({
3563 "id": "evt-1",
3564 "timestamp": "2026-04-30T12:00:00Z",
3565 "parentId": null,
3566 "agentId": "sub-agent-42",
3567 "type": "assistant.message",
3568 "data": { "message": "hi" }
3569 });
3570
3571 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
3572 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
3573
3574 let roundtripped = serde_json::to_value(&event).unwrap();
3576 assert_eq!(roundtripped["agentId"], "sub-agent-42");
3577
3578 let main_agent_event: SessionEvent = serde_json::from_value(json!({
3580 "id": "evt-2",
3581 "timestamp": "2026-04-30T12:00:01Z",
3582 "parentId": null,
3583 "type": "session.idle",
3584 "data": {}
3585 }))
3586 .unwrap();
3587 assert!(main_agent_event.agent_id.is_none());
3588 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
3589 assert!(roundtripped.get("agentId").is_none());
3590 }
3591
3592 #[test]
3594 fn typed_session_event_round_trips_agent_id_on_envelope() {
3595 let wire = json!({
3596 "id": "evt-1",
3597 "timestamp": "2026-04-30T12:00:00Z",
3598 "parentId": null,
3599 "agentId": "sub-agent-42",
3600 "type": "session.idle",
3601 "data": {}
3602 });
3603
3604 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
3605 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
3606
3607 let roundtripped = serde_json::to_value(&event).unwrap();
3608 assert_eq!(roundtripped["agentId"], "sub-agent-42");
3609 }
3610
3611 #[test]
3612 fn connection_state_other_variants_serialize_as_lowercase() {
3613 assert_eq!(
3614 serde_json::to_string(&ConnectionState::Disconnected).unwrap(),
3615 "\"disconnected\""
3616 );
3617 assert_eq!(
3618 serde_json::to_string(&ConnectionState::Connecting).unwrap(),
3619 "\"connecting\""
3620 );
3621 assert_eq!(
3622 serde_json::to_string(&ConnectionState::Connected).unwrap(),
3623 "\"connected\""
3624 );
3625 }
3626
3627 #[test]
3628 fn deserializes_runtime_attachment_variants() {
3629 let attachments: Vec<Attachment> = serde_json::from_value(json!([
3630 {
3631 "type": "file",
3632 "path": "/tmp/file.rs",
3633 "displayName": "file.rs",
3634 "lineRange": { "start": 7, "end": 12 }
3635 },
3636 {
3637 "type": "directory",
3638 "path": "/tmp/project",
3639 "displayName": "project"
3640 },
3641 {
3642 "type": "selection",
3643 "filePath": "/tmp/lib.rs",
3644 "displayName": "lib.rs",
3645 "text": "fn main() {}",
3646 "selection": {
3647 "start": { "line": 1, "character": 2 },
3648 "end": { "line": 3, "character": 4 }
3649 }
3650 },
3651 {
3652 "type": "blob",
3653 "data": "Zm9v",
3654 "mimeType": "image/png",
3655 "displayName": "image.png"
3656 },
3657 {
3658 "type": "github_reference",
3659 "number": 42,
3660 "title": "Fix rendering",
3661 "referenceType": "issue",
3662 "state": "open",
3663 "url": "https://github.com/example/repo/issues/42"
3664 }
3665 ]))
3666 .expect("attachments should deserialize");
3667
3668 assert_eq!(attachments.len(), 5);
3669 assert!(matches!(
3670 &attachments[0],
3671 Attachment::File {
3672 path,
3673 display_name,
3674 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
3675 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
3676 ));
3677 assert!(matches!(
3678 &attachments[1],
3679 Attachment::Directory { path, display_name }
3680 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
3681 ));
3682 assert!(matches!(
3683 &attachments[2],
3684 Attachment::Selection {
3685 file_path,
3686 display_name,
3687 selection:
3688 AttachmentSelectionRange {
3689 start: AttachmentSelectionPosition { line: 1, character: 2 },
3690 end: AttachmentSelectionPosition { line: 3, character: 4 },
3691 },
3692 ..
3693 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
3694 ));
3695 assert!(matches!(
3696 &attachments[3],
3697 Attachment::Blob {
3698 data,
3699 mime_type,
3700 display_name,
3701 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
3702 ));
3703 assert!(matches!(
3704 &attachments[4],
3705 Attachment::GitHubReference {
3706 number: 42,
3707 title,
3708 reference_type: GitHubReferenceType::Issue,
3709 state,
3710 url,
3711 } if title == "Fix rendering"
3712 && state == "open"
3713 && url == "https://github.com/example/repo/issues/42"
3714 ));
3715 }
3716
3717 #[test]
3718 fn ensures_display_names_for_variants_that_support_them() {
3719 let mut attachments = vec![
3720 Attachment::File {
3721 path: PathBuf::from("/tmp/file.rs"),
3722 display_name: None,
3723 line_range: None,
3724 },
3725 Attachment::Selection {
3726 file_path: PathBuf::from("/tmp/src/lib.rs"),
3727 display_name: None,
3728 text: "fn main() {}".to_string(),
3729 selection: AttachmentSelectionRange {
3730 start: AttachmentSelectionPosition {
3731 line: 0,
3732 character: 0,
3733 },
3734 end: AttachmentSelectionPosition {
3735 line: 0,
3736 character: 10,
3737 },
3738 },
3739 },
3740 Attachment::Blob {
3741 data: "Zm9v".to_string(),
3742 mime_type: "image/png".to_string(),
3743 display_name: None,
3744 },
3745 Attachment::GitHubReference {
3746 number: 7,
3747 title: "Track regressions".to_string(),
3748 reference_type: GitHubReferenceType::Issue,
3749 state: "open".to_string(),
3750 url: "https://example.com/issues/7".to_string(),
3751 },
3752 ];
3753
3754 ensure_attachment_display_names(&mut attachments);
3755
3756 assert_eq!(attachments[0].display_name(), Some("file.rs"));
3757 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
3758 assert_eq!(attachments[2].display_name(), Some("attachment"));
3759 assert_eq!(attachments[3].display_name(), None);
3760 assert_eq!(
3761 attachments[3].label(),
3762 Some("Track regressions".to_string())
3763 );
3764 }
3765}
3766
3767#[cfg(test)]
3768mod permission_builder_tests {
3769 use std::sync::Arc;
3770
3771 use crate::handler::{
3772 ApproveAllHandler, HandlerEvent, HandlerResponse, PermissionResult, SessionHandler,
3773 };
3774 use crate::types::{
3775 PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, SessionId,
3776 };
3777
3778 fn permission_event() -> HandlerEvent {
3779 HandlerEvent::PermissionRequest {
3780 session_id: SessionId::from("s1"),
3781 request_id: RequestId::new("1"),
3782 data: PermissionRequestData {
3783 extra: serde_json::json!({"tool": "shell"}),
3784 ..Default::default()
3785 },
3786 }
3787 }
3788
3789 async fn dispatch(handler: &Arc<dyn SessionHandler>) -> HandlerResponse {
3790 handler.on_event(permission_event()).await
3791 }
3792
3793 #[tokio::test]
3794 async fn session_config_approve_all_wraps_existing_handler() {
3795 let cfg = SessionConfig::default()
3796 .with_handler(Arc::new(ApproveAllHandler))
3797 .approve_all_permissions();
3798 let handler = cfg.handler.expect("handler should be set");
3799 match dispatch(&handler).await {
3800 HandlerResponse::Permission(PermissionResult::Approved) => {}
3801 other => panic!("expected Approved, got {other:?}"),
3802 }
3803 }
3804
3805 #[tokio::test]
3806 async fn session_config_approve_all_defaults_to_deny_inner() {
3807 let cfg = SessionConfig::default().approve_all_permissions();
3811 let handler = cfg.handler.expect("handler should be set");
3812 match dispatch(&handler).await {
3813 HandlerResponse::Permission(PermissionResult::Approved) => {}
3814 other => panic!("expected Approved, got {other:?}"),
3815 }
3816 }
3817
3818 #[tokio::test]
3819 async fn session_config_deny_all_denies() {
3820 let cfg = SessionConfig::default()
3821 .with_handler(Arc::new(ApproveAllHandler))
3822 .deny_all_permissions();
3823 let handler = cfg.handler.expect("handler should be set");
3824 match dispatch(&handler).await {
3825 HandlerResponse::Permission(PermissionResult::Denied) => {}
3826 other => panic!("expected Denied, got {other:?}"),
3827 }
3828 }
3829
3830 #[tokio::test]
3831 async fn session_config_approve_permissions_if_consults_predicate() {
3832 let cfg = SessionConfig::default()
3833 .with_handler(Arc::new(ApproveAllHandler))
3834 .approve_permissions_if(|data| {
3835 data.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
3836 });
3837 let handler = cfg.handler.expect("handler should be set");
3838 match dispatch(&handler).await {
3839 HandlerResponse::Permission(PermissionResult::Denied) => {}
3840 other => panic!("expected Denied for shell, got {other:?}"),
3841 }
3842 }
3843
3844 #[tokio::test]
3845 async fn resume_session_config_approve_all_wraps_existing_handler() {
3846 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
3847 .with_handler(Arc::new(ApproveAllHandler))
3848 .approve_all_permissions();
3849 let handler = cfg.handler.expect("handler should be set");
3850 match dispatch(&handler).await {
3851 HandlerResponse::Permission(PermissionResult::Approved) => {}
3852 other => panic!("expected Approved, got {other:?}"),
3853 }
3854 }
3855}