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::canvas::{CanvasDeclaration, CanvasHandler};
16use crate::generated::api_types::OpenCanvasInstance;
17pub use crate::generated::session_events::ContextTier;
19use crate::generated::session_events::ReasoningSummary;
20use crate::handler::{
21 AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, PermissionHandler,
22 UserInputHandler,
23};
24use crate::hooks::SessionHooks;
25pub use crate::session_fs::{
26 DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig,
27 SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult,
28 SessionFsSqliteQueryType,
29};
30pub use crate::trace_context::{TraceContext, TraceContextProvider};
31use crate::transforms::SystemMessageTransform;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36#[allow(dead_code)]
37#[non_exhaustive]
38pub(crate) enum ConnectionState {
39 Disconnected,
41 Connecting,
43 Connected,
45 Error,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
54#[non_exhaustive]
55pub enum SessionLifecycleEventType {
56 #[serde(rename = "session.created")]
58 Created,
59 #[serde(rename = "session.deleted")]
61 Deleted,
62 #[serde(rename = "session.updated")]
64 Updated,
65 #[serde(rename = "session.foreground")]
67 Foreground,
68 #[serde(rename = "session.background")]
70 Background,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct SessionLifecycleEventMetadata {
76 #[serde(rename = "startTime")]
78 pub start_time: String,
79 #[serde(rename = "modifiedTime")]
81 pub modified_time: String,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub summary: Option<String>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90pub struct SessionLifecycleEvent {
91 #[serde(rename = "type")]
93 pub event_type: SessionLifecycleEventType,
94 #[serde(rename = "sessionId")]
96 pub session_id: SessionId,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub metadata: Option<SessionLifecycleEventMetadata>,
100}
101
102#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
108#[serde(transparent)]
109pub struct SessionId(String);
110
111impl SessionId {
112 pub fn new(id: impl Into<String>) -> Self {
114 Self(id.into())
115 }
116
117 pub fn as_str(&self) -> &str {
119 &self.0
120 }
121
122 pub fn into_inner(self) -> String {
124 self.0
125 }
126}
127
128impl std::ops::Deref for SessionId {
129 type Target = str;
130
131 fn deref(&self) -> &str {
132 &self.0
133 }
134}
135
136impl std::fmt::Display for SessionId {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 f.write_str(&self.0)
139 }
140}
141
142impl From<String> for SessionId {
143 fn from(s: String) -> Self {
144 Self(s)
145 }
146}
147
148impl From<&str> for SessionId {
149 fn from(s: &str) -> Self {
150 Self(s.to_owned())
151 }
152}
153
154impl AsRef<str> for SessionId {
155 fn as_ref(&self) -> &str {
156 &self.0
157 }
158}
159
160impl std::borrow::Borrow<str> for SessionId {
161 fn borrow(&self) -> &str {
162 &self.0
163 }
164}
165
166impl From<SessionId> for String {
167 fn from(id: SessionId) -> String {
168 id.0
169 }
170}
171
172impl PartialEq<str> for SessionId {
173 fn eq(&self, other: &str) -> bool {
174 self.0 == other
175 }
176}
177
178impl PartialEq<String> for SessionId {
179 fn eq(&self, other: &String) -> bool {
180 &self.0 == other
181 }
182}
183
184impl PartialEq<SessionId> for String {
185 fn eq(&self, other: &SessionId) -> bool {
186 self == &other.0
187 }
188}
189
190impl PartialEq<&str> for SessionId {
191 fn eq(&self, other: &&str) -> bool {
192 self.0 == *other
193 }
194}
195
196impl PartialEq<&SessionId> for SessionId {
197 fn eq(&self, other: &&SessionId) -> bool {
198 self.0 == other.0
199 }
200}
201
202impl PartialEq<SessionId> for &SessionId {
203 fn eq(&self, other: &SessionId) -> bool {
204 self.0 == other.0
205 }
206}
207
208#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
214#[serde(transparent)]
215pub struct RequestId(String);
216
217impl RequestId {
218 pub fn new(id: impl Into<String>) -> Self {
220 Self(id.into())
221 }
222
223 pub fn into_inner(self) -> String {
225 self.0
226 }
227}
228
229impl std::ops::Deref for RequestId {
230 type Target = str;
231
232 fn deref(&self) -> &str {
233 &self.0
234 }
235}
236
237impl std::fmt::Display for RequestId {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 f.write_str(&self.0)
240 }
241}
242
243impl From<String> for RequestId {
244 fn from(s: String) -> Self {
245 Self(s)
246 }
247}
248
249impl From<&str> for RequestId {
250 fn from(s: &str) -> Self {
251 Self(s.to_owned())
252 }
253}
254
255impl AsRef<str> for RequestId {
256 fn as_ref(&self) -> &str {
257 &self.0
258 }
259}
260
261impl std::borrow::Borrow<str> for RequestId {
262 fn borrow(&self) -> &str {
263 &self.0
264 }
265}
266
267impl From<RequestId> for String {
268 fn from(id: RequestId) -> String {
269 id.0
270 }
271}
272
273impl PartialEq<str> for RequestId {
274 fn eq(&self, other: &str) -> bool {
275 self.0 == other
276 }
277}
278
279impl PartialEq<String> for RequestId {
280 fn eq(&self, other: &String) -> bool {
281 &self.0 == other
282 }
283}
284
285impl PartialEq<RequestId> for String {
286 fn eq(&self, other: &RequestId) -> bool {
287 self == &other.0
288 }
289}
290
291impl PartialEq<&str> for RequestId {
292 fn eq(&self, other: &&str) -> bool {
293 self.0 == *other
294 }
295}
296
297#[derive(Clone, Default, Serialize, Deserialize)]
312#[serde(rename_all = "camelCase")]
313#[non_exhaustive]
314pub struct Tool {
315 pub name: String,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub namespaced_name: Option<String>,
321 #[serde(default)]
323 pub description: String,
324 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub instructions: Option<String>,
327 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
329 pub parameters: HashMap<String, Value>,
330 #[serde(default, skip_serializing_if = "is_false")]
334 pub overrides_built_in_tool: bool,
335 #[serde(default, skip_serializing_if = "is_false")]
339 pub skip_permission: bool,
340 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub defer: Option<DeferMode>,
347 #[serde(skip)]
359 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
360}
361
362#[inline]
363fn is_false(b: &bool) -> bool {
364 !*b
365}
366
367#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
370#[serde(rename_all = "lowercase")]
371pub enum DeferMode {
372 Auto,
374 Never,
376}
377
378impl Tool {
379 pub fn new(name: impl Into<String>) -> Self {
399 Self {
400 name: name.into(),
401 ..Default::default()
402 }
403 }
404
405 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
408 self.namespaced_name = Some(namespaced_name.into());
409 self
410 }
411
412 pub fn with_description(mut self, description: impl Into<String>) -> Self {
414 self.description = description.into();
415 self
416 }
417
418 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
420 self.instructions = Some(instructions.into());
421 self
422 }
423
424 pub fn with_parameters(mut self, parameters: Value) -> Self {
438 self.parameters = crate::tool::tool_parameters(parameters);
439 self
440 }
441
442 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
446 self.overrides_built_in_tool = overrides;
447 self
448 }
449
450 pub fn with_skip_permission(mut self, skip: bool) -> Self {
454 self.skip_permission = skip;
455 self
456 }
457
458 pub fn with_defer(mut self, defer: DeferMode) -> Self {
462 self.defer = Some(defer);
463 self
464 }
465
466 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
470 self.handler = Some(handler);
471 self
472 }
473
474 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
479 self.handler.as_ref()
480 }
481}
482
483impl std::fmt::Debug for Tool {
484 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
485 f.debug_struct("Tool")
486 .field("name", &self.name)
487 .field("namespaced_name", &self.namespaced_name)
488 .field("description", &self.description)
489 .field("instructions", &self.instructions)
490 .field("parameters", &self.parameters)
491 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
492 .field("skip_permission", &self.skip_permission)
493 .field("defer", &self.defer)
494 .field(
495 "handler",
496 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
497 )
498 .finish()
499 }
500}
501
502#[non_exhaustive]
505#[derive(Debug, Clone)]
506pub struct CommandContext {
507 pub session_id: SessionId,
509 pub command: String,
511 pub command_name: String,
513 pub args: String,
515}
516
517#[async_trait::async_trait]
523pub trait CommandHandler: Send + Sync {
524 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
526}
527
528#[non_exhaustive]
534#[derive(Clone)]
535pub struct CommandDefinition {
536 pub name: String,
538 pub description: Option<String>,
540 pub handler: Arc<dyn CommandHandler>,
542}
543
544impl CommandDefinition {
545 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
548 Self {
549 name: name.into(),
550 description: None,
551 handler,
552 }
553 }
554
555 pub fn with_description(mut self, description: impl Into<String>) -> Self {
557 self.description = Some(description.into());
558 self
559 }
560}
561
562impl std::fmt::Debug for CommandDefinition {
563 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
564 f.debug_struct("CommandDefinition")
565 .field("name", &self.name)
566 .field("description", &self.description)
567 .field("handler", &"<set>")
568 .finish()
569 }
570}
571
572impl Serialize for CommandDefinition {
573 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
574 use serde::ser::SerializeStruct;
575 let len = if self.description.is_some() { 2 } else { 1 };
576 let mut state = serializer.serialize_struct("CommandDefinition", len)?;
577 state.serialize_field("name", &self.name)?;
578 if let Some(description) = &self.description {
579 state.serialize_field("description", description)?;
580 }
581 state.end()
582 }
583}
584
585#[derive(Debug, Clone, Default, Serialize, Deserialize)]
592#[serde(rename_all = "camelCase")]
593#[non_exhaustive]
594pub struct CustomAgentConfig {
595 pub name: String,
597 #[serde(default, skip_serializing_if = "Option::is_none")]
599 pub display_name: Option<String>,
600 #[serde(default, skip_serializing_if = "Option::is_none")]
602 pub description: Option<String>,
603 #[serde(default, skip_serializing_if = "Option::is_none")]
605 pub tools: Option<Vec<String>>,
606 pub prompt: String,
608 #[serde(default, skip_serializing_if = "Option::is_none")]
610 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
611 #[serde(default, skip_serializing_if = "Option::is_none")]
613 pub infer: Option<bool>,
614 #[serde(default, skip_serializing_if = "Option::is_none")]
616 pub skills: Option<Vec<String>>,
617 #[serde(default, skip_serializing_if = "Option::is_none")]
622 pub model: Option<String>,
623}
624
625impl CustomAgentConfig {
626 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
633 Self {
634 name: name.into(),
635 prompt: prompt.into(),
636 ..Self::default()
637 }
638 }
639
640 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
642 self.display_name = Some(display_name.into());
643 self
644 }
645
646 pub fn with_description(mut self, description: impl Into<String>) -> Self {
648 self.description = Some(description.into());
649 self
650 }
651
652 pub fn with_tools<I, S>(mut self, tools: I) -> Self
655 where
656 I: IntoIterator<Item = S>,
657 S: Into<String>,
658 {
659 self.tools = Some(tools.into_iter().map(Into::into).collect());
660 self
661 }
662
663 pub fn with_mcp_servers(mut self, mcp_servers: HashMap<String, McpServerConfig>) -> Self {
665 self.mcp_servers = Some(mcp_servers);
666 self
667 }
668
669 pub fn with_infer(mut self, infer: bool) -> Self {
671 self.infer = Some(infer);
672 self
673 }
674
675 pub fn with_skills<I, S>(mut self, skills: I) -> Self
677 where
678 I: IntoIterator<Item = S>,
679 S: Into<String>,
680 {
681 self.skills = Some(skills.into_iter().map(Into::into).collect());
682 self
683 }
684
685 pub fn with_model(mut self, model: impl Into<String>) -> Self {
687 self.model = Some(model.into());
688 self
689 }
690}
691
692#[derive(Debug, Clone, Default, Serialize, Deserialize)]
699#[serde(rename_all = "camelCase")]
700pub struct DefaultAgentConfig {
701 #[serde(default, skip_serializing_if = "Option::is_none")]
703 pub excluded_tools: Option<Vec<String>>,
704}
705
706#[derive(Debug, Clone, Default, Serialize, Deserialize)]
712#[serde(rename_all = "camelCase")]
713#[non_exhaustive]
714pub struct LargeToolOutputConfig {
715 #[serde(default, skip_serializing_if = "Option::is_none")]
717 pub enabled: Option<bool>,
718 #[serde(default, skip_serializing_if = "Option::is_none")]
721 pub max_size_bytes: Option<u64>,
722 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
725 pub output_directory: Option<PathBuf>,
726}
727
728impl LargeToolOutputConfig {
729 pub fn new() -> Self {
732 Self::default()
733 }
734
735 pub fn with_enabled(mut self, enabled: bool) -> Self {
737 self.enabled = Some(enabled);
738 self
739 }
740
741 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
743 self.max_size_bytes = Some(max_size_bytes);
744 self
745 }
746
747 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
749 self.output_directory = Some(output_directory.into());
750 self
751 }
752}
753
754#[derive(Debug, Clone, Default, Serialize, Deserialize)]
761#[serde(rename_all = "camelCase")]
762#[non_exhaustive]
763pub struct InfiniteSessionConfig {
764 #[serde(default, skip_serializing_if = "Option::is_none")]
766 pub enabled: Option<bool>,
767 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub background_compaction_threshold: Option<f64>,
771 #[serde(default, skip_serializing_if = "Option::is_none")]
774 pub buffer_exhaustion_threshold: Option<f64>,
775}
776
777impl InfiniteSessionConfig {
778 pub fn new() -> Self {
781 Self::default()
782 }
783
784 pub fn with_enabled(mut self, enabled: bool) -> Self {
787 self.enabled = Some(enabled);
788 self
789 }
790
791 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
794 self.background_compaction_threshold = Some(threshold);
795 self
796 }
797
798 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
801 self.buffer_exhaustion_threshold = Some(threshold);
802 self
803 }
804}
805
806#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
817#[serde(rename_all = "camelCase")]
818#[non_exhaustive]
819pub struct MemoryConfiguration {
820 pub enabled: bool,
822}
823
824impl MemoryConfiguration {
825 pub fn enabled() -> Self {
827 Self { enabled: true }
828 }
829
830 pub fn disabled() -> Self {
832 Self { enabled: false }
833 }
834
835 pub fn with_enabled(mut self, enabled: bool) -> Self {
837 self.enabled = enabled;
838 self
839 }
840}
841
842#[derive(Debug, Clone, Serialize, Deserialize)]
844#[serde(rename_all = "camelCase")]
845#[non_exhaustive]
846pub struct CloudSessionRepository {
847 pub owner: String,
849 pub name: String,
851 #[serde(skip_serializing_if = "Option::is_none")]
853 pub branch: Option<String>,
854}
855
856impl CloudSessionRepository {
857 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
859 Self {
860 owner: owner.into(),
861 name: name.into(),
862 branch: None,
863 }
864 }
865
866 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
868 self.branch = Some(branch.into());
869 self
870 }
871}
872
873#[derive(Debug, Clone, Default, Serialize, Deserialize)]
875#[serde(rename_all = "camelCase")]
876#[non_exhaustive]
877pub struct CloudSessionOptions {
878 #[serde(skip_serializing_if = "Option::is_none")]
880 pub repository: Option<CloudSessionRepository>,
881}
882
883impl CloudSessionOptions {
884 pub fn with_repository(repository: CloudSessionRepository) -> Self {
886 Self {
887 repository: Some(repository),
888 }
889 }
890}
891
892#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
894#[serde(rename_all = "camelCase")]
895pub struct ExtensionInfo {
896 pub source: String,
898 pub name: String,
900}
901
902impl ExtensionInfo {
903 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
905 Self {
906 source: source.into(),
907 name: name.into(),
908 }
909 }
910}
911
912#[derive(Debug, Clone, Serialize, Deserialize)]
946#[serde(tag = "type", rename_all = "lowercase")]
947#[non_exhaustive]
948pub enum McpServerConfig {
949 #[serde(alias = "local")]
953 Stdio(McpStdioServerConfig),
954 Http(McpHttpServerConfig),
956 Sse(McpHttpServerConfig),
958}
959
960#[derive(Debug, Clone, Default, Serialize, Deserialize)]
964#[serde(rename_all = "camelCase")]
965pub struct McpStdioServerConfig {
966 #[serde(default, skip_serializing_if = "Option::is_none")]
972 pub tools: Option<Vec<String>>,
973 #[serde(default, skip_serializing_if = "Option::is_none")]
975 pub timeout: Option<i64>,
976 pub command: String,
978 #[serde(default, skip_serializing_if = "Vec::is_empty")]
980 pub args: Vec<String>,
981 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
984 pub env: HashMap<String, String>,
985 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
987 pub working_directory: Option<String>,
988}
989
990#[derive(Debug, Clone, Default, Serialize, Deserialize)]
994#[serde(rename_all = "camelCase")]
995pub struct McpHttpServerConfig {
996 #[serde(default, skip_serializing_if = "Option::is_none")]
1002 pub tools: Option<Vec<String>>,
1003 #[serde(default, skip_serializing_if = "Option::is_none")]
1005 pub timeout: Option<i64>,
1006 pub url: String,
1008 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1010 pub headers: HashMap<String, String>,
1011}
1012
1013#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1019#[serde(rename_all = "camelCase")]
1020#[non_exhaustive]
1021pub struct ProviderConfig {
1022 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1025 pub provider_type: Option<String>,
1026 #[serde(default, skip_serializing_if = "Option::is_none")]
1029 pub wire_api: Option<String>,
1030 pub base_url: String,
1032 #[serde(default, skip_serializing_if = "Option::is_none")]
1034 pub api_key: Option<String>,
1035 #[serde(default, skip_serializing_if = "Option::is_none")]
1039 pub bearer_token: Option<String>,
1040 #[serde(default, skip_serializing_if = "Option::is_none")]
1042 pub azure: Option<AzureProviderOptions>,
1043 #[serde(default, skip_serializing_if = "Option::is_none")]
1045 pub headers: Option<HashMap<String, String>>,
1046 #[serde(default, skip_serializing_if = "Option::is_none")]
1050 pub model_id: Option<String>,
1051 #[serde(default, skip_serializing_if = "Option::is_none")]
1058 pub wire_model: Option<String>,
1059 #[serde(default, skip_serializing_if = "Option::is_none")]
1064 pub max_prompt_tokens: Option<i64>,
1065 #[serde(default, skip_serializing_if = "Option::is_none")]
1068 pub max_output_tokens: Option<i64>,
1069}
1070
1071impl ProviderConfig {
1072 pub fn new(base_url: impl Into<String>) -> Self {
1075 Self {
1076 base_url: base_url.into(),
1077 ..Self::default()
1078 }
1079 }
1080
1081 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1083 self.provider_type = Some(provider_type.into());
1084 self
1085 }
1086
1087 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1089 self.wire_api = Some(wire_api.into());
1090 self
1091 }
1092
1093 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1095 self.api_key = Some(api_key.into());
1096 self
1097 }
1098
1099 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1102 self.bearer_token = Some(bearer_token.into());
1103 self
1104 }
1105
1106 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1108 self.azure = Some(azure);
1109 self
1110 }
1111
1112 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1114 self.headers = Some(headers);
1115 self
1116 }
1117
1118 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1121 self.model_id = Some(model_id.into());
1122 self
1123 }
1124
1125 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1130 self.wire_model = Some(wire_model.into());
1131 self
1132 }
1133
1134 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1138 self.max_prompt_tokens = Some(max);
1139 self
1140 }
1141
1142 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1145 self.max_output_tokens = Some(max);
1146 self
1147 }
1148}
1149
1150#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1152#[serde(rename_all = "camelCase")]
1153pub struct AzureProviderOptions {
1154 #[serde(default, skip_serializing_if = "Option::is_none")]
1156 pub api_version: Option<String>,
1157}
1158
1159#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1170#[serde(rename_all = "camelCase")]
1171#[non_exhaustive]
1172pub struct NamedProviderConfig {
1173 pub name: String,
1176 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1179 pub provider_type: Option<String>,
1180 #[serde(default, skip_serializing_if = "Option::is_none")]
1183 pub wire_api: Option<String>,
1184 pub base_url: String,
1186 #[serde(default, skip_serializing_if = "Option::is_none")]
1188 pub api_key: Option<String>,
1189 #[serde(default, skip_serializing_if = "Option::is_none")]
1192 pub bearer_token: Option<String>,
1193 #[serde(default, skip_serializing_if = "Option::is_none")]
1195 pub azure: Option<AzureProviderOptions>,
1196 #[serde(default, skip_serializing_if = "Option::is_none")]
1198 pub headers: Option<HashMap<String, String>>,
1199}
1200
1201impl NamedProviderConfig {
1202 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1205 Self {
1206 name: name.into(),
1207 base_url: base_url.into(),
1208 ..Self::default()
1209 }
1210 }
1211
1212 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1214 self.provider_type = Some(provider_type.into());
1215 self
1216 }
1217
1218 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1220 self.wire_api = Some(wire_api.into());
1221 self
1222 }
1223
1224 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1226 self.api_key = Some(api_key.into());
1227 self
1228 }
1229
1230 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1233 self.bearer_token = Some(bearer_token.into());
1234 self
1235 }
1236
1237 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1239 self.azure = Some(azure);
1240 self
1241 }
1242
1243 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1245 self.headers = Some(headers);
1246 self
1247 }
1248}
1249
1250#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1258#[serde(rename_all = "camelCase")]
1259#[non_exhaustive]
1260pub struct ProviderModelConfig {
1261 pub id: String,
1264 pub provider: String,
1266 #[serde(default, skip_serializing_if = "Option::is_none")]
1269 pub wire_model: Option<String>,
1270 #[serde(default, skip_serializing_if = "Option::is_none")]
1273 pub model_id: Option<String>,
1274 #[serde(default, skip_serializing_if = "Option::is_none")]
1276 pub name: Option<String>,
1277 #[serde(default, skip_serializing_if = "Option::is_none")]
1279 pub max_prompt_tokens: Option<i64>,
1280 #[serde(default, skip_serializing_if = "Option::is_none")]
1282 pub max_context_window_tokens: Option<i64>,
1283 #[serde(default, skip_serializing_if = "Option::is_none")]
1285 pub max_output_tokens: Option<i64>,
1286 #[serde(default, skip_serializing_if = "Option::is_none")]
1289 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1290}
1291
1292impl ProviderModelConfig {
1293 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1296 Self {
1297 id: id.into(),
1298 provider: provider.into(),
1299 ..Self::default()
1300 }
1301 }
1302
1303 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1305 self.wire_model = Some(wire_model.into());
1306 self
1307 }
1308
1309 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1312 self.model_id = Some(model_id.into());
1313 self
1314 }
1315
1316 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1318 self.name = Some(name.into());
1319 self
1320 }
1321
1322 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1324 self.max_prompt_tokens = Some(max);
1325 self
1326 }
1327
1328 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1330 self.max_context_window_tokens = Some(max);
1331 self
1332 }
1333
1334 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1336 self.max_output_tokens = Some(max);
1337 self
1338 }
1339
1340 pub fn with_capabilities(
1342 mut self,
1343 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1344 ) -> Self {
1345 self.capabilities = Some(capabilities);
1346 self
1347 }
1348}
1349
1350#[derive(Clone)]
1402#[non_exhaustive]
1403pub struct SessionConfig {
1404 pub session_id: Option<SessionId>,
1406 pub model: Option<String>,
1408 pub client_name: Option<String>,
1410 pub reasoning_effort: Option<String>,
1412 pub reasoning_summary: Option<ReasoningSummary>,
1416 pub context_tier: Option<String>,
1419 pub streaming: Option<bool>,
1421 pub system_message: Option<SystemMessageConfig>,
1423 pub tools: Option<Vec<Tool>>,
1425 pub canvases: Option<Vec<CanvasDeclaration>>,
1427 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1432 pub request_canvas_renderer: Option<bool>,
1434 pub request_extensions: Option<bool>,
1436 pub extension_sdk_path: Option<String>,
1440 pub extension_info: Option<ExtensionInfo>,
1442 pub available_tools: Option<Vec<String>>,
1444 pub excluded_tools: Option<Vec<String>>,
1446 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
1448 pub mcp_oauth_token_storage: Option<String>,
1457 pub enable_config_discovery: Option<bool>,
1459 pub skip_embedding_retrieval: Option<bool>,
1461 pub embedding_cache_storage: Option<String>,
1464 pub organization_custom_instructions: Option<String>,
1466 pub enable_on_demand_instruction_discovery: Option<bool>,
1468 pub enable_file_hooks: Option<bool>,
1470 pub enable_host_git_operations: Option<bool>,
1472 pub enable_session_store: Option<bool>,
1474 pub enable_skills: Option<bool>,
1476 pub enable_mcp_apps: Option<bool>,
1503 pub skill_directories: Option<Vec<PathBuf>>,
1505 pub instruction_directories: Option<Vec<PathBuf>>,
1508 pub plugin_directories: Option<Vec<PathBuf>>,
1510 pub large_output: Option<LargeToolOutputConfig>,
1512 pub disabled_skills: Option<Vec<String>>,
1515 pub hooks: Option<bool>,
1519 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1521 pub default_agent: Option<DefaultAgentConfig>,
1525 pub agent: Option<String>,
1528 pub infinite_sessions: Option<InfiniteSessionConfig>,
1531 pub provider: Option<ProviderConfig>,
1535 pub providers: Option<Vec<NamedProviderConfig>>,
1542 pub models: Option<Vec<ProviderModelConfig>>,
1548 pub enable_session_telemetry: Option<bool>,
1556 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1559 pub memory: Option<MemoryConfiguration>,
1561 pub config_directory: Option<PathBuf>,
1564 pub working_directory: Option<PathBuf>,
1567 pub github_token: Option<String>,
1573 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
1579 pub cloud: Option<CloudSessionOptions>,
1582 pub include_sub_agent_streaming_events: Option<bool>,
1586 pub commands: Option<Vec<CommandDefinition>>,
1590 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1595 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
1599 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
1602 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
1606 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
1609 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
1612 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1616 pub(crate) permission_policy: Option<crate::permission::Policy>,
1620 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
1625 pub skip_custom_instructions: Option<bool>,
1629 pub custom_agents_local_only: Option<bool>,
1633 pub coauthor_enabled: Option<bool>,
1637 pub manage_schedule_enabled: Option<bool>,
1641}
1642
1643impl std::fmt::Debug for SessionConfig {
1644 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1645 f.debug_struct("SessionConfig")
1646 .field("session_id", &self.session_id)
1647 .field("model", &self.model)
1648 .field("client_name", &self.client_name)
1649 .field("reasoning_effort", &self.reasoning_effort)
1650 .field("reasoning_summary", &self.reasoning_summary)
1651 .field("context_tier", &self.context_tier)
1652 .field("streaming", &self.streaming)
1653 .field("system_message", &self.system_message)
1654 .field("tools", &self.tools)
1655 .field("canvases", &self.canvases)
1656 .field(
1657 "canvas_handler",
1658 &self.canvas_handler.as_ref().map(|_| "<set>"),
1659 )
1660 .field("request_canvas_renderer", &self.request_canvas_renderer)
1661 .field("request_extensions", &self.request_extensions)
1662 .field("extension_sdk_path", &self.extension_sdk_path)
1663 .field("extension_info", &self.extension_info)
1664 .field("available_tools", &self.available_tools)
1665 .field("excluded_tools", &self.excluded_tools)
1666 .field("mcp_servers", &self.mcp_servers)
1667 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
1668 .field("embedding_cache_storage", &self.embedding_cache_storage)
1669 .field("enable_config_discovery", &self.enable_config_discovery)
1670 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
1671 .field(
1672 "organization_custom_instructions",
1673 &self
1674 .organization_custom_instructions
1675 .as_ref()
1676 .map(|_| "<redacted>"),
1677 )
1678 .field(
1679 "enable_on_demand_instruction_discovery",
1680 &self.enable_on_demand_instruction_discovery,
1681 )
1682 .field("enable_file_hooks", &self.enable_file_hooks)
1683 .field(
1684 "enable_host_git_operations",
1685 &self.enable_host_git_operations,
1686 )
1687 .field("enable_session_store", &self.enable_session_store)
1688 .field("enable_skills", &self.enable_skills)
1689 .field("enable_mcp_apps", &self.enable_mcp_apps)
1690 .field("skill_directories", &self.skill_directories)
1691 .field("instruction_directories", &self.instruction_directories)
1692 .field("plugin_directories", &self.plugin_directories)
1693 .field("large_output", &self.large_output)
1694 .field("disabled_skills", &self.disabled_skills)
1695 .field("hooks", &self.hooks)
1696 .field("custom_agents", &self.custom_agents)
1697 .field("default_agent", &self.default_agent)
1698 .field("agent", &self.agent)
1699 .field("infinite_sessions", &self.infinite_sessions)
1700 .field("provider", &self.provider)
1701 .field("enable_session_telemetry", &self.enable_session_telemetry)
1702 .field("model_capabilities", &self.model_capabilities)
1703 .field("memory", &self.memory)
1704 .field("config_directory", &self.config_directory)
1705 .field("working_directory", &self.working_directory)
1706 .field(
1707 "github_token",
1708 &self.github_token.as_ref().map(|_| "<redacted>"),
1709 )
1710 .field("remote_session", &self.remote_session)
1711 .field("cloud", &self.cloud)
1712 .field(
1713 "include_sub_agent_streaming_events",
1714 &self.include_sub_agent_streaming_events,
1715 )
1716 .field("commands", &self.commands)
1717 .field(
1718 "session_fs_provider",
1719 &self.session_fs_provider.as_ref().map(|_| "<set>"),
1720 )
1721 .field(
1722 "permission_handler",
1723 &self.permission_handler.as_ref().map(|_| "<set>"),
1724 )
1725 .field(
1726 "elicitation_handler",
1727 &self.elicitation_handler.as_ref().map(|_| "<set>"),
1728 )
1729 .field(
1730 "user_input_handler",
1731 &self.user_input_handler.as_ref().map(|_| "<set>"),
1732 )
1733 .field(
1734 "exit_plan_mode_handler",
1735 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
1736 )
1737 .field(
1738 "auto_mode_switch_handler",
1739 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
1740 )
1741 .field(
1742 "hooks_handler",
1743 &self.hooks_handler.as_ref().map(|_| "<set>"),
1744 )
1745 .field(
1746 "system_message_transform",
1747 &self.system_message_transform.as_ref().map(|_| "<set>"),
1748 )
1749 .finish()
1750 }
1751}
1752
1753impl Default for SessionConfig {
1754 fn default() -> Self {
1760 Self {
1761 session_id: None,
1762 model: None,
1763 client_name: None,
1764 reasoning_effort: None,
1765 reasoning_summary: None,
1766 context_tier: None,
1767 streaming: None,
1768 system_message: None,
1769 tools: None,
1770 canvases: None,
1771 canvas_handler: None,
1772 request_canvas_renderer: None,
1773 request_extensions: None,
1774 extension_sdk_path: None,
1775 extension_info: None,
1776 available_tools: None,
1777 excluded_tools: None,
1778 mcp_servers: None,
1779 mcp_oauth_token_storage: None,
1780 enable_config_discovery: None,
1781 skip_embedding_retrieval: None,
1782 organization_custom_instructions: None,
1783 enable_on_demand_instruction_discovery: None,
1784 enable_file_hooks: None,
1785 enable_host_git_operations: None,
1786 enable_session_store: None,
1787 enable_skills: None,
1788 embedding_cache_storage: None,
1789 enable_mcp_apps: None,
1790 skill_directories: None,
1791 instruction_directories: None,
1792 plugin_directories: None,
1793 large_output: None,
1794 disabled_skills: None,
1795 hooks: None,
1796 custom_agents: None,
1797 default_agent: None,
1798 agent: None,
1799 infinite_sessions: None,
1800 provider: None,
1801 providers: None,
1802 models: None,
1803 enable_session_telemetry: None,
1804 model_capabilities: None,
1805 memory: None,
1806 config_directory: None,
1807 working_directory: None,
1808 github_token: None,
1809 remote_session: None,
1810 cloud: None,
1811 include_sub_agent_streaming_events: None,
1812 commands: None,
1813 session_fs_provider: None,
1814 permission_handler: None,
1815 elicitation_handler: None,
1816 user_input_handler: None,
1817 exit_plan_mode_handler: None,
1818 auto_mode_switch_handler: None,
1819 hooks_handler: None,
1820 permission_policy: None,
1821 system_message_transform: None,
1822 skip_custom_instructions: None,
1823 custom_agents_local_only: None,
1824 coauthor_enabled: None,
1825 manage_schedule_enabled: None,
1826 }
1827 }
1828}
1829
1830pub(crate) struct SessionConfigRuntime {
1836 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
1837 pub permission_policy: Option<crate::permission::Policy>,
1838 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
1839 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
1840 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
1841 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
1842 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1843 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
1844 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
1845 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1846 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1847 pub commands: Option<Vec<CommandDefinition>>,
1848}
1849
1850impl SessionConfig {
1851 pub(crate) fn into_wire(
1863 mut self,
1864 session_id: Option<SessionId>,
1865 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
1866 let permission_active =
1867 self.permission_handler.is_some() || self.permission_policy.is_some();
1868 let request_user_input = self.user_input_handler.is_some();
1869 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
1870 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
1871 let request_elicitation = self.elicitation_handler.is_some();
1872 let hooks_flag = self.hooks_handler.is_some();
1873
1874 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
1875 if let Some(tools) = self.tools.as_mut() {
1876 for tool in tools.iter_mut() {
1877 if let Some(handler) = tool.handler.take()
1878 && tool_handlers.insert(tool.name.clone(), handler).is_some()
1879 {
1880 return Err(crate::Error::with_message(
1881 crate::ErrorKind::InvalidConfig,
1882 format!("duplicate tool handler registered for name {:?}", tool.name),
1883 ));
1884 }
1885 }
1886 }
1887
1888 let wire_commands = self.commands.as_ref().map(|cmds| {
1889 cmds.iter()
1890 .map(|c| crate::wire::CommandWireDefinition {
1891 name: c.name.clone(),
1892 description: c.description.clone(),
1893 })
1894 .collect()
1895 });
1896 let wire_canvases = self.canvases.clone();
1897 let canvas_handler = self.canvas_handler.clone();
1898
1899 let wire = crate::wire::SessionCreateWire {
1900 session_id,
1901 model: self.model,
1902 client_name: self.client_name,
1903 reasoning_effort: self.reasoning_effort,
1904 reasoning_summary: self.reasoning_summary,
1905 context_tier: self.context_tier,
1906 streaming: self.streaming,
1907 system_message: self.system_message,
1908 tools: self.tools,
1909 canvases: wire_canvases,
1910 request_canvas_renderer: self.request_canvas_renderer,
1911 request_extensions: self.request_extensions,
1912 extension_sdk_path: self.extension_sdk_path,
1913 extension_info: self.extension_info,
1914 available_tools: self.available_tools,
1915 excluded_tools: self.excluded_tools,
1916 tool_filter_precedence: "excluded",
1917 mcp_servers: self.mcp_servers,
1918 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
1919 embedding_cache_storage: self.embedding_cache_storage,
1920 env_value_mode: "direct",
1921 enable_config_discovery: self.enable_config_discovery,
1922 skip_embedding_retrieval: self.skip_embedding_retrieval,
1923 organization_custom_instructions: self.organization_custom_instructions,
1924 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
1925 enable_file_hooks: self.enable_file_hooks,
1926 enable_host_git_operations: self.enable_host_git_operations,
1927 enable_session_store: self.enable_session_store,
1928 enable_skills: self.enable_skills,
1929 request_user_input,
1930 request_permission: permission_active,
1931 request_exit_plan_mode,
1932 request_auto_mode_switch,
1933 request_elicitation,
1934 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
1935 hooks: hooks_flag,
1936 skill_directories: self.skill_directories,
1937 instruction_directories: self.instruction_directories,
1938 plugin_directories: self.plugin_directories,
1939 large_output: self.large_output,
1940 disabled_skills: self.disabled_skills,
1941 custom_agents: self.custom_agents,
1942 default_agent: self.default_agent,
1943 agent: self.agent,
1944 infinite_sessions: self.infinite_sessions,
1945 provider: self.provider,
1946 providers: self.providers,
1947 models: self.models,
1948 enable_session_telemetry: self.enable_session_telemetry,
1949 model_capabilities: self.model_capabilities,
1950 memory: self.memory,
1951 config_dir: self.config_directory,
1952 working_directory: self.working_directory,
1953 github_token: self.github_token,
1954 remote_session: self.remote_session,
1955 cloud: self.cloud,
1956 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
1957 commands: wire_commands,
1958 };
1959
1960 let runtime = SessionConfigRuntime {
1961 permission_handler: self.permission_handler,
1962 permission_policy: self.permission_policy,
1963 elicitation_handler: self.elicitation_handler,
1964 user_input_handler: self.user_input_handler,
1965 exit_plan_mode_handler: self.exit_plan_mode_handler,
1966 auto_mode_switch_handler: self.auto_mode_switch_handler,
1967 hooks_handler: self.hooks_handler,
1968 system_message_transform: self.system_message_transform,
1969 tool_handlers,
1970 canvas_handler,
1971 session_fs_provider: self.session_fs_provider,
1972 commands: self.commands,
1973 };
1974
1975 Ok((wire, runtime))
1976 }
1977
1978 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
1982 self.permission_handler = Some(handler);
1983 self
1984 }
1985
1986 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
1989 self.elicitation_handler = Some(handler);
1990 self
1991 }
1992
1993 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
1996 self.user_input_handler = Some(handler);
1997 self
1998 }
1999
2000 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2002 self.exit_plan_mode_handler = Some(handler);
2003 self
2004 }
2005
2006 pub fn with_auto_mode_switch_handler(
2008 mut self,
2009 handler: Arc<dyn AutoModeSwitchHandler>,
2010 ) -> Self {
2011 self.auto_mode_switch_handler = Some(handler);
2012 self
2013 }
2014
2015 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2020 self.commands = Some(commands);
2021 self
2022 }
2023
2024 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2028 self.session_fs_provider = Some(provider);
2029 self
2030 }
2031
2032 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2035 self.hooks_handler = Some(hooks);
2036 self
2037 }
2038
2039 pub fn with_system_message_transform(
2043 mut self,
2044 transform: Arc<dyn SystemMessageTransform>,
2045 ) -> Self {
2046 self.system_message_transform = Some(transform);
2047 self
2048 }
2049
2050 pub fn approve_all_permissions(mut self) -> Self {
2056 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2057 self
2058 }
2059
2060 pub fn deny_all_permissions(mut self) -> Self {
2063 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2064 self
2065 }
2066
2067 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2072 where
2073 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2074 {
2075 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2076 self
2077 }
2078
2079 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2081 self.session_id = Some(id.into());
2082 self
2083 }
2084
2085 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2087 self.model = Some(model.into());
2088 self
2089 }
2090
2091 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2093 self.client_name = Some(name.into());
2094 self
2095 }
2096
2097 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2099 self.reasoning_effort = Some(effort.into());
2100 self
2101 }
2102
2103 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2105 self.reasoning_summary = Some(summary);
2106 self
2107 }
2108
2109 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2111 self.context_tier = Some(tier.into());
2112 self
2113 }
2114
2115 pub fn with_streaming(mut self, streaming: bool) -> Self {
2117 self.streaming = Some(streaming);
2118 self
2119 }
2120
2121 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2123 self.system_message = Some(system_message);
2124 self
2125 }
2126
2127 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2129 self.tools = Some(tools.into_iter().collect());
2130 self
2131 }
2132
2133 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2138 self.canvases = Some(canvases.into_iter().collect());
2139 self
2140 }
2141
2142 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2144 self.canvas_handler = Some(handler);
2145 self
2146 }
2147
2148 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2150 self.request_canvas_renderer = Some(request);
2151 self
2152 }
2153
2154 pub fn with_request_extensions(mut self, request: bool) -> Self {
2156 self.request_extensions = Some(request);
2157 self
2158 }
2159
2160 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2164 self.extension_sdk_path = Some(path.into());
2165 self
2166 }
2167
2168 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2170 self.extension_info = Some(extension_info);
2171 self
2172 }
2173
2174 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2176 where
2177 I: IntoIterator<Item = S>,
2178 S: Into<String>,
2179 {
2180 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2181 self
2182 }
2183
2184 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2186 where
2187 I: IntoIterator<Item = S>,
2188 S: Into<String>,
2189 {
2190 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2191 self
2192 }
2193
2194 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
2196 self.mcp_servers = Some(servers);
2197 self
2198 }
2199
2200 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2208 self.mcp_oauth_token_storage = Some(mode.into());
2209 self
2210 }
2211
2212 pub fn with_embedding_cache_storage(
2214 mut self,
2215 embedding_cache_storage: impl Into<String>,
2216 ) -> Self {
2217 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2218 self
2219 }
2220
2221 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2223 self.enable_config_discovery = Some(enable);
2224 self
2225 }
2226
2227 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2229 self.skip_embedding_retrieval = Some(value);
2230 self
2231 }
2232
2233 pub fn with_organization_custom_instructions(
2235 mut self,
2236 instructions: impl Into<String>,
2237 ) -> Self {
2238 self.organization_custom_instructions = Some(instructions.into());
2239 self
2240 }
2241
2242 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2244 self.enable_on_demand_instruction_discovery = Some(value);
2245 self
2246 }
2247
2248 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2250 self.enable_file_hooks = Some(value);
2251 self
2252 }
2253
2254 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2256 self.enable_host_git_operations = Some(value);
2257 self
2258 }
2259
2260 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2262 self.enable_session_store = Some(value);
2263 self
2264 }
2265
2266 pub fn with_enable_skills(mut self, value: bool) -> Self {
2268 self.enable_skills = Some(value);
2269 self
2270 }
2271
2272 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2278 self.enable_mcp_apps = Some(enable);
2279 self
2280 }
2281
2282 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2284 where
2285 I: IntoIterator<Item = P>,
2286 P: Into<PathBuf>,
2287 {
2288 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2289 self
2290 }
2291
2292 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2296 where
2297 I: IntoIterator<Item = P>,
2298 P: Into<PathBuf>,
2299 {
2300 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2301 self
2302 }
2303
2304 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2306 where
2307 I: IntoIterator<Item = P>,
2308 P: Into<PathBuf>,
2309 {
2310 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2311 self
2312 }
2313
2314 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2316 self.large_output = Some(config);
2317 self
2318 }
2319
2320 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2322 where
2323 I: IntoIterator<Item = S>,
2324 S: Into<String>,
2325 {
2326 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2327 self
2328 }
2329
2330 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2332 mut self,
2333 agents: I,
2334 ) -> Self {
2335 self.custom_agents = Some(agents.into_iter().collect());
2336 self
2337 }
2338
2339 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2341 self.default_agent = Some(agent);
2342 self
2343 }
2344
2345 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2348 self.agent = Some(name.into());
2349 self
2350 }
2351
2352 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2355 self.infinite_sessions = Some(config);
2356 self
2357 }
2358
2359 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2361 self.provider = Some(provider);
2362 self
2363 }
2364
2365 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
2371 self.providers = Some(providers);
2372 self
2373 }
2374
2375 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
2381 self.models = Some(models);
2382 self
2383 }
2384
2385 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2389 self.enable_session_telemetry = Some(enable);
2390 self
2391 }
2392
2393 pub fn with_model_capabilities(
2395 mut self,
2396 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2397 ) -> Self {
2398 self.model_capabilities = Some(capabilities);
2399 self
2400 }
2401
2402 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
2404 self.memory = Some(memory);
2405 self
2406 }
2407
2408 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2410 self.config_directory = Some(dir.into());
2411 self
2412 }
2413
2414 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2417 self.working_directory = Some(dir.into());
2418 self
2419 }
2420
2421 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2426 self.github_token = Some(token.into());
2427 self
2428 }
2429
2430 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2433 self.include_sub_agent_streaming_events = Some(include);
2434 self
2435 }
2436
2437 pub fn with_remote_session(
2439 mut self,
2440 mode: crate::generated::api_types::RemoteSessionMode,
2441 ) -> Self {
2442 self.remote_session = Some(mode);
2443 self
2444 }
2445
2446 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
2448 self.cloud = Some(cloud);
2449 self
2450 }
2451
2452 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
2454 self.skip_custom_instructions = Some(value);
2455 self
2456 }
2457
2458 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
2460 self.custom_agents_local_only = Some(value);
2461 self
2462 }
2463
2464 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
2466 self.coauthor_enabled = Some(value);
2467 self
2468 }
2469
2470 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
2472 self.manage_schedule_enabled = Some(value);
2473 self
2474 }
2475}
2476
2477#[derive(Clone)]
2485#[non_exhaustive]
2486pub struct ResumeSessionConfig {
2487 pub session_id: SessionId,
2489 pub client_name: Option<String>,
2491 pub reasoning_effort: Option<String>,
2493 pub reasoning_summary: Option<ReasoningSummary>,
2497 pub context_tier: Option<String>,
2500 pub streaming: Option<bool>,
2502 pub system_message: Option<SystemMessageConfig>,
2505 pub tools: Option<Vec<Tool>>,
2507 pub canvases: Option<Vec<CanvasDeclaration>>,
2509 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2512 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
2514 pub request_canvas_renderer: Option<bool>,
2516 pub request_extensions: Option<bool>,
2518 pub extension_sdk_path: Option<String>,
2522 pub extension_info: Option<ExtensionInfo>,
2524 pub available_tools: Option<Vec<String>>,
2526 pub excluded_tools: Option<Vec<String>>,
2528 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
2530 pub mcp_oauth_token_storage: Option<String>,
2533 pub enable_config_discovery: Option<bool>,
2535 pub skip_embedding_retrieval: Option<bool>,
2537 pub embedding_cache_storage: Option<String>,
2539 pub organization_custom_instructions: Option<String>,
2541 pub enable_on_demand_instruction_discovery: Option<bool>,
2543 pub enable_file_hooks: Option<bool>,
2545 pub enable_host_git_operations: Option<bool>,
2547 pub enable_session_store: Option<bool>,
2549 pub enable_skills: Option<bool>,
2551 pub enable_mcp_apps: Option<bool>,
2557 pub skill_directories: Option<Vec<PathBuf>>,
2559 pub instruction_directories: Option<Vec<PathBuf>>,
2562 pub plugin_directories: Option<Vec<PathBuf>>,
2564 pub large_output: Option<LargeToolOutputConfig>,
2566 pub disabled_skills: Option<Vec<String>>,
2568 pub hooks: Option<bool>,
2570 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2572 pub default_agent: Option<DefaultAgentConfig>,
2574 pub agent: Option<String>,
2576 pub infinite_sessions: Option<InfiniteSessionConfig>,
2578 pub provider: Option<ProviderConfig>,
2580 pub providers: Option<Vec<NamedProviderConfig>>,
2586 pub models: Option<Vec<ProviderModelConfig>>,
2592 pub enable_session_telemetry: Option<bool>,
2600 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2602 pub memory: Option<MemoryConfiguration>,
2604 pub config_directory: Option<PathBuf>,
2606 pub working_directory: Option<PathBuf>,
2608 pub github_token: Option<String>,
2611 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2614 pub include_sub_agent_streaming_events: Option<bool>,
2616 pub commands: Option<Vec<CommandDefinition>>,
2620 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2625 pub suppress_resume_event: Option<bool>,
2628 pub continue_pending_work: Option<bool>,
2636 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2639 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2642 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2645 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2648 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2651 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2653 pub(crate) permission_policy: Option<crate::permission::Policy>,
2655 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2657 pub skip_custom_instructions: Option<bool>,
2659 pub custom_agents_local_only: Option<bool>,
2661 pub coauthor_enabled: Option<bool>,
2663 pub manage_schedule_enabled: Option<bool>,
2665}
2666
2667impl std::fmt::Debug for ResumeSessionConfig {
2668 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2669 f.debug_struct("ResumeSessionConfig")
2670 .field("session_id", &self.session_id)
2671 .field("client_name", &self.client_name)
2672 .field("reasoning_effort", &self.reasoning_effort)
2673 .field("reasoning_summary", &self.reasoning_summary)
2674 .field("context_tier", &self.context_tier)
2675 .field("streaming", &self.streaming)
2676 .field("system_message", &self.system_message)
2677 .field("tools", &self.tools)
2678 .field("canvases", &self.canvases)
2679 .field(
2680 "canvas_handler",
2681 &self.canvas_handler.as_ref().map(|_| "<set>"),
2682 )
2683 .field("open_canvases", &self.open_canvases)
2684 .field("request_canvas_renderer", &self.request_canvas_renderer)
2685 .field("request_extensions", &self.request_extensions)
2686 .field("extension_sdk_path", &self.extension_sdk_path)
2687 .field("extension_info", &self.extension_info)
2688 .field("available_tools", &self.available_tools)
2689 .field("excluded_tools", &self.excluded_tools)
2690 .field("mcp_servers", &self.mcp_servers)
2691 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2692 .field("embedding_cache_storage", &self.embedding_cache_storage)
2693 .field("enable_config_discovery", &self.enable_config_discovery)
2694 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2695 .field(
2696 "organization_custom_instructions",
2697 &self
2698 .organization_custom_instructions
2699 .as_ref()
2700 .map(|_| "<redacted>"),
2701 )
2702 .field(
2703 "enable_on_demand_instruction_discovery",
2704 &self.enable_on_demand_instruction_discovery,
2705 )
2706 .field("enable_file_hooks", &self.enable_file_hooks)
2707 .field(
2708 "enable_host_git_operations",
2709 &self.enable_host_git_operations,
2710 )
2711 .field("enable_session_store", &self.enable_session_store)
2712 .field("enable_skills", &self.enable_skills)
2713 .field("enable_mcp_apps", &self.enable_mcp_apps)
2714 .field("skill_directories", &self.skill_directories)
2715 .field("instruction_directories", &self.instruction_directories)
2716 .field("plugin_directories", &self.plugin_directories)
2717 .field("large_output", &self.large_output)
2718 .field("disabled_skills", &self.disabled_skills)
2719 .field("hooks", &self.hooks)
2720 .field("custom_agents", &self.custom_agents)
2721 .field("default_agent", &self.default_agent)
2722 .field("agent", &self.agent)
2723 .field("infinite_sessions", &self.infinite_sessions)
2724 .field("provider", &self.provider)
2725 .field("enable_session_telemetry", &self.enable_session_telemetry)
2726 .field("model_capabilities", &self.model_capabilities)
2727 .field("memory", &self.memory)
2728 .field("config_directory", &self.config_directory)
2729 .field("working_directory", &self.working_directory)
2730 .field(
2731 "github_token",
2732 &self.github_token.as_ref().map(|_| "<redacted>"),
2733 )
2734 .field("remote_session", &self.remote_session)
2735 .field(
2736 "include_sub_agent_streaming_events",
2737 &self.include_sub_agent_streaming_events,
2738 )
2739 .field("commands", &self.commands)
2740 .field(
2741 "session_fs_provider",
2742 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2743 )
2744 .field(
2745 "permission_handler",
2746 &self.permission_handler.as_ref().map(|_| "<set>"),
2747 )
2748 .field(
2749 "elicitation_handler",
2750 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2751 )
2752 .field(
2753 "user_input_handler",
2754 &self.user_input_handler.as_ref().map(|_| "<set>"),
2755 )
2756 .field(
2757 "exit_plan_mode_handler",
2758 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2759 )
2760 .field(
2761 "auto_mode_switch_handler",
2762 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2763 )
2764 .field(
2765 "hooks_handler",
2766 &self.hooks_handler.as_ref().map(|_| "<set>"),
2767 )
2768 .field(
2769 "system_message_transform",
2770 &self.system_message_transform.as_ref().map(|_| "<set>"),
2771 )
2772 .field("suppress_resume_event", &self.suppress_resume_event)
2773 .field("continue_pending_work", &self.continue_pending_work)
2774 .finish()
2775 }
2776}
2777
2778impl ResumeSessionConfig {
2779 pub(crate) fn into_wire(
2787 mut self,
2788 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
2789 let permission_active =
2790 self.permission_handler.is_some() || self.permission_policy.is_some();
2791 let request_user_input = self.user_input_handler.is_some();
2792 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2793 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2794 let request_elicitation = self.elicitation_handler.is_some();
2795 let hooks_flag = self.hooks_handler.is_some();
2796
2797 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2798 if let Some(tools) = self.tools.as_mut() {
2799 for tool in tools.iter_mut() {
2800 if let Some(handler) = tool.handler.take()
2801 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2802 {
2803 return Err(crate::Error::with_message(
2804 crate::ErrorKind::InvalidConfig,
2805 format!("duplicate tool handler registered for name {:?}", tool.name),
2806 ));
2807 }
2808 }
2809 }
2810
2811 let wire_commands = self.commands.as_ref().map(|cmds| {
2812 cmds.iter()
2813 .map(|c| crate::wire::CommandWireDefinition {
2814 name: c.name.clone(),
2815 description: c.description.clone(),
2816 })
2817 .collect()
2818 });
2819 let wire_canvases = self.canvases.clone();
2820 let canvas_handler = self.canvas_handler.clone();
2821
2822 let wire = crate::wire::SessionResumeWire {
2823 session_id: self.session_id,
2824 client_name: self.client_name,
2825 reasoning_effort: self.reasoning_effort,
2826 reasoning_summary: self.reasoning_summary,
2827 context_tier: self.context_tier,
2828 streaming: self.streaming,
2829 system_message: self.system_message,
2830 tools: self.tools,
2831 canvases: wire_canvases,
2832 open_canvases: self.open_canvases,
2833 request_canvas_renderer: self.request_canvas_renderer,
2834 request_extensions: self.request_extensions,
2835 extension_sdk_path: self.extension_sdk_path,
2836 extension_info: self.extension_info,
2837 available_tools: self.available_tools,
2838 excluded_tools: self.excluded_tools,
2839 tool_filter_precedence: "excluded",
2840 mcp_servers: self.mcp_servers,
2841 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2842 embedding_cache_storage: self.embedding_cache_storage,
2843 env_value_mode: "direct",
2844 enable_config_discovery: self.enable_config_discovery,
2845 skip_embedding_retrieval: self.skip_embedding_retrieval,
2846 organization_custom_instructions: self.organization_custom_instructions,
2847 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2848 enable_file_hooks: self.enable_file_hooks,
2849 enable_host_git_operations: self.enable_host_git_operations,
2850 enable_session_store: self.enable_session_store,
2851 enable_skills: self.enable_skills,
2852 request_user_input,
2853 request_permission: permission_active,
2854 request_exit_plan_mode,
2855 request_auto_mode_switch,
2856 request_elicitation,
2857 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2858 hooks: hooks_flag,
2859 skill_directories: self.skill_directories,
2860 instruction_directories: self.instruction_directories,
2861 plugin_directories: self.plugin_directories,
2862 large_output: self.large_output,
2863 disabled_skills: self.disabled_skills,
2864 custom_agents: self.custom_agents,
2865 default_agent: self.default_agent,
2866 agent: self.agent,
2867 infinite_sessions: self.infinite_sessions,
2868 provider: self.provider,
2869 providers: self.providers,
2870 models: self.models,
2871 enable_session_telemetry: self.enable_session_telemetry,
2872 model_capabilities: self.model_capabilities,
2873 memory: self.memory,
2874 config_dir: self.config_directory,
2875 working_directory: self.working_directory,
2876 github_token: self.github_token,
2877 remote_session: self.remote_session,
2878 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2879 commands: wire_commands,
2880 suppress_resume_event: self.suppress_resume_event,
2881 continue_pending_work: self.continue_pending_work,
2882 };
2883
2884 let runtime = SessionConfigRuntime {
2885 permission_handler: self.permission_handler,
2886 permission_policy: self.permission_policy,
2887 elicitation_handler: self.elicitation_handler,
2888 user_input_handler: self.user_input_handler,
2889 exit_plan_mode_handler: self.exit_plan_mode_handler,
2890 auto_mode_switch_handler: self.auto_mode_switch_handler,
2891 hooks_handler: self.hooks_handler,
2892 system_message_transform: self.system_message_transform,
2893 tool_handlers,
2894 canvas_handler,
2895 session_fs_provider: self.session_fs_provider,
2896 commands: self.commands,
2897 };
2898
2899 Ok((wire, runtime))
2900 }
2901
2902 pub fn new(session_id: SessionId) -> Self {
2907 Self {
2908 session_id,
2909 client_name: None,
2910 reasoning_effort: None,
2911 reasoning_summary: None,
2912 context_tier: None,
2913 streaming: None,
2914 system_message: None,
2915 tools: None,
2916 canvases: None,
2917 canvas_handler: None,
2918 open_canvases: None,
2919 request_canvas_renderer: None,
2920 request_extensions: None,
2921 extension_sdk_path: None,
2922 extension_info: None,
2923 available_tools: None,
2924 excluded_tools: None,
2925 mcp_servers: None,
2926 mcp_oauth_token_storage: None,
2927 enable_config_discovery: None,
2928 skip_embedding_retrieval: None,
2929 organization_custom_instructions: None,
2930 enable_on_demand_instruction_discovery: None,
2931 enable_file_hooks: None,
2932 enable_host_git_operations: None,
2933 enable_session_store: None,
2934 enable_skills: None,
2935 embedding_cache_storage: None,
2936 enable_mcp_apps: None,
2937 skill_directories: None,
2938 instruction_directories: None,
2939 plugin_directories: None,
2940 large_output: None,
2941 disabled_skills: None,
2942 hooks: None,
2943 custom_agents: None,
2944 default_agent: None,
2945 agent: None,
2946 infinite_sessions: None,
2947 provider: None,
2948 providers: None,
2949 models: None,
2950 enable_session_telemetry: None,
2951 model_capabilities: None,
2952 memory: None,
2953 config_directory: None,
2954 working_directory: None,
2955 github_token: None,
2956 remote_session: None,
2957 include_sub_agent_streaming_events: None,
2958 commands: None,
2959 session_fs_provider: None,
2960 suppress_resume_event: None,
2961 continue_pending_work: None,
2962 permission_handler: None,
2963 elicitation_handler: None,
2964 user_input_handler: None,
2965 exit_plan_mode_handler: None,
2966 auto_mode_switch_handler: None,
2967 hooks_handler: None,
2968 permission_policy: None,
2969 system_message_transform: None,
2970 skip_custom_instructions: None,
2971 custom_agents_local_only: None,
2972 coauthor_enabled: None,
2973 manage_schedule_enabled: None,
2974 }
2975 }
2976
2977 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2979 self.permission_handler = Some(handler);
2980 self
2981 }
2982
2983 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2985 self.elicitation_handler = Some(handler);
2986 self
2987 }
2988
2989 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2991 self.user_input_handler = Some(handler);
2992 self
2993 }
2994
2995 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2997 self.exit_plan_mode_handler = Some(handler);
2998 self
2999 }
3000
3001 pub fn with_auto_mode_switch_handler(
3003 mut self,
3004 handler: Arc<dyn AutoModeSwitchHandler>,
3005 ) -> Self {
3006 self.auto_mode_switch_handler = Some(handler);
3007 self
3008 }
3009
3010 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3013 self.hooks_handler = Some(hooks);
3014 self
3015 }
3016
3017 pub fn with_system_message_transform(
3019 mut self,
3020 transform: Arc<dyn SystemMessageTransform>,
3021 ) -> Self {
3022 self.system_message_transform = Some(transform);
3023 self
3024 }
3025
3026 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3030 self.commands = Some(commands);
3031 self
3032 }
3033
3034 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3037 self.session_fs_provider = Some(provider);
3038 self
3039 }
3040
3041 pub fn approve_all_permissions(mut self) -> Self {
3044 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3045 self
3046 }
3047
3048 pub fn deny_all_permissions(mut self) -> Self {
3051 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3052 self
3053 }
3054
3055 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3058 where
3059 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3060 {
3061 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3062 self
3063 }
3064
3065 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3067 self.client_name = Some(name.into());
3068 self
3069 }
3070
3071 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3073 self.reasoning_effort = Some(effort.into());
3074 self
3075 }
3076
3077 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3079 self.reasoning_summary = Some(summary);
3080 self
3081 }
3082
3083 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3086 self.context_tier = Some(tier.into());
3087 self
3088 }
3089
3090 pub fn with_streaming(mut self, streaming: bool) -> Self {
3092 self.streaming = Some(streaming);
3093 self
3094 }
3095
3096 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3099 self.system_message = Some(system_message);
3100 self
3101 }
3102
3103 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3105 self.tools = Some(tools.into_iter().collect());
3106 self
3107 }
3108
3109 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3111 self.canvases = Some(canvases.into_iter().collect());
3112 self
3113 }
3114
3115 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
3117 self.canvas_handler = Some(handler);
3118 self
3119 }
3120
3121 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
3123 mut self,
3124 open_canvases: I,
3125 ) -> Self {
3126 self.open_canvases = Some(open_canvases.into_iter().collect());
3127 self
3128 }
3129
3130 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
3132 self.request_canvas_renderer = Some(request);
3133 self
3134 }
3135
3136 pub fn with_request_extensions(mut self, request: bool) -> Self {
3138 self.request_extensions = Some(request);
3139 self
3140 }
3141
3142 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
3146 self.extension_sdk_path = Some(path.into());
3147 self
3148 }
3149
3150 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
3152 self.extension_info = Some(extension_info);
3153 self
3154 }
3155
3156 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
3158 where
3159 I: IntoIterator<Item = S>,
3160 S: Into<String>,
3161 {
3162 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
3163 self
3164 }
3165
3166 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
3168 where
3169 I: IntoIterator<Item = S>,
3170 S: Into<String>,
3171 {
3172 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
3173 self
3174 }
3175
3176 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
3178 self.mcp_servers = Some(servers);
3179 self
3180 }
3181
3182 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3185 self.mcp_oauth_token_storage = Some(mode.into());
3186 self
3187 }
3188
3189 pub fn with_embedding_cache_storage(
3191 mut self,
3192 embedding_cache_storage: impl Into<String>,
3193 ) -> Self {
3194 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3195 self
3196 }
3197
3198 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3200 self.enable_config_discovery = Some(enable);
3201 self
3202 }
3203
3204 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3206 self.skip_embedding_retrieval = Some(value);
3207 self
3208 }
3209
3210 pub fn with_organization_custom_instructions(
3212 mut self,
3213 instructions: impl Into<String>,
3214 ) -> Self {
3215 self.organization_custom_instructions = Some(instructions.into());
3216 self
3217 }
3218
3219 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3221 self.enable_on_demand_instruction_discovery = Some(value);
3222 self
3223 }
3224
3225 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3227 self.enable_file_hooks = Some(value);
3228 self
3229 }
3230
3231 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3233 self.enable_host_git_operations = Some(value);
3234 self
3235 }
3236
3237 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3239 self.enable_session_store = Some(value);
3240 self
3241 }
3242
3243 pub fn with_enable_skills(mut self, value: bool) -> Self {
3245 self.enable_skills = Some(value);
3246 self
3247 }
3248
3249 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3255 self.enable_mcp_apps = Some(enable);
3256 self
3257 }
3258
3259 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3261 where
3262 I: IntoIterator<Item = P>,
3263 P: Into<PathBuf>,
3264 {
3265 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3266 self
3267 }
3268
3269 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3273 where
3274 I: IntoIterator<Item = P>,
3275 P: Into<PathBuf>,
3276 {
3277 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3278 self
3279 }
3280
3281 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3283 where
3284 I: IntoIterator<Item = P>,
3285 P: Into<PathBuf>,
3286 {
3287 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3288 self
3289 }
3290
3291 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3293 self.large_output = Some(config);
3294 self
3295 }
3296
3297 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3299 where
3300 I: IntoIterator<Item = S>,
3301 S: Into<String>,
3302 {
3303 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3304 self
3305 }
3306
3307 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3309 mut self,
3310 agents: I,
3311 ) -> Self {
3312 self.custom_agents = Some(agents.into_iter().collect());
3313 self
3314 }
3315
3316 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3318 self.default_agent = Some(agent);
3319 self
3320 }
3321
3322 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3324 self.agent = Some(name.into());
3325 self
3326 }
3327
3328 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3330 self.infinite_sessions = Some(config);
3331 self
3332 }
3333
3334 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3336 self.provider = Some(provider);
3337 self
3338 }
3339
3340 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3346 self.providers = Some(providers);
3347 self
3348 }
3349
3350 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3356 self.models = Some(models);
3357 self
3358 }
3359
3360 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3364 self.enable_session_telemetry = Some(enable);
3365 self
3366 }
3367
3368 pub fn with_model_capabilities(
3370 mut self,
3371 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3372 ) -> Self {
3373 self.model_capabilities = Some(capabilities);
3374 self
3375 }
3376
3377 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3379 self.memory = Some(memory);
3380 self
3381 }
3382
3383 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3385 self.config_directory = Some(dir.into());
3386 self
3387 }
3388
3389 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3391 self.working_directory = Some(dir.into());
3392 self
3393 }
3394
3395 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3399 self.github_token = Some(token.into());
3400 self
3401 }
3402
3403 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3405 self.include_sub_agent_streaming_events = Some(include);
3406 self
3407 }
3408
3409 pub fn with_remote_session(
3411 mut self,
3412 mode: crate::generated::api_types::RemoteSessionMode,
3413 ) -> Self {
3414 self.remote_session = Some(mode);
3415 self
3416 }
3417
3418 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
3421 self.suppress_resume_event = Some(suppress);
3422 self
3423 }
3424
3425 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
3431 self.continue_pending_work = Some(continue_pending);
3432 self
3433 }
3434
3435 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3437 self.skip_custom_instructions = Some(value);
3438 self
3439 }
3440
3441 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3443 self.custom_agents_local_only = Some(value);
3444 self
3445 }
3446
3447 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3449 self.coauthor_enabled = Some(value);
3450 self
3451 }
3452
3453 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3455 self.manage_schedule_enabled = Some(value);
3456 self
3457 }
3458}
3459
3460#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3466#[serde(rename_all = "camelCase")]
3467#[non_exhaustive]
3468pub struct SystemMessageConfig {
3469 #[serde(skip_serializing_if = "Option::is_none")]
3471 pub mode: Option<String>,
3472 #[serde(skip_serializing_if = "Option::is_none")]
3474 pub content: Option<String>,
3475 #[serde(skip_serializing_if = "Option::is_none")]
3477 pub sections: Option<HashMap<String, SectionOverride>>,
3478}
3479
3480impl SystemMessageConfig {
3481 pub fn new() -> Self {
3484 Self::default()
3485 }
3486
3487 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
3490 self.mode = Some(mode.into());
3491 self
3492 }
3493
3494 pub fn with_content(mut self, content: impl Into<String>) -> Self {
3497 self.content = Some(content.into());
3498 self
3499 }
3500
3501 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
3503 self.sections = Some(sections);
3504 self
3505 }
3506}
3507
3508#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3514#[serde(rename_all = "camelCase")]
3515pub struct SectionOverride {
3516 #[serde(skip_serializing_if = "Option::is_none")]
3518 pub action: Option<String>,
3519 #[serde(skip_serializing_if = "Option::is_none")]
3521 pub content: Option<String>,
3522}
3523
3524#[derive(Debug, Clone, Serialize, Deserialize)]
3526#[serde(rename_all = "camelCase")]
3527pub struct CreateSessionResult {
3528 pub session_id: SessionId,
3530 #[serde(skip_serializing_if = "Option::is_none")]
3532 pub workspace_path: Option<PathBuf>,
3533 #[serde(default, alias = "remote_url")]
3535 pub remote_url: Option<String>,
3536 #[serde(skip_serializing_if = "Option::is_none")]
3538 pub capabilities: Option<SessionCapabilities>,
3539}
3540
3541#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3543#[serde(rename_all = "camelCase")]
3544pub(crate) struct ResumeSessionResult {
3545 #[serde(default)]
3547 pub session_id: Option<SessionId>,
3548 #[serde(default, skip_serializing_if = "Option::is_none")]
3550 pub workspace_path: Option<PathBuf>,
3551 #[serde(default, alias = "remote_url")]
3553 pub remote_url: Option<String>,
3554 #[serde(default, skip_serializing_if = "Option::is_none")]
3556 pub capabilities: Option<SessionCapabilities>,
3557 #[serde(
3559 default,
3560 alias = "openCanvasInstances",
3561 skip_serializing_if = "Option::is_none"
3562 )]
3563 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3564}
3565
3566#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3568#[serde(rename_all = "lowercase")]
3569pub enum LogLevel {
3570 #[default]
3572 Info,
3573 Warning,
3575 Error,
3577}
3578
3579#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
3584#[serde(rename_all = "camelCase")]
3585pub struct LogOptions {
3586 #[serde(skip_serializing_if = "Option::is_none")]
3588 pub level: Option<LogLevel>,
3589 #[serde(skip_serializing_if = "Option::is_none")]
3592 pub ephemeral: Option<bool>,
3593}
3594
3595impl LogOptions {
3596 pub fn with_level(mut self, level: LogLevel) -> Self {
3598 self.level = Some(level);
3599 self
3600 }
3601
3602 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
3604 self.ephemeral = Some(ephemeral);
3605 self
3606 }
3607}
3608
3609#[derive(Debug, Clone, Default)]
3613pub struct SetModelOptions {
3614 pub reasoning_effort: Option<String>,
3617 pub reasoning_summary: Option<ReasoningSummary>,
3621 pub context_tier: Option<ContextTier>,
3624 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3628}
3629
3630impl SetModelOptions {
3631 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3633 self.reasoning_effort = Some(effort.into());
3634 self
3635 }
3636
3637 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3639 self.reasoning_summary = Some(summary);
3640 self
3641 }
3642
3643 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
3645 self.context_tier = Some(tier);
3646 self
3647 }
3648
3649 pub fn with_model_capabilities(
3651 mut self,
3652 caps: crate::generated::api_types::ModelCapabilitiesOverride,
3653 ) -> Self {
3654 self.model_capabilities = Some(caps);
3655 self
3656 }
3657}
3658
3659#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
3666#[serde(rename_all = "camelCase")]
3667pub struct PingResponse {
3668 #[serde(default)]
3670 pub message: String,
3671 #[serde(default)]
3673 pub timestamp: String,
3674 #[serde(skip_serializing_if = "Option::is_none")]
3676 pub protocol_version: Option<u32>,
3677}
3678
3679#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3681#[serde(rename_all = "camelCase")]
3682pub struct AttachmentLineRange {
3683 pub start: u32,
3685 pub end: u32,
3687}
3688
3689#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3691#[serde(rename_all = "camelCase")]
3692pub struct AttachmentSelectionPosition {
3693 pub line: u32,
3695 pub character: u32,
3697}
3698
3699#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3701#[serde(rename_all = "camelCase")]
3702pub struct AttachmentSelectionRange {
3703 pub start: AttachmentSelectionPosition,
3705 pub end: AttachmentSelectionPosition,
3707}
3708
3709#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3711#[serde(rename_all = "snake_case")]
3712#[non_exhaustive]
3713pub enum GitHubReferenceType {
3714 Issue,
3716 Pr,
3718 Discussion,
3720}
3721
3722#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3724#[serde(
3725 tag = "type",
3726 rename_all = "camelCase",
3727 rename_all_fields = "camelCase"
3728)]
3729#[non_exhaustive]
3730pub enum Attachment {
3731 File {
3733 path: PathBuf,
3735 #[serde(skip_serializing_if = "Option::is_none")]
3737 display_name: Option<String>,
3738 #[serde(skip_serializing_if = "Option::is_none")]
3740 line_range: Option<AttachmentLineRange>,
3741 },
3742 Directory {
3744 path: PathBuf,
3746 #[serde(skip_serializing_if = "Option::is_none")]
3748 display_name: Option<String>,
3749 },
3750 Selection {
3752 file_path: PathBuf,
3754 text: String,
3756 #[serde(skip_serializing_if = "Option::is_none")]
3758 display_name: Option<String>,
3759 selection: AttachmentSelectionRange,
3761 },
3762 Blob {
3764 data: String,
3766 mime_type: String,
3768 #[serde(skip_serializing_if = "Option::is_none")]
3770 display_name: Option<String>,
3771 },
3772 #[serde(rename = "github_reference")]
3774 GitHubReference {
3775 number: u64,
3777 title: String,
3779 reference_type: GitHubReferenceType,
3781 state: String,
3783 url: String,
3785 },
3786}
3787
3788impl Attachment {
3789 pub fn display_name(&self) -> Option<&str> {
3791 match self {
3792 Self::File { display_name, .. }
3793 | Self::Directory { display_name, .. }
3794 | Self::Selection { display_name, .. }
3795 | Self::Blob { display_name, .. } => display_name.as_deref(),
3796 Self::GitHubReference { .. } => None,
3797 }
3798 }
3799
3800 pub fn label(&self) -> Option<String> {
3802 if let Some(display_name) = self
3803 .display_name()
3804 .map(str::trim)
3805 .filter(|name| !name.is_empty())
3806 {
3807 return Some(display_name.to_string());
3808 }
3809
3810 match self {
3811 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
3812 format!("#{}", number)
3813 } else {
3814 title.trim().to_string()
3815 }),
3816 _ => self.derived_display_name(),
3817 }
3818 }
3819
3820 pub fn ensure_display_name(&mut self) {
3822 if self
3823 .display_name()
3824 .map(str::trim)
3825 .is_some_and(|name| !name.is_empty())
3826 {
3827 return;
3828 }
3829
3830 let Some(derived_display_name) = self.derived_display_name() else {
3831 return;
3832 };
3833
3834 match self {
3835 Self::File { display_name, .. }
3836 | Self::Directory { display_name, .. }
3837 | Self::Selection { display_name, .. }
3838 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
3839 Self::GitHubReference { .. } => {}
3840 }
3841 }
3842
3843 fn derived_display_name(&self) -> Option<String> {
3844 match self {
3845 Self::File { path, .. } | Self::Directory { path, .. } => {
3846 Some(attachment_name_from_path(path))
3847 }
3848 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
3849 Self::Blob { .. } => Some("attachment".to_string()),
3850 Self::GitHubReference { .. } => None,
3851 }
3852 }
3853}
3854
3855fn attachment_name_from_path(path: &Path) -> String {
3856 path.file_name()
3857 .map(|name| name.to_string_lossy().into_owned())
3858 .filter(|name| !name.is_empty())
3859 .unwrap_or_else(|| {
3860 let full = path.to_string_lossy();
3861 if full.is_empty() {
3862 "attachment".to_string()
3863 } else {
3864 full.into_owned()
3865 }
3866 })
3867}
3868
3869pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
3871 for attachment in attachments {
3872 attachment.ensure_display_name();
3873 }
3874}
3875
3876#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3881#[serde(rename_all = "lowercase")]
3882#[non_exhaustive]
3883pub enum DeliveryMode {
3884 Enqueue,
3886 Immediate,
3888}
3889
3890#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3895#[serde(rename_all = "lowercase")]
3896#[non_exhaustive]
3897pub enum AgentMode {
3898 Interactive,
3900 Plan,
3902 Autopilot,
3904 Shell,
3906}
3907
3908#[derive(Debug, Clone)]
3937#[non_exhaustive]
3938pub struct MessageOptions {
3939 pub prompt: String,
3941 pub mode: Option<DeliveryMode>,
3947 pub agent_mode: Option<AgentMode>,
3951 pub attachments: Option<Vec<Attachment>>,
3953 pub wait_timeout: Option<Duration>,
3956 pub request_headers: Option<HashMap<String, String>>,
3960 pub traceparent: Option<String>,
3967 pub tracestate: Option<String>,
3971 pub display_prompt: Option<String>,
3973}
3974
3975impl MessageOptions {
3976 pub fn new(prompt: impl Into<String>) -> Self {
3978 Self {
3979 prompt: prompt.into(),
3980 mode: None,
3981 agent_mode: None,
3982 attachments: None,
3983 wait_timeout: None,
3984 request_headers: None,
3985 traceparent: None,
3986 tracestate: None,
3987 display_prompt: None,
3988 }
3989 }
3990
3991 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
3997 self.mode = Some(mode);
3998 self
3999 }
4000
4001 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
4005 self.agent_mode = Some(agent_mode);
4006 self
4007 }
4008
4009 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
4011 self.attachments = Some(attachments);
4012 self
4013 }
4014
4015 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
4017 self.wait_timeout = Some(timeout);
4018 self
4019 }
4020
4021 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
4023 self.request_headers = Some(headers);
4024 self
4025 }
4026
4027 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
4032 self.traceparent = ctx.traceparent;
4033 self.tracestate = ctx.tracestate;
4034 self
4035 }
4036
4037 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
4039 self.traceparent = Some(traceparent.into());
4040 self
4041 }
4042
4043 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
4045 self.tracestate = Some(tracestate.into());
4046 self
4047 }
4048
4049 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
4051 self.display_prompt = Some(display_prompt.into());
4052 self
4053 }
4054}
4055
4056impl From<&str> for MessageOptions {
4057 fn from(prompt: &str) -> Self {
4058 Self::new(prompt)
4059 }
4060}
4061
4062impl From<String> for MessageOptions {
4063 fn from(prompt: String) -> Self {
4064 Self::new(prompt)
4065 }
4066}
4067
4068impl From<&String> for MessageOptions {
4069 fn from(prompt: &String) -> Self {
4070 Self::new(prompt.clone())
4071 }
4072}
4073
4074#[derive(Debug, Clone, Serialize, Deserialize)]
4076#[serde(rename_all = "camelCase")]
4077#[non_exhaustive]
4078pub struct GetStatusResponse {
4079 pub version: String,
4081 pub protocol_version: u32,
4083}
4084
4085#[derive(Debug, Clone, Serialize, Deserialize)]
4087#[serde(rename_all = "camelCase")]
4088#[non_exhaustive]
4089pub struct GetAuthStatusResponse {
4090 pub is_authenticated: bool,
4092 #[serde(skip_serializing_if = "Option::is_none")]
4095 pub auth_type: Option<String>,
4096 #[serde(skip_serializing_if = "Option::is_none")]
4098 pub host: Option<String>,
4099 #[serde(skip_serializing_if = "Option::is_none")]
4101 pub login: Option<String>,
4102 #[serde(skip_serializing_if = "Option::is_none")]
4104 pub status_message: Option<String>,
4105}
4106
4107#[derive(Debug, Clone, Serialize, Deserialize)]
4111#[serde(rename_all = "camelCase")]
4112pub struct SessionEventNotification {
4113 pub session_id: SessionId,
4115 pub event: SessionEvent,
4117}
4118
4119#[derive(Debug, Clone, Serialize, Deserialize)]
4126#[serde(rename_all = "camelCase")]
4127pub struct SessionEvent {
4128 pub id: String,
4130 pub timestamp: String,
4132 pub parent_id: Option<String>,
4134 #[serde(skip_serializing_if = "Option::is_none")]
4136 pub ephemeral: Option<bool>,
4137 #[serde(skip_serializing_if = "Option::is_none")]
4140 pub agent_id: Option<String>,
4141 #[serde(skip_serializing_if = "Option::is_none")]
4143 pub debug_cli_received_at_ms: Option<i64>,
4144 #[serde(skip_serializing_if = "Option::is_none")]
4146 pub debug_ws_forwarded_at_ms: Option<i64>,
4147 #[serde(rename = "type")]
4149 pub event_type: String,
4150 pub data: Value,
4152}
4153
4154impl SessionEvent {
4155 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
4160 use serde::de::IntoDeserializer;
4161 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
4162 self.event_type.as_str().into_deserializer();
4163 crate::generated::SessionEventType::deserialize(deserializer)
4164 .unwrap_or(crate::generated::SessionEventType::Unknown)
4165 }
4166
4167 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
4173 serde_json::from_value(self.data.clone()).ok()
4174 }
4175
4176 pub fn is_transient_error(&self) -> bool {
4180 self.event_type == "session.error"
4181 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
4182 }
4183}
4184
4185#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4190#[serde(rename_all = "camelCase")]
4191#[non_exhaustive]
4192pub struct ToolInvocation {
4193 pub session_id: SessionId,
4195 pub tool_call_id: String,
4197 pub tool_name: String,
4199 pub arguments: Value,
4201 #[serde(default, skip_serializing_if = "Option::is_none")]
4206 pub traceparent: Option<String>,
4207 #[serde(default, skip_serializing_if = "Option::is_none")]
4210 pub tracestate: Option<String>,
4211}
4212
4213impl ToolInvocation {
4214 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
4235 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
4236 }
4237
4238 pub fn trace_context(&self) -> TraceContext {
4241 TraceContext {
4242 traceparent: self.traceparent.clone(),
4243 tracestate: self.tracestate.clone(),
4244 }
4245 }
4246}
4247
4248#[derive(Debug, Clone, Serialize, Deserialize)]
4250#[serde(rename_all = "camelCase")]
4251pub struct ToolBinaryResult {
4252 pub data: String,
4254 pub mime_type: String,
4256 pub r#type: String,
4258 #[serde(default, skip_serializing_if = "Option::is_none")]
4260 pub description: Option<String>,
4261}
4262
4263#[derive(Debug, Clone, Serialize, Deserialize)]
4265#[serde(rename_all = "camelCase")]
4266pub struct ToolResultExpanded {
4267 pub text_result_for_llm: String,
4269 pub result_type: String,
4271 #[serde(default, skip_serializing_if = "Option::is_none")]
4273 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
4274 #[serde(skip_serializing_if = "Option::is_none")]
4276 pub session_log: Option<String>,
4277 #[serde(skip_serializing_if = "Option::is_none")]
4279 pub error: Option<String>,
4280 #[serde(default, skip_serializing_if = "Option::is_none")]
4282 pub tool_telemetry: Option<HashMap<String, Value>>,
4283}
4284
4285#[derive(Debug, Clone, Serialize, Deserialize)]
4287#[serde(untagged)]
4288#[non_exhaustive]
4289pub enum ToolResult {
4290 Text(String),
4292 Expanded(ToolResultExpanded),
4294}
4295
4296#[derive(Debug, Clone, Serialize, Deserialize)]
4298#[serde(rename_all = "camelCase")]
4299pub struct ToolResultResponse {
4300 pub result: ToolResult,
4302}
4303
4304#[derive(Debug, Clone, Serialize, Deserialize)]
4306#[serde(rename_all = "camelCase")]
4307pub struct SessionMetadata {
4308 pub session_id: SessionId,
4310 pub start_time: String,
4312 pub modified_time: String,
4314 #[serde(skip_serializing_if = "Option::is_none")]
4316 pub summary: Option<String>,
4317 pub is_remote: bool,
4319}
4320
4321#[derive(Debug, Clone, Serialize, Deserialize)]
4323#[serde(rename_all = "camelCase")]
4324pub struct ListSessionsResponse {
4325 pub sessions: Vec<SessionMetadata>,
4327}
4328
4329#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4333#[serde(rename_all = "camelCase")]
4334pub struct SessionListFilter {
4335 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
4337 pub working_directory: Option<String>,
4338 #[serde(default, skip_serializing_if = "Option::is_none")]
4340 pub git_root: Option<String>,
4341 #[serde(default, skip_serializing_if = "Option::is_none")]
4343 pub repository: Option<String>,
4344 #[serde(default, skip_serializing_if = "Option::is_none")]
4346 pub branch: Option<String>,
4347}
4348
4349#[derive(Debug, Clone, Serialize, Deserialize)]
4351#[serde(rename_all = "camelCase")]
4352pub struct GetSessionMetadataResponse {
4353 #[serde(skip_serializing_if = "Option::is_none")]
4355 pub session: Option<SessionMetadata>,
4356}
4357
4358#[derive(Debug, Clone, Serialize, Deserialize)]
4360#[serde(rename_all = "camelCase")]
4361pub struct GetLastSessionIdResponse {
4362 #[serde(skip_serializing_if = "Option::is_none")]
4364 pub session_id: Option<SessionId>,
4365}
4366
4367#[derive(Debug, Clone, Serialize, Deserialize)]
4369#[serde(rename_all = "camelCase")]
4370pub struct GetForegroundSessionResponse {
4371 #[serde(skip_serializing_if = "Option::is_none")]
4373 pub session_id: Option<SessionId>,
4374}
4375
4376#[derive(Debug, Clone, Serialize, Deserialize)]
4378#[serde(rename_all = "camelCase")]
4379pub struct GetMessagesResponse {
4380 pub events: Vec<SessionEvent>,
4382}
4383
4384#[derive(Debug, Clone, Serialize, Deserialize)]
4386#[serde(rename_all = "camelCase")]
4387pub struct ElicitationResult {
4388 pub action: String,
4390 #[serde(skip_serializing_if = "Option::is_none")]
4392 pub content: Option<Value>,
4393}
4394
4395#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4401#[serde(rename_all = "camelCase")]
4402#[non_exhaustive]
4403pub enum ElicitationMode {
4404 Form,
4406 Url,
4408 #[serde(other)]
4410 Unknown,
4411}
4412
4413#[derive(Debug, Clone, Serialize, Deserialize)]
4420#[serde(rename_all = "camelCase")]
4421pub struct ElicitationRequest {
4422 pub message: String,
4424 #[serde(skip_serializing_if = "Option::is_none")]
4426 pub requested_schema: Option<Value>,
4427 #[serde(skip_serializing_if = "Option::is_none")]
4429 pub mode: Option<ElicitationMode>,
4430 #[serde(skip_serializing_if = "Option::is_none")]
4432 pub elicitation_source: Option<String>,
4433 #[serde(skip_serializing_if = "Option::is_none")]
4435 pub url: Option<String>,
4436}
4437
4438#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4443#[serde(rename_all = "camelCase")]
4444pub struct SessionCapabilities {
4445 #[serde(skip_serializing_if = "Option::is_none")]
4447 pub ui: Option<UiCapabilities>,
4448}
4449
4450#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4452#[serde(rename_all = "camelCase")]
4453pub struct UiCapabilities {
4454 #[serde(skip_serializing_if = "Option::is_none")]
4456 pub elicitation: Option<bool>,
4457 #[serde(skip_serializing_if = "Option::is_none")]
4468 pub mcp_apps: Option<bool>,
4469 #[serde(skip_serializing_if = "Option::is_none")]
4471 pub canvases: Option<bool>,
4472}
4473
4474#[derive(Debug, Clone, Default)]
4476pub struct UiInputOptions<'a> {
4477 pub title: Option<&'a str>,
4479 pub description: Option<&'a str>,
4481 pub min_length: Option<u64>,
4483 pub max_length: Option<u64>,
4485 pub format: Option<InputFormat>,
4487 pub default: Option<&'a str>,
4489}
4490
4491#[derive(Debug, Clone, Copy)]
4493#[non_exhaustive]
4494pub enum InputFormat {
4495 Email,
4497 Uri,
4499 Date,
4501 DateTime,
4503}
4504
4505impl InputFormat {
4506 pub fn as_str(&self) -> &'static str {
4508 match self {
4509 Self::Email => "email",
4510 Self::Uri => "uri",
4511 Self::Date => "date",
4512 Self::DateTime => "date-time",
4513 }
4514 }
4515}
4516
4517pub use crate::generated::api_types::{
4522 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
4523 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
4524 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
4525 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
4526};
4527
4528#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4534#[serde(rename_all = "kebab-case")]
4535#[non_exhaustive]
4536pub enum PermissionRequestKind {
4537 Shell,
4539 Write,
4541 Read,
4543 Url,
4545 Mcp,
4547 CustomTool,
4549 Memory,
4551 Hook,
4553 #[serde(other)]
4556 Unknown,
4557}
4558
4559#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4565#[serde(rename_all = "camelCase")]
4566pub struct PermissionRequestData {
4567 #[serde(default, skip_serializing_if = "Option::is_none")]
4571 pub kind: Option<PermissionRequestKind>,
4572 #[serde(default, skip_serializing_if = "Option::is_none")]
4575 pub tool_call_id: Option<String>,
4576 #[serde(flatten)]
4579 pub extra: Value,
4580}
4581
4582#[derive(Debug, Clone, Serialize, Deserialize)]
4584#[serde(rename_all = "camelCase")]
4585pub struct ExitPlanModeData {
4586 #[serde(default)]
4588 pub summary: String,
4589 #[serde(default, skip_serializing_if = "Option::is_none")]
4591 pub plan_content: Option<String>,
4592 #[serde(default)]
4594 pub actions: Vec<String>,
4595 #[serde(default = "default_recommended_action")]
4597 pub recommended_action: String,
4598}
4599
4600fn default_recommended_action() -> String {
4601 "autopilot".to_string()
4602}
4603
4604impl Default for ExitPlanModeData {
4605 fn default() -> Self {
4606 Self {
4607 summary: String::new(),
4608 plan_content: None,
4609 actions: Vec::new(),
4610 recommended_action: default_recommended_action(),
4611 }
4612 }
4613}
4614
4615#[cfg(test)]
4616mod tests {
4617 use std::path::PathBuf;
4618
4619 use serde_json::json;
4620
4621 use super::{
4622 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
4623 AttachmentSelectionRange, AzureProviderOptions, ConnectionState, CustomAgentConfig,
4624 DeliveryMode, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig,
4625 LargeToolOutputConfig, MemoryConfiguration, NamedProviderConfig, ProviderConfig,
4626 ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
4627 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
4628 ToolResultResponse, ensure_attachment_display_names,
4629 };
4630 use crate::generated::session_events::TypedSessionEvent;
4631
4632 #[test]
4633 fn tool_builder_composes() {
4634 let tool = Tool::new("greet")
4635 .with_description("Say hello")
4636 .with_namespaced_name("hello/greet")
4637 .with_instructions("Pass the user's name")
4638 .with_parameters(json!({
4639 "type": "object",
4640 "properties": { "name": { "type": "string" } },
4641 "required": ["name"]
4642 }))
4643 .with_overrides_built_in_tool(true)
4644 .with_skip_permission(true);
4645 assert_eq!(tool.name, "greet");
4646 assert_eq!(tool.description, "Say hello");
4647 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
4648 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
4649 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
4650 assert!(tool.overrides_built_in_tool);
4651 assert!(tool.skip_permission);
4652 }
4653
4654 #[test]
4655 fn tool_defer_serialization() {
4656 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
4657 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
4658 let value = serde_json::to_value(&tool).unwrap();
4659 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
4660
4661 let plain = Tool::new("plain");
4662 let value = serde_json::to_value(&plain).unwrap();
4663 assert!(value.get("defer").is_none());
4664 }
4665
4666 #[test]
4667 fn custom_agent_config_builder_with_model() {
4668 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
4669 .with_model("claude-haiku-4.5")
4670 .with_display_name("My Agent");
4671 assert_eq!(agent.name, "my-agent");
4672 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
4673 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
4674 }
4675
4676 #[test]
4677 fn custom_agent_config_serializes_model() {
4678 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
4679 let wire = serde_json::to_value(&agent).unwrap();
4680 assert_eq!(wire["model"], "claude-haiku-4.5");
4681 assert_eq!(wire["name"], "model-agent");
4682 }
4683
4684 #[test]
4685 fn custom_agent_config_omits_model_when_none() {
4686 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
4687 let wire = serde_json::to_value(&agent).unwrap();
4688 assert!(wire.get("model").is_none());
4689 }
4690
4691 #[test]
4692 #[should_panic(expected = "tool parameter schema must be a JSON object")]
4693 fn tool_with_parameters_panics_on_non_object_value() {
4694 let _ = Tool::new("noop").with_parameters(json!(null));
4695 }
4696
4697 #[test]
4698 fn tool_result_expanded_serializes_binary_results_for_llm() {
4699 let response = ToolResultResponse {
4700 result: ToolResult::Expanded(ToolResultExpanded {
4701 text_result_for_llm: "rendered chart".to_string(),
4702 result_type: "success".to_string(),
4703 binary_results_for_llm: Some(vec![ToolBinaryResult {
4704 data: "aW1n".to_string(),
4705 mime_type: "image/png".to_string(),
4706 r#type: "image".to_string(),
4707 description: Some("chart preview".to_string()),
4708 }]),
4709 session_log: None,
4710 error: None,
4711 tool_telemetry: None,
4712 }),
4713 };
4714
4715 let wire = serde_json::to_value(&response).unwrap();
4716
4717 assert_eq!(
4718 wire,
4719 json!({
4720 "result": {
4721 "textResultForLlm": "rendered chart",
4722 "resultType": "success",
4723 "binaryResultsForLlm": [
4724 {
4725 "data": "aW1n",
4726 "mimeType": "image/png",
4727 "type": "image",
4728 "description": "chart preview"
4729 }
4730 ]
4731 }
4732 })
4733 );
4734 }
4735
4736 #[test]
4737 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
4738 let response = ToolResultResponse {
4739 result: ToolResult::Expanded(ToolResultExpanded {
4740 text_result_for_llm: "ok".to_string(),
4741 result_type: "success".to_string(),
4742 binary_results_for_llm: None,
4743 session_log: None,
4744 error: None,
4745 tool_telemetry: None,
4746 }),
4747 };
4748
4749 let wire = serde_json::to_value(&response).unwrap();
4750
4751 assert_eq!(wire["result"]["textResultForLlm"], "ok");
4752 assert!(wire["result"].get("binaryResultsForLlm").is_none());
4753 }
4754
4755 #[test]
4756 fn session_config_default_wire_flags_off_without_handlers() {
4757 let cfg = SessionConfig::default();
4758 assert_eq!(cfg.mcp_oauth_token_storage, None);
4759 let (wire, _runtime) = cfg
4763 .into_wire(Some(SessionId::from("default-flags")))
4764 .expect("default config has no duplicate handlers");
4765 assert!(!wire.request_user_input);
4766 assert!(!wire.request_permission);
4767 assert!(!wire.request_elicitation);
4768 assert!(!wire.request_exit_plan_mode);
4769 assert!(!wire.request_auto_mode_switch);
4770 assert!(!wire.hooks);
4771 assert!(!wire.request_mcp_apps);
4772 }
4773
4774 #[test]
4775 fn resume_session_config_new_wire_flags_off_without_handlers() {
4776 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
4777 assert_eq!(cfg.mcp_oauth_token_storage, None);
4778 let (wire, _runtime) = cfg
4779 .into_wire()
4780 .expect("default resume config has no duplicate handlers");
4781 assert!(!wire.request_user_input);
4782 assert!(!wire.request_permission);
4783 assert!(!wire.request_elicitation);
4784 assert!(!wire.request_exit_plan_mode);
4785 assert!(!wire.request_auto_mode_switch);
4786 assert!(!wire.hooks);
4787 assert!(!wire.request_mcp_apps);
4788 }
4789
4790 #[test]
4791 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
4792 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
4793 assert_eq!(cfg.enable_mcp_apps, Some(true));
4794
4795 let (wire, _runtime) = cfg
4796 .into_wire(Some(SessionId::from("enable-mcp-apps")))
4797 .expect("enable_mcp_apps config has no duplicate handlers");
4798 assert!(wire.request_mcp_apps);
4799
4800 let json = serde_json::to_value(&wire).unwrap();
4801 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
4802 }
4803
4804 #[test]
4805 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
4806 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
4807 .with_enable_mcp_apps(true);
4808 assert_eq!(cfg.enable_mcp_apps, Some(true));
4809
4810 let (wire, _runtime) = cfg
4811 .into_wire()
4812 .expect("resume enable_mcp_apps config has no duplicate handlers");
4813 assert!(wire.request_mcp_apps);
4814
4815 let json = serde_json::to_value(&wire).unwrap();
4816 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
4817 }
4818
4819 #[test]
4820 fn memory_configuration_constructors_and_serde() {
4821 assert!(MemoryConfiguration::enabled().enabled);
4822 assert!(!MemoryConfiguration::disabled().enabled);
4823 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
4824
4825 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
4826 assert_eq!(json, serde_json::json!({ "enabled": true }));
4827 }
4828
4829 #[test]
4830 fn session_config_with_memory_serializes() {
4831 let (wire, _runtime) = SessionConfig::default()
4832 .with_memory(MemoryConfiguration::enabled())
4833 .into_wire(Some(SessionId::from("memory-on")))
4834 .expect("no duplicate handlers");
4835 let json = serde_json::to_value(&wire).unwrap();
4836 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
4837
4838 let (wire_off, _) = SessionConfig::default()
4839 .with_memory(MemoryConfiguration::disabled())
4840 .into_wire(Some(SessionId::from("memory-off")))
4841 .expect("no duplicate handlers");
4842 let json_off = serde_json::to_value(&wire_off).unwrap();
4843 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
4844
4845 let (empty_wire, _) = SessionConfig::default()
4847 .into_wire(Some(SessionId::from("memory-unset")))
4848 .expect("no duplicate handlers");
4849 let empty_json = serde_json::to_value(&empty_wire).unwrap();
4850 assert!(empty_json.get("memory").is_none());
4851 }
4852
4853 #[test]
4854 fn resume_session_config_with_memory_serializes() {
4855 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
4856 .with_memory(MemoryConfiguration::enabled())
4857 .into_wire()
4858 .expect("no duplicate handlers");
4859 let json = serde_json::to_value(&wire).unwrap();
4860 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
4861
4862 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
4864 .into_wire()
4865 .expect("no duplicate handlers");
4866 let empty_json = serde_json::to_value(&empty_wire).unwrap();
4867 assert!(empty_json.get("memory").is_none());
4868 }
4869
4870 #[test]
4871 #[allow(clippy::field_reassign_with_default)]
4872 fn session_config_into_wire_serializes_bucket_b_fields() {
4873 use std::path::PathBuf;
4874
4875 use super::{CloudSessionOptions, CloudSessionRepository};
4876
4877 let mut cfg = SessionConfig::default();
4878 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
4879 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
4880 cfg.github_token = Some("ghs_secret".to_string());
4881 cfg.include_sub_agent_streaming_events = Some(false);
4882 cfg.enable_session_telemetry = Some(false);
4883 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
4884 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
4885 cfg.enable_on_demand_instruction_discovery = Some(false);
4886 cfg.cloud = Some(CloudSessionOptions::with_repository(
4887 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
4888 ));
4889
4890 let (wire, _runtime) = cfg
4891 .into_wire(Some(SessionId::from("custom-id")))
4892 .expect("no duplicate handlers");
4893 let wire_json = serde_json::to_value(&wire).unwrap();
4894 assert_eq!(wire_json["sessionId"], "custom-id");
4895 assert_eq!(wire_json["configDir"], "/tmp/cfg");
4896 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
4897 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
4898 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
4899 assert_eq!(wire_json["enableSessionTelemetry"], false);
4900 assert_eq!(wire_json["reasoningSummary"], "concise");
4901 assert_eq!(wire_json["remoteSession"], "export");
4902 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
4903 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
4904 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
4905 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
4906
4907 let (empty_wire, _) = SessionConfig::default()
4909 .into_wire(Some(SessionId::from("empty")))
4910 .expect("default has no duplicate handlers");
4911 let empty_json = serde_json::to_value(&empty_wire).unwrap();
4912 assert!(empty_json.get("gitHubToken").is_none());
4913 assert!(empty_json.get("enableSessionTelemetry").is_none());
4914 assert!(empty_json.get("reasoningSummary").is_none());
4915 assert!(empty_json.get("remoteSession").is_none());
4916 assert!(
4917 empty_json
4918 .get("enableOnDemandInstructionDiscovery")
4919 .is_none()
4920 );
4921 assert!(empty_json.get("cloud").is_none());
4922 }
4923
4924 #[test]
4925 fn session_config_into_wire_serializes_named_providers_and_models() {
4926 let cfg = SessionConfig::default()
4927 .with_providers(vec![
4928 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
4929 .with_provider_type("openai")
4930 .with_wire_api("responses")
4931 .with_api_key("sk-test"),
4932 ])
4933 .with_models(vec![
4934 ProviderModelConfig::new("gpt-x", "my-openai")
4935 .with_wire_model("gpt-x-2025")
4936 .with_max_output_tokens(2048),
4937 ]);
4938
4939 let (wire, _) = cfg
4940 .into_wire(Some(SessionId::from("sess-providers")))
4941 .expect("no duplicate handlers");
4942 let wire_json = serde_json::to_value(&wire).unwrap();
4943 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
4944 assert_eq!(
4945 wire_json["providers"][0]["baseUrl"],
4946 "https://api.example.com/v1"
4947 );
4948 assert_eq!(wire_json["providers"][0]["type"], "openai");
4949 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
4950 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
4951 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
4952 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
4953 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
4954 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
4955
4956 let (empty_wire, _) = SessionConfig::default()
4957 .into_wire(Some(SessionId::from("empty")))
4958 .expect("default has no duplicate handlers");
4959 let empty_json = serde_json::to_value(&empty_wire).unwrap();
4960 assert!(empty_json.get("providers").is_none());
4961 assert!(empty_json.get("models").is_none());
4962 }
4963
4964 #[test]
4965 fn resume_config_into_wire_serializes_named_providers_and_models() {
4966 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
4967 .with_providers(vec![
4968 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
4969 .with_provider_type("azure")
4970 .with_azure(AzureProviderOptions {
4971 api_version: Some("2024-10-21".to_string()),
4972 }),
4973 ])
4974 .with_models(vec![
4975 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
4976 ]);
4977
4978 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
4979 let wire_json = serde_json::to_value(&wire).unwrap();
4980 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
4981 assert_eq!(wire_json["providers"][0]["type"], "azure");
4982 assert_eq!(
4983 wire_json["providers"][0]["azure"]["apiVersion"],
4984 "2024-10-21"
4985 );
4986 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
4987 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
4988 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
4989
4990 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
4991 .into_wire()
4992 .expect("default has no duplicate handlers");
4993 let empty_json = serde_json::to_value(&empty_wire).unwrap();
4994 assert!(empty_json.get("providers").is_none());
4995 assert!(empty_json.get("models").is_none());
4996 }
4997
4998 #[test]
4999 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
5000 use std::path::PathBuf;
5001
5002 let cfg = SessionConfig {
5003 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
5004 large_output: Some(
5005 LargeToolOutputConfig::new()
5006 .with_enabled(true)
5007 .with_max_size_bytes(1024)
5008 .with_output_directory(PathBuf::from("/tmp/large-output")),
5009 ),
5010 ..Default::default()
5011 };
5012
5013 let (wire, _) = cfg
5014 .into_wire(Some(SessionId::from("sess-1")))
5015 .expect("no duplicate handlers");
5016 let wire_json = serde_json::to_value(&wire).unwrap();
5017 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
5018 assert_eq!(wire_json["largeOutput"]["enabled"], true);
5019 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
5020 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
5021
5022 let (empty_wire, _) = SessionConfig::default()
5023 .into_wire(Some(SessionId::from("empty")))
5024 .expect("default has no duplicate handlers");
5025 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5026 assert!(empty_json.get("pluginDirectories").is_none());
5027 assert!(empty_json.get("largeOutput").is_none());
5028 }
5029
5030 #[test]
5031 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
5032 use std::path::PathBuf;
5033
5034 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5035 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5036 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5037 cfg.github_token = Some("ghs_secret".to_string());
5038 cfg.include_sub_agent_streaming_events = Some(true);
5039 cfg.enable_session_telemetry = Some(false);
5040 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
5041 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
5042 cfg.enable_on_demand_instruction_discovery = Some(false);
5043
5044 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5045 let wire_json = serde_json::to_value(&wire).unwrap();
5046 assert_eq!(wire_json["sessionId"], "sess-1");
5047 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5048 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5049 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5050 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
5051 assert_eq!(wire_json["enableSessionTelemetry"], false);
5052 assert_eq!(wire_json["reasoningSummary"], "detailed");
5053 assert_eq!(wire_json["remoteSession"], "on");
5054 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5055
5056 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5058 .into_wire()
5059 .expect("default resume has no duplicate handlers");
5060 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5061 assert!(empty_json.get("reasoningSummary").is_none());
5062 assert!(empty_json.get("remoteSession").is_none());
5063 assert!(
5064 empty_json
5065 .get("enableOnDemandInstructionDiscovery")
5066 .is_none()
5067 );
5068 }
5069
5070 #[test]
5071 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
5072 use std::path::PathBuf;
5073
5074 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5075 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
5076 cfg.large_output = Some(
5077 LargeToolOutputConfig::new()
5078 .with_enabled(false)
5079 .with_max_size_bytes(2048)
5080 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
5081 );
5082
5083 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5084 let wire_json = serde_json::to_value(&wire).unwrap();
5085 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
5086 assert_eq!(wire_json["largeOutput"]["enabled"], false);
5087 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
5088 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
5089
5090 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5091 .into_wire()
5092 .expect("default resume has no duplicate handlers");
5093 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5094 assert!(empty_json.get("pluginDirectories").is_none());
5095 assert!(empty_json.get("largeOutput").is_none());
5096 }
5097
5098 #[test]
5099 fn session_config_builder_composes() {
5100 use std::collections::HashMap;
5101
5102 let cfg = SessionConfig::default()
5103 .with_session_id(SessionId::from("sess-1"))
5104 .with_model("claude-sonnet-4")
5105 .with_client_name("test-app")
5106 .with_reasoning_effort("medium")
5107 .with_reasoning_summary(ReasoningSummary::Concise)
5108 .with_context_tier("long_context")
5109 .with_streaming(true)
5110 .with_tools([Tool::new("greet")])
5111 .with_available_tools(["bash", "view"])
5112 .with_excluded_tools(["dangerous"])
5113 .with_mcp_servers(HashMap::new())
5114 .with_mcp_oauth_token_storage("persistent")
5115 .with_enable_config_discovery(true)
5116 .with_enable_on_demand_instruction_discovery(true)
5117 .with_skill_directories([PathBuf::from("/tmp/skills")])
5118 .with_disabled_skills(["broken-skill"])
5119 .with_agent("researcher")
5120 .with_config_directory(PathBuf::from("/tmp/config"))
5121 .with_working_directory(PathBuf::from("/tmp/work"))
5122 .with_github_token("ghp_test")
5123 .with_enable_session_telemetry(false)
5124 .with_include_sub_agent_streaming_events(false)
5125 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5126
5127 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
5128 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
5129 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5130 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
5131 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
5132 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
5133 assert_eq!(cfg.streaming, Some(true));
5134 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5135 assert_eq!(
5136 cfg.available_tools.as_deref(),
5137 Some(&["bash".to_string(), "view".to_string()][..])
5138 );
5139 assert_eq!(
5140 cfg.excluded_tools.as_deref(),
5141 Some(&["dangerous".to_string()][..])
5142 );
5143 assert!(cfg.mcp_servers.is_some());
5144 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5145 assert_eq!(cfg.enable_config_discovery, Some(true));
5146 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
5147 assert_eq!(
5148 cfg.skill_directories.as_deref(),
5149 Some(&[PathBuf::from("/tmp/skills")][..])
5150 );
5151 assert_eq!(
5152 cfg.disabled_skills.as_deref(),
5153 Some(&["broken-skill".to_string()][..])
5154 );
5155 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5156 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5157 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5158 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5159 assert_eq!(cfg.enable_session_telemetry, Some(false));
5160 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
5161 assert_eq!(
5162 cfg.extension_info,
5163 Some(ExtensionInfo::new("github-app", "counter"))
5164 );
5165 }
5166
5167 #[test]
5168 fn resume_session_config_builder_composes() {
5169 use std::collections::HashMap;
5170
5171 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
5172 .with_client_name("test-app")
5173 .with_reasoning_summary(ReasoningSummary::None)
5174 .with_context_tier("default")
5175 .with_streaming(true)
5176 .with_tools([Tool::new("greet")])
5177 .with_available_tools(["bash", "view"])
5178 .with_excluded_tools(["dangerous"])
5179 .with_mcp_servers(HashMap::new())
5180 .with_mcp_oauth_token_storage("persistent")
5181 .with_enable_config_discovery(true)
5182 .with_enable_on_demand_instruction_discovery(false)
5183 .with_skill_directories([PathBuf::from("/tmp/skills")])
5184 .with_disabled_skills(["broken-skill"])
5185 .with_agent("researcher")
5186 .with_config_directory(PathBuf::from("/tmp/config"))
5187 .with_working_directory(PathBuf::from("/tmp/work"))
5188 .with_github_token("ghp_test")
5189 .with_enable_session_telemetry(false)
5190 .with_include_sub_agent_streaming_events(true)
5191 .with_suppress_resume_event(true)
5192 .with_continue_pending_work(true)
5193 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5194
5195 assert_eq!(cfg.session_id.as_str(), "sess-2");
5196 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5197 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
5198 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
5199 assert_eq!(cfg.streaming, Some(true));
5200 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5201 assert_eq!(
5202 cfg.available_tools.as_deref(),
5203 Some(&["bash".to_string(), "view".to_string()][..])
5204 );
5205 assert_eq!(
5206 cfg.excluded_tools.as_deref(),
5207 Some(&["dangerous".to_string()][..])
5208 );
5209 assert!(cfg.mcp_servers.is_some());
5210 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5211 assert_eq!(cfg.enable_config_discovery, Some(true));
5212 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
5213 assert_eq!(
5214 cfg.skill_directories.as_deref(),
5215 Some(&[PathBuf::from("/tmp/skills")][..])
5216 );
5217 assert_eq!(
5218 cfg.disabled_skills.as_deref(),
5219 Some(&["broken-skill".to_string()][..])
5220 );
5221 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5222 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5223 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5224 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5225 assert_eq!(cfg.enable_session_telemetry, Some(false));
5226 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
5227 assert_eq!(cfg.suppress_resume_event, Some(true));
5228 assert_eq!(cfg.continue_pending_work, Some(true));
5229 assert_eq!(
5230 cfg.extension_info,
5231 Some(ExtensionInfo::new("github-app", "counter"))
5232 );
5233 }
5234
5235 #[test]
5239 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
5240 let cfg =
5241 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
5242 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5243 let json = serde_json::to_value(&wire).unwrap();
5244 assert_eq!(json["continuePendingWork"], true);
5245
5246 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5248 .into_wire()
5249 .expect("no duplicate handlers");
5250 let json = serde_json::to_value(&wire).unwrap();
5251 assert!(json.get("continuePendingWork").is_none());
5252 }
5253
5254 #[test]
5258 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
5259 let cfg =
5260 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
5261 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5262 let json = serde_json::to_value(&wire).unwrap();
5263 assert_eq!(json["disableResume"], true);
5264 assert!(json.get("suppressResumeEvent").is_none());
5265 }
5266
5267 #[test]
5270 fn session_config_serializes_instruction_directories_to_camel_case() {
5271 let cfg =
5272 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
5273 let (wire, _) = cfg
5274 .into_wire(Some(SessionId::from("instr-on")))
5275 .expect("no duplicate handlers");
5276 let json = serde_json::to_value(&wire).unwrap();
5277 assert_eq!(
5278 json["instructionDirectories"],
5279 serde_json::json!(["/tmp/instr"])
5280 );
5281
5282 let (wire, _) = SessionConfig::default()
5284 .into_wire(Some(SessionId::from("instr-off")))
5285 .expect("no duplicate handlers");
5286 let json = serde_json::to_value(&wire).unwrap();
5287 assert!(json.get("instructionDirectories").is_none());
5288 }
5289
5290 #[test]
5293 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
5294 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
5295 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
5296 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5297 let json = serde_json::to_value(&wire).unwrap();
5298 assert_eq!(
5299 json["instructionDirectories"],
5300 serde_json::json!(["/tmp/instr"])
5301 );
5302
5303 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5304 .into_wire()
5305 .expect("no duplicate handlers");
5306 let json = serde_json::to_value(&wire).unwrap();
5307 assert!(json.get("instructionDirectories").is_none());
5308 }
5309
5310 #[test]
5311 fn custom_agent_config_builder_composes() {
5312 use std::collections::HashMap;
5313
5314 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
5315 .with_display_name("Research Assistant")
5316 .with_description("Investigates technical questions.")
5317 .with_tools(["bash", "view"])
5318 .with_mcp_servers(HashMap::new())
5319 .with_infer(true)
5320 .with_skills(["rust-coding-skill"]);
5321
5322 assert_eq!(cfg.name, "researcher");
5323 assert_eq!(cfg.prompt, "You are a research assistant.");
5324 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
5325 assert_eq!(
5326 cfg.description.as_deref(),
5327 Some("Investigates technical questions.")
5328 );
5329 assert_eq!(
5330 cfg.tools.as_deref(),
5331 Some(&["bash".to_string(), "view".to_string()][..])
5332 );
5333 assert!(cfg.mcp_servers.is_some());
5334 assert_eq!(cfg.infer, Some(true));
5335 assert_eq!(
5336 cfg.skills.as_deref(),
5337 Some(&["rust-coding-skill".to_string()][..])
5338 );
5339 }
5340
5341 #[test]
5342 fn infinite_session_config_builder_composes() {
5343 let cfg = InfiniteSessionConfig::new()
5344 .with_enabled(true)
5345 .with_background_compaction_threshold(0.75)
5346 .with_buffer_exhaustion_threshold(0.92);
5347
5348 assert_eq!(cfg.enabled, Some(true));
5349 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
5350 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
5351 }
5352
5353 #[test]
5354 fn provider_config_builder_composes() {
5355 use std::collections::HashMap;
5356
5357 let mut headers = HashMap::new();
5358 headers.insert("X-Custom".to_string(), "value".to_string());
5359
5360 let cfg = ProviderConfig::new("https://api.example.com")
5361 .with_provider_type("openai")
5362 .with_wire_api("completions")
5363 .with_api_key("sk-test")
5364 .with_bearer_token("bearer-test")
5365 .with_headers(headers)
5366 .with_model_id("gpt-4")
5367 .with_wire_model("azure-gpt-4-deployment")
5368 .with_max_prompt_tokens(8192)
5369 .with_max_output_tokens(2048);
5370
5371 assert_eq!(cfg.base_url, "https://api.example.com");
5372 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
5373 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
5374 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
5375 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
5376 assert_eq!(
5377 cfg.headers
5378 .as_ref()
5379 .and_then(|h| h.get("X-Custom"))
5380 .map(String::as_str),
5381 Some("value"),
5382 );
5383 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
5384 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
5385 assert_eq!(cfg.max_prompt_tokens, Some(8192));
5386 assert_eq!(cfg.max_output_tokens, Some(2048));
5387
5388 let wire = serde_json::to_value(&cfg).unwrap();
5390 assert_eq!(wire["modelId"], "gpt-4");
5391 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
5392 assert_eq!(wire["maxPromptTokens"], 8192);
5393 assert_eq!(wire["maxOutputTokens"], 2048);
5394
5395 let unset = ProviderConfig::new("https://api.example.com");
5396 let wire_unset = serde_json::to_value(&unset).unwrap();
5397 assert!(wire_unset.get("modelId").is_none());
5398 assert!(wire_unset.get("wireModel").is_none());
5399 assert!(wire_unset.get("maxPromptTokens").is_none());
5400 assert!(wire_unset.get("maxOutputTokens").is_none());
5401 }
5402
5403 #[test]
5404 fn system_message_config_builder_composes() {
5405 use std::collections::HashMap;
5406
5407 let cfg = SystemMessageConfig::new()
5408 .with_mode("replace")
5409 .with_content("Custom system message.")
5410 .with_sections(HashMap::new());
5411
5412 assert_eq!(cfg.mode.as_deref(), Some("replace"));
5413 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
5414 assert!(cfg.sections.is_some());
5415 }
5416
5417 #[test]
5418 fn delivery_mode_serializes_to_kebab_case_strings() {
5419 assert_eq!(
5420 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
5421 "\"enqueue\""
5422 );
5423 assert_eq!(
5424 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
5425 "\"immediate\""
5426 );
5427 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
5428 assert_eq!(parsed, DeliveryMode::Immediate);
5429 }
5430
5431 #[test]
5432 fn agent_mode_serializes_to_kebab_case_strings() {
5433 assert_eq!(
5434 serde_json::to_string(&AgentMode::Interactive).unwrap(),
5435 "\"interactive\""
5436 );
5437 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
5438 assert_eq!(
5439 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
5440 "\"autopilot\""
5441 );
5442 assert_eq!(
5443 serde_json::to_string(&AgentMode::Shell).unwrap(),
5444 "\"shell\""
5445 );
5446 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
5447 assert_eq!(parsed, AgentMode::Plan);
5448 }
5449
5450 #[test]
5451 fn connection_state_distinguishes_variants() {
5452 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
5455 }
5456
5457 #[test]
5463 fn session_event_round_trips_agent_id_on_envelope() {
5464 let wire = json!({
5465 "id": "evt-1",
5466 "timestamp": "2026-04-30T12:00:00Z",
5467 "parentId": null,
5468 "agentId": "sub-agent-42",
5469 "type": "assistant.message",
5470 "data": { "message": "hi" }
5471 });
5472
5473 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
5474 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
5475
5476 let roundtripped = serde_json::to_value(&event).unwrap();
5478 assert_eq!(roundtripped["agentId"], "sub-agent-42");
5479
5480 let main_agent_event: SessionEvent = serde_json::from_value(json!({
5482 "id": "evt-2",
5483 "timestamp": "2026-04-30T12:00:01Z",
5484 "parentId": null,
5485 "type": "session.idle",
5486 "data": {}
5487 }))
5488 .unwrap();
5489 assert!(main_agent_event.agent_id.is_none());
5490 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
5491 assert!(roundtripped.get("agentId").is_none());
5492 }
5493
5494 #[test]
5496 fn typed_session_event_round_trips_agent_id_on_envelope() {
5497 let wire = json!({
5498 "id": "evt-1",
5499 "timestamp": "2026-04-30T12:00:00Z",
5500 "parentId": null,
5501 "agentId": "sub-agent-42",
5502 "type": "session.idle",
5503 "data": {}
5504 });
5505
5506 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
5507 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
5508
5509 let roundtripped = serde_json::to_value(&event).unwrap();
5510 assert_eq!(roundtripped["agentId"], "sub-agent-42");
5511 }
5512
5513 #[test]
5514 fn connection_state_variants_compile() {
5515 let _ = ConnectionState::Disconnected;
5519 let _ = ConnectionState::Connecting;
5520 let _ = ConnectionState::Connected;
5521 let _ = ConnectionState::Error;
5522 }
5523
5524 #[test]
5525 fn deserializes_runtime_attachment_variants() {
5526 let attachments: Vec<Attachment> = serde_json::from_value(json!([
5527 {
5528 "type": "file",
5529 "path": "/tmp/file.rs",
5530 "displayName": "file.rs",
5531 "lineRange": { "start": 7, "end": 12 }
5532 },
5533 {
5534 "type": "directory",
5535 "path": "/tmp/project",
5536 "displayName": "project"
5537 },
5538 {
5539 "type": "selection",
5540 "filePath": "/tmp/lib.rs",
5541 "displayName": "lib.rs",
5542 "text": "fn main() {}",
5543 "selection": {
5544 "start": { "line": 1, "character": 2 },
5545 "end": { "line": 3, "character": 4 }
5546 }
5547 },
5548 {
5549 "type": "blob",
5550 "data": "Zm9v",
5551 "mimeType": "image/png",
5552 "displayName": "image.png"
5553 },
5554 {
5555 "type": "github_reference",
5556 "number": 42,
5557 "title": "Fix rendering",
5558 "referenceType": "issue",
5559 "state": "open",
5560 "url": "https://github.com/example/repo/issues/42"
5561 }
5562 ]))
5563 .expect("attachments should deserialize");
5564
5565 assert_eq!(attachments.len(), 5);
5566 assert!(matches!(
5567 &attachments[0],
5568 Attachment::File {
5569 path,
5570 display_name,
5571 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
5572 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
5573 ));
5574 assert!(matches!(
5575 &attachments[1],
5576 Attachment::Directory { path, display_name }
5577 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
5578 ));
5579 assert!(matches!(
5580 &attachments[2],
5581 Attachment::Selection {
5582 file_path,
5583 display_name,
5584 selection:
5585 AttachmentSelectionRange {
5586 start: AttachmentSelectionPosition { line: 1, character: 2 },
5587 end: AttachmentSelectionPosition { line: 3, character: 4 },
5588 },
5589 ..
5590 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
5591 ));
5592 assert!(matches!(
5593 &attachments[3],
5594 Attachment::Blob {
5595 data,
5596 mime_type,
5597 display_name,
5598 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
5599 ));
5600 assert!(matches!(
5601 &attachments[4],
5602 Attachment::GitHubReference {
5603 number: 42,
5604 title,
5605 reference_type: GitHubReferenceType::Issue,
5606 state,
5607 url,
5608 } if title == "Fix rendering"
5609 && state == "open"
5610 && url == "https://github.com/example/repo/issues/42"
5611 ));
5612 }
5613
5614 #[test]
5615 fn ensures_display_names_for_variants_that_support_them() {
5616 let mut attachments = vec![
5617 Attachment::File {
5618 path: PathBuf::from("/tmp/file.rs"),
5619 display_name: None,
5620 line_range: None,
5621 },
5622 Attachment::Selection {
5623 file_path: PathBuf::from("/tmp/src/lib.rs"),
5624 display_name: None,
5625 text: "fn main() {}".to_string(),
5626 selection: AttachmentSelectionRange {
5627 start: AttachmentSelectionPosition {
5628 line: 0,
5629 character: 0,
5630 },
5631 end: AttachmentSelectionPosition {
5632 line: 0,
5633 character: 10,
5634 },
5635 },
5636 },
5637 Attachment::Blob {
5638 data: "Zm9v".to_string(),
5639 mime_type: "image/png".to_string(),
5640 display_name: None,
5641 },
5642 Attachment::GitHubReference {
5643 number: 7,
5644 title: "Track regressions".to_string(),
5645 reference_type: GitHubReferenceType::Issue,
5646 state: "open".to_string(),
5647 url: "https://example.com/issues/7".to_string(),
5648 },
5649 ];
5650
5651 ensure_attachment_display_names(&mut attachments);
5652
5653 assert_eq!(attachments[0].display_name(), Some("file.rs"));
5654 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
5655 assert_eq!(attachments[2].display_name(), Some("attachment"));
5656 assert_eq!(attachments[3].display_name(), None);
5657 assert_eq!(
5658 attachments[3].label(),
5659 Some("Track regressions".to_string())
5660 );
5661 }
5662}
5663
5664#[cfg(test)]
5665mod permission_builder_tests {
5666 use std::sync::Arc;
5667
5668 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
5669 use crate::permission;
5670 use crate::types::{
5671 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
5672 SessionId,
5673 };
5674
5675 fn data() -> PermissionRequestData {
5676 PermissionRequestData {
5677 extra: serde_json::json!({"tool": "shell"}),
5678 ..Default::default()
5679 }
5680 }
5681
5682 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
5685 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
5686 }
5687
5688 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
5689 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
5690 }
5691
5692 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
5693 handler
5694 .handle(SessionId::from("s1"), RequestId::new("1"), data())
5695 .await
5696 }
5697
5698 #[tokio::test]
5699 async fn approve_all_with_handler_present_approves() {
5700 let cfg = SessionConfig::default()
5701 .with_permission_handler(Arc::new(ApproveAllHandler))
5702 .approve_all_permissions();
5703 let h = resolve_create(cfg).expect("policy + handler yields handler");
5704 assert!(matches!(
5705 dispatch(&h).await,
5706 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
5707 ));
5708 }
5709
5710 #[tokio::test]
5711 async fn approve_all_standalone_produces_handler() {
5712 let cfg = SessionConfig::default().approve_all_permissions();
5713 let h = resolve_create(cfg).expect("policy alone yields handler");
5714 assert!(matches!(
5715 dispatch(&h).await,
5716 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
5717 ));
5718 }
5719
5720 #[tokio::test]
5723 async fn approve_all_is_order_independent() {
5724 let a = SessionConfig::default()
5725 .with_permission_handler(Arc::new(ApproveAllHandler))
5726 .approve_all_permissions();
5727 let b = SessionConfig::default()
5728 .approve_all_permissions()
5729 .with_permission_handler(Arc::new(ApproveAllHandler));
5730 let ha = resolve_create(a).unwrap();
5731 let hb = resolve_create(b).unwrap();
5732 assert!(matches!(
5733 dispatch(&ha).await,
5734 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
5735 ));
5736 assert!(matches!(
5737 dispatch(&hb).await,
5738 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
5739 ));
5740 }
5741
5742 #[tokio::test]
5743 async fn deny_all_is_order_independent() {
5744 let a = SessionConfig::default()
5745 .with_permission_handler(Arc::new(ApproveAllHandler))
5746 .deny_all_permissions();
5747 let b = SessionConfig::default()
5748 .deny_all_permissions()
5749 .with_permission_handler(Arc::new(ApproveAllHandler));
5750 let ha = resolve_create(a).unwrap();
5751 let hb = resolve_create(b).unwrap();
5752 assert!(matches!(
5753 dispatch(&ha).await,
5754 PermissionResult::Decision(PermissionDecision::Reject(_))
5755 ));
5756 assert!(matches!(
5757 dispatch(&hb).await,
5758 PermissionResult::Decision(PermissionDecision::Reject(_))
5759 ));
5760 }
5761
5762 #[tokio::test]
5763 async fn approve_permissions_if_consults_predicate() {
5764 let cfg = SessionConfig::default().approve_permissions_if(|d| {
5765 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
5766 });
5767 let h = resolve_create(cfg).unwrap();
5768 assert!(matches!(
5769 dispatch(&h).await,
5770 PermissionResult::Decision(PermissionDecision::Reject(_))
5771 ));
5772 }
5773
5774 #[tokio::test]
5775 async fn approve_permissions_if_is_order_independent() {
5776 let predicate = |d: &PermissionRequestData| {
5777 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
5778 };
5779 let a = SessionConfig::default()
5780 .with_permission_handler(Arc::new(ApproveAllHandler))
5781 .approve_permissions_if(predicate);
5782 let b = SessionConfig::default()
5783 .approve_permissions_if(predicate)
5784 .with_permission_handler(Arc::new(ApproveAllHandler));
5785 let ha = resolve_create(a).unwrap();
5786 let hb = resolve_create(b).unwrap();
5787 assert!(matches!(
5788 dispatch(&ha).await,
5789 PermissionResult::Decision(PermissionDecision::Reject(_))
5790 ));
5791 assert!(matches!(
5792 dispatch(&hb).await,
5793 PermissionResult::Decision(PermissionDecision::Reject(_))
5794 ));
5795 }
5796
5797 #[tokio::test]
5798 async fn resume_session_config_approve_all_works() {
5799 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
5800 .with_permission_handler(Arc::new(ApproveAllHandler))
5801 .approve_all_permissions();
5802 let h = resolve_resume(cfg).unwrap();
5803 assert!(matches!(
5804 dispatch(&h).await,
5805 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
5806 ));
5807 }
5808
5809 #[tokio::test]
5810 async fn resume_session_config_approve_all_is_order_independent() {
5811 let a = ResumeSessionConfig::new(SessionId::from("s1"))
5812 .with_permission_handler(Arc::new(ApproveAllHandler))
5813 .approve_all_permissions();
5814 let b = ResumeSessionConfig::new(SessionId::from("s1"))
5815 .approve_all_permissions()
5816 .with_permission_handler(Arc::new(ApproveAllHandler));
5817 let ha = resolve_resume(a).unwrap();
5818 let hb = resolve_resume(b).unwrap();
5819 assert!(matches!(
5820 dispatch(&ha).await,
5821 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
5822 ));
5823 assert!(matches!(
5824 dispatch(&hb).await,
5825 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
5826 ));
5827 }
5828}