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(Clone)]
1211#[non_exhaustive]
1212pub struct SessionConfig {
1213 pub session_id: Option<SessionId>,
1215 pub model: Option<String>,
1217 pub client_name: Option<String>,
1219 pub reasoning_effort: Option<String>,
1221 pub reasoning_summary: Option<ReasoningSummary>,
1225 pub context_tier: Option<String>,
1228 pub streaming: Option<bool>,
1230 pub system_message: Option<SystemMessageConfig>,
1232 pub tools: Option<Vec<Tool>>,
1234 pub canvases: Option<Vec<CanvasDeclaration>>,
1236 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1241 pub request_canvas_renderer: Option<bool>,
1243 pub request_extensions: Option<bool>,
1245 pub extension_sdk_path: Option<String>,
1249 pub extension_info: Option<ExtensionInfo>,
1251 pub available_tools: Option<Vec<String>>,
1253 pub excluded_tools: Option<Vec<String>>,
1255 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
1257 pub mcp_oauth_token_storage: Option<String>,
1266 pub enable_config_discovery: Option<bool>,
1268 pub skip_embedding_retrieval: Option<bool>,
1270 pub embedding_cache_storage: Option<String>,
1273 pub organization_custom_instructions: Option<String>,
1275 pub enable_on_demand_instruction_discovery: Option<bool>,
1277 pub enable_file_hooks: Option<bool>,
1279 pub enable_host_git_operations: Option<bool>,
1281 pub enable_session_store: Option<bool>,
1283 pub enable_skills: Option<bool>,
1285 pub enable_mcp_apps: Option<bool>,
1312 pub skill_directories: Option<Vec<PathBuf>>,
1314 pub instruction_directories: Option<Vec<PathBuf>>,
1317 pub plugin_directories: Option<Vec<PathBuf>>,
1319 pub large_output: Option<LargeToolOutputConfig>,
1321 pub disabled_skills: Option<Vec<String>>,
1324 pub hooks: Option<bool>,
1328 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1330 pub default_agent: Option<DefaultAgentConfig>,
1334 pub agent: Option<String>,
1337 pub infinite_sessions: Option<InfiniteSessionConfig>,
1340 pub provider: Option<ProviderConfig>,
1344 pub enable_session_telemetry: Option<bool>,
1352 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1355 pub memory: Option<MemoryConfiguration>,
1357 pub config_directory: Option<PathBuf>,
1360 pub working_directory: Option<PathBuf>,
1363 pub github_token: Option<String>,
1369 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
1375 pub cloud: Option<CloudSessionOptions>,
1378 pub include_sub_agent_streaming_events: Option<bool>,
1382 pub commands: Option<Vec<CommandDefinition>>,
1386 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1391 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
1395 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
1398 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
1402 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
1405 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
1408 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1412 pub(crate) permission_policy: Option<crate::permission::Policy>,
1416 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
1421 pub skip_custom_instructions: Option<bool>,
1425 pub custom_agents_local_only: Option<bool>,
1429 pub coauthor_enabled: Option<bool>,
1433 pub manage_schedule_enabled: Option<bool>,
1437}
1438
1439impl std::fmt::Debug for SessionConfig {
1440 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1441 f.debug_struct("SessionConfig")
1442 .field("session_id", &self.session_id)
1443 .field("model", &self.model)
1444 .field("client_name", &self.client_name)
1445 .field("reasoning_effort", &self.reasoning_effort)
1446 .field("reasoning_summary", &self.reasoning_summary)
1447 .field("context_tier", &self.context_tier)
1448 .field("streaming", &self.streaming)
1449 .field("system_message", &self.system_message)
1450 .field("tools", &self.tools)
1451 .field("canvases", &self.canvases)
1452 .field(
1453 "canvas_handler",
1454 &self.canvas_handler.as_ref().map(|_| "<set>"),
1455 )
1456 .field("request_canvas_renderer", &self.request_canvas_renderer)
1457 .field("request_extensions", &self.request_extensions)
1458 .field("extension_sdk_path", &self.extension_sdk_path)
1459 .field("extension_info", &self.extension_info)
1460 .field("available_tools", &self.available_tools)
1461 .field("excluded_tools", &self.excluded_tools)
1462 .field("mcp_servers", &self.mcp_servers)
1463 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
1464 .field("embedding_cache_storage", &self.embedding_cache_storage)
1465 .field("enable_config_discovery", &self.enable_config_discovery)
1466 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
1467 .field(
1468 "organization_custom_instructions",
1469 &self
1470 .organization_custom_instructions
1471 .as_ref()
1472 .map(|_| "<redacted>"),
1473 )
1474 .field(
1475 "enable_on_demand_instruction_discovery",
1476 &self.enable_on_demand_instruction_discovery,
1477 )
1478 .field("enable_file_hooks", &self.enable_file_hooks)
1479 .field(
1480 "enable_host_git_operations",
1481 &self.enable_host_git_operations,
1482 )
1483 .field("enable_session_store", &self.enable_session_store)
1484 .field("enable_skills", &self.enable_skills)
1485 .field("enable_mcp_apps", &self.enable_mcp_apps)
1486 .field("skill_directories", &self.skill_directories)
1487 .field("instruction_directories", &self.instruction_directories)
1488 .field("plugin_directories", &self.plugin_directories)
1489 .field("large_output", &self.large_output)
1490 .field("disabled_skills", &self.disabled_skills)
1491 .field("hooks", &self.hooks)
1492 .field("custom_agents", &self.custom_agents)
1493 .field("default_agent", &self.default_agent)
1494 .field("agent", &self.agent)
1495 .field("infinite_sessions", &self.infinite_sessions)
1496 .field("provider", &self.provider)
1497 .field("enable_session_telemetry", &self.enable_session_telemetry)
1498 .field("model_capabilities", &self.model_capabilities)
1499 .field("memory", &self.memory)
1500 .field("config_directory", &self.config_directory)
1501 .field("working_directory", &self.working_directory)
1502 .field(
1503 "github_token",
1504 &self.github_token.as_ref().map(|_| "<redacted>"),
1505 )
1506 .field("remote_session", &self.remote_session)
1507 .field("cloud", &self.cloud)
1508 .field(
1509 "include_sub_agent_streaming_events",
1510 &self.include_sub_agent_streaming_events,
1511 )
1512 .field("commands", &self.commands)
1513 .field(
1514 "session_fs_provider",
1515 &self.session_fs_provider.as_ref().map(|_| "<set>"),
1516 )
1517 .field(
1518 "permission_handler",
1519 &self.permission_handler.as_ref().map(|_| "<set>"),
1520 )
1521 .field(
1522 "elicitation_handler",
1523 &self.elicitation_handler.as_ref().map(|_| "<set>"),
1524 )
1525 .field(
1526 "user_input_handler",
1527 &self.user_input_handler.as_ref().map(|_| "<set>"),
1528 )
1529 .field(
1530 "exit_plan_mode_handler",
1531 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
1532 )
1533 .field(
1534 "auto_mode_switch_handler",
1535 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
1536 )
1537 .field(
1538 "hooks_handler",
1539 &self.hooks_handler.as_ref().map(|_| "<set>"),
1540 )
1541 .field(
1542 "system_message_transform",
1543 &self.system_message_transform.as_ref().map(|_| "<set>"),
1544 )
1545 .finish()
1546 }
1547}
1548
1549impl Default for SessionConfig {
1550 fn default() -> Self {
1556 Self {
1557 session_id: None,
1558 model: None,
1559 client_name: None,
1560 reasoning_effort: None,
1561 reasoning_summary: None,
1562 context_tier: None,
1563 streaming: None,
1564 system_message: None,
1565 tools: None,
1566 canvases: None,
1567 canvas_handler: None,
1568 request_canvas_renderer: None,
1569 request_extensions: None,
1570 extension_sdk_path: None,
1571 extension_info: None,
1572 available_tools: None,
1573 excluded_tools: None,
1574 mcp_servers: None,
1575 mcp_oauth_token_storage: None,
1576 enable_config_discovery: None,
1577 skip_embedding_retrieval: None,
1578 organization_custom_instructions: None,
1579 enable_on_demand_instruction_discovery: None,
1580 enable_file_hooks: None,
1581 enable_host_git_operations: None,
1582 enable_session_store: None,
1583 enable_skills: None,
1584 embedding_cache_storage: None,
1585 enable_mcp_apps: None,
1586 skill_directories: None,
1587 instruction_directories: None,
1588 plugin_directories: None,
1589 large_output: None,
1590 disabled_skills: None,
1591 hooks: None,
1592 custom_agents: None,
1593 default_agent: None,
1594 agent: None,
1595 infinite_sessions: None,
1596 provider: None,
1597 enable_session_telemetry: None,
1598 model_capabilities: None,
1599 memory: None,
1600 config_directory: None,
1601 working_directory: None,
1602 github_token: None,
1603 remote_session: None,
1604 cloud: None,
1605 include_sub_agent_streaming_events: None,
1606 commands: None,
1607 session_fs_provider: None,
1608 permission_handler: None,
1609 elicitation_handler: None,
1610 user_input_handler: None,
1611 exit_plan_mode_handler: None,
1612 auto_mode_switch_handler: None,
1613 hooks_handler: None,
1614 permission_policy: None,
1615 system_message_transform: None,
1616 skip_custom_instructions: None,
1617 custom_agents_local_only: None,
1618 coauthor_enabled: None,
1619 manage_schedule_enabled: None,
1620 }
1621 }
1622}
1623
1624pub(crate) struct SessionConfigRuntime {
1630 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
1631 pub permission_policy: Option<crate::permission::Policy>,
1632 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
1633 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
1634 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
1635 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
1636 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1637 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
1638 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
1639 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1640 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1641 pub commands: Option<Vec<CommandDefinition>>,
1642}
1643
1644impl SessionConfig {
1645 pub(crate) fn into_wire(
1657 mut self,
1658 session_id: Option<SessionId>,
1659 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
1660 let permission_active =
1661 self.permission_handler.is_some() || self.permission_policy.is_some();
1662 let request_user_input = self.user_input_handler.is_some();
1663 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
1664 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
1665 let request_elicitation = self.elicitation_handler.is_some();
1666 let hooks_flag = self.hooks_handler.is_some();
1667
1668 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
1669 if let Some(tools) = self.tools.as_mut() {
1670 for tool in tools.iter_mut() {
1671 if let Some(handler) = tool.handler.take()
1672 && tool_handlers.insert(tool.name.clone(), handler).is_some()
1673 {
1674 return Err(crate::Error::with_message(
1675 crate::ErrorKind::InvalidConfig,
1676 format!("duplicate tool handler registered for name {:?}", tool.name),
1677 ));
1678 }
1679 }
1680 }
1681
1682 let wire_commands = self.commands.as_ref().map(|cmds| {
1683 cmds.iter()
1684 .map(|c| crate::wire::CommandWireDefinition {
1685 name: c.name.clone(),
1686 description: c.description.clone(),
1687 })
1688 .collect()
1689 });
1690 let wire_canvases = self.canvases.clone();
1691 let canvas_handler = self.canvas_handler.clone();
1692
1693 let wire = crate::wire::SessionCreateWire {
1694 session_id,
1695 model: self.model,
1696 client_name: self.client_name,
1697 reasoning_effort: self.reasoning_effort,
1698 reasoning_summary: self.reasoning_summary,
1699 context_tier: self.context_tier,
1700 streaming: self.streaming,
1701 system_message: self.system_message,
1702 tools: self.tools,
1703 canvases: wire_canvases,
1704 request_canvas_renderer: self.request_canvas_renderer,
1705 request_extensions: self.request_extensions,
1706 extension_sdk_path: self.extension_sdk_path,
1707 extension_info: self.extension_info,
1708 available_tools: self.available_tools,
1709 excluded_tools: self.excluded_tools,
1710 tool_filter_precedence: "excluded",
1711 mcp_servers: self.mcp_servers,
1712 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
1713 embedding_cache_storage: self.embedding_cache_storage,
1714 env_value_mode: "direct",
1715 enable_config_discovery: self.enable_config_discovery,
1716 skip_embedding_retrieval: self.skip_embedding_retrieval,
1717 organization_custom_instructions: self.organization_custom_instructions,
1718 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
1719 enable_file_hooks: self.enable_file_hooks,
1720 enable_host_git_operations: self.enable_host_git_operations,
1721 enable_session_store: self.enable_session_store,
1722 enable_skills: self.enable_skills,
1723 request_user_input,
1724 request_permission: permission_active,
1725 request_exit_plan_mode,
1726 request_auto_mode_switch,
1727 request_elicitation,
1728 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
1729 hooks: hooks_flag,
1730 skill_directories: self.skill_directories,
1731 instruction_directories: self.instruction_directories,
1732 plugin_directories: self.plugin_directories,
1733 large_output: self.large_output,
1734 disabled_skills: self.disabled_skills,
1735 custom_agents: self.custom_agents,
1736 default_agent: self.default_agent,
1737 agent: self.agent,
1738 infinite_sessions: self.infinite_sessions,
1739 provider: self.provider,
1740 enable_session_telemetry: self.enable_session_telemetry,
1741 model_capabilities: self.model_capabilities,
1742 memory: self.memory,
1743 config_dir: self.config_directory,
1744 working_directory: self.working_directory,
1745 github_token: self.github_token,
1746 remote_session: self.remote_session,
1747 cloud: self.cloud,
1748 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
1749 commands: wire_commands,
1750 };
1751
1752 let runtime = SessionConfigRuntime {
1753 permission_handler: self.permission_handler,
1754 permission_policy: self.permission_policy,
1755 elicitation_handler: self.elicitation_handler,
1756 user_input_handler: self.user_input_handler,
1757 exit_plan_mode_handler: self.exit_plan_mode_handler,
1758 auto_mode_switch_handler: self.auto_mode_switch_handler,
1759 hooks_handler: self.hooks_handler,
1760 system_message_transform: self.system_message_transform,
1761 tool_handlers,
1762 canvas_handler,
1763 session_fs_provider: self.session_fs_provider,
1764 commands: self.commands,
1765 };
1766
1767 Ok((wire, runtime))
1768 }
1769
1770 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
1774 self.permission_handler = Some(handler);
1775 self
1776 }
1777
1778 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
1781 self.elicitation_handler = Some(handler);
1782 self
1783 }
1784
1785 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
1788 self.user_input_handler = Some(handler);
1789 self
1790 }
1791
1792 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
1794 self.exit_plan_mode_handler = Some(handler);
1795 self
1796 }
1797
1798 pub fn with_auto_mode_switch_handler(
1800 mut self,
1801 handler: Arc<dyn AutoModeSwitchHandler>,
1802 ) -> Self {
1803 self.auto_mode_switch_handler = Some(handler);
1804 self
1805 }
1806
1807 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
1812 self.commands = Some(commands);
1813 self
1814 }
1815
1816 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
1820 self.session_fs_provider = Some(provider);
1821 self
1822 }
1823
1824 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
1827 self.hooks_handler = Some(hooks);
1828 self
1829 }
1830
1831 pub fn with_system_message_transform(
1835 mut self,
1836 transform: Arc<dyn SystemMessageTransform>,
1837 ) -> Self {
1838 self.system_message_transform = Some(transform);
1839 self
1840 }
1841
1842 pub fn approve_all_permissions(mut self) -> Self {
1848 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
1849 self
1850 }
1851
1852 pub fn deny_all_permissions(mut self) -> Self {
1855 self.permission_policy = Some(crate::permission::Policy::DenyAll);
1856 self
1857 }
1858
1859 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
1864 where
1865 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
1866 {
1867 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
1868 self
1869 }
1870
1871 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
1873 self.session_id = Some(id.into());
1874 self
1875 }
1876
1877 pub fn with_model(mut self, model: impl Into<String>) -> Self {
1879 self.model = Some(model.into());
1880 self
1881 }
1882
1883 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
1885 self.client_name = Some(name.into());
1886 self
1887 }
1888
1889 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
1891 self.reasoning_effort = Some(effort.into());
1892 self
1893 }
1894
1895 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
1897 self.reasoning_summary = Some(summary);
1898 self
1899 }
1900
1901 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
1903 self.context_tier = Some(tier.into());
1904 self
1905 }
1906
1907 pub fn with_streaming(mut self, streaming: bool) -> Self {
1909 self.streaming = Some(streaming);
1910 self
1911 }
1912
1913 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
1915 self.system_message = Some(system_message);
1916 self
1917 }
1918
1919 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
1921 self.tools = Some(tools.into_iter().collect());
1922 self
1923 }
1924
1925 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
1930 self.canvases = Some(canvases.into_iter().collect());
1931 self
1932 }
1933
1934 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
1936 self.canvas_handler = Some(handler);
1937 self
1938 }
1939
1940 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
1942 self.request_canvas_renderer = Some(request);
1943 self
1944 }
1945
1946 pub fn with_request_extensions(mut self, request: bool) -> Self {
1948 self.request_extensions = Some(request);
1949 self
1950 }
1951
1952 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
1956 self.extension_sdk_path = Some(path.into());
1957 self
1958 }
1959
1960 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
1962 self.extension_info = Some(extension_info);
1963 self
1964 }
1965
1966 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
1968 where
1969 I: IntoIterator<Item = S>,
1970 S: Into<String>,
1971 {
1972 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
1973 self
1974 }
1975
1976 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
1978 where
1979 I: IntoIterator<Item = S>,
1980 S: Into<String>,
1981 {
1982 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
1983 self
1984 }
1985
1986 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
1988 self.mcp_servers = Some(servers);
1989 self
1990 }
1991
1992 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2000 self.mcp_oauth_token_storage = Some(mode.into());
2001 self
2002 }
2003
2004 pub fn with_embedding_cache_storage(
2006 mut self,
2007 embedding_cache_storage: impl Into<String>,
2008 ) -> Self {
2009 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2010 self
2011 }
2012
2013 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2015 self.enable_config_discovery = Some(enable);
2016 self
2017 }
2018
2019 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2021 self.skip_embedding_retrieval = Some(value);
2022 self
2023 }
2024
2025 pub fn with_organization_custom_instructions(
2027 mut self,
2028 instructions: impl Into<String>,
2029 ) -> Self {
2030 self.organization_custom_instructions = Some(instructions.into());
2031 self
2032 }
2033
2034 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2036 self.enable_on_demand_instruction_discovery = Some(value);
2037 self
2038 }
2039
2040 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2042 self.enable_file_hooks = Some(value);
2043 self
2044 }
2045
2046 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2048 self.enable_host_git_operations = Some(value);
2049 self
2050 }
2051
2052 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2054 self.enable_session_store = Some(value);
2055 self
2056 }
2057
2058 pub fn with_enable_skills(mut self, value: bool) -> Self {
2060 self.enable_skills = Some(value);
2061 self
2062 }
2063
2064 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2070 self.enable_mcp_apps = Some(enable);
2071 self
2072 }
2073
2074 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2076 where
2077 I: IntoIterator<Item = P>,
2078 P: Into<PathBuf>,
2079 {
2080 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2081 self
2082 }
2083
2084 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2088 where
2089 I: IntoIterator<Item = P>,
2090 P: Into<PathBuf>,
2091 {
2092 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2093 self
2094 }
2095
2096 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2098 where
2099 I: IntoIterator<Item = P>,
2100 P: Into<PathBuf>,
2101 {
2102 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2103 self
2104 }
2105
2106 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2108 self.large_output = Some(config);
2109 self
2110 }
2111
2112 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2114 where
2115 I: IntoIterator<Item = S>,
2116 S: Into<String>,
2117 {
2118 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2119 self
2120 }
2121
2122 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2124 mut self,
2125 agents: I,
2126 ) -> Self {
2127 self.custom_agents = Some(agents.into_iter().collect());
2128 self
2129 }
2130
2131 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2133 self.default_agent = Some(agent);
2134 self
2135 }
2136
2137 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2140 self.agent = Some(name.into());
2141 self
2142 }
2143
2144 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2147 self.infinite_sessions = Some(config);
2148 self
2149 }
2150
2151 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2153 self.provider = Some(provider);
2154 self
2155 }
2156
2157 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2161 self.enable_session_telemetry = Some(enable);
2162 self
2163 }
2164
2165 pub fn with_model_capabilities(
2167 mut self,
2168 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2169 ) -> Self {
2170 self.model_capabilities = Some(capabilities);
2171 self
2172 }
2173
2174 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
2176 self.memory = Some(memory);
2177 self
2178 }
2179
2180 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2182 self.config_directory = Some(dir.into());
2183 self
2184 }
2185
2186 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2189 self.working_directory = Some(dir.into());
2190 self
2191 }
2192
2193 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2198 self.github_token = Some(token.into());
2199 self
2200 }
2201
2202 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2205 self.include_sub_agent_streaming_events = Some(include);
2206 self
2207 }
2208
2209 pub fn with_remote_session(
2211 mut self,
2212 mode: crate::generated::api_types::RemoteSessionMode,
2213 ) -> Self {
2214 self.remote_session = Some(mode);
2215 self
2216 }
2217
2218 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
2220 self.cloud = Some(cloud);
2221 self
2222 }
2223
2224 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
2226 self.skip_custom_instructions = Some(value);
2227 self
2228 }
2229
2230 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
2232 self.custom_agents_local_only = Some(value);
2233 self
2234 }
2235
2236 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
2238 self.coauthor_enabled = Some(value);
2239 self
2240 }
2241
2242 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
2244 self.manage_schedule_enabled = Some(value);
2245 self
2246 }
2247}
2248
2249#[derive(Clone)]
2257#[non_exhaustive]
2258pub struct ResumeSessionConfig {
2259 pub session_id: SessionId,
2261 pub client_name: Option<String>,
2263 pub reasoning_effort: Option<String>,
2265 pub reasoning_summary: Option<ReasoningSummary>,
2269 pub context_tier: Option<String>,
2272 pub streaming: Option<bool>,
2274 pub system_message: Option<SystemMessageConfig>,
2277 pub tools: Option<Vec<Tool>>,
2279 pub canvases: Option<Vec<CanvasDeclaration>>,
2281 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2284 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
2286 pub request_canvas_renderer: Option<bool>,
2288 pub request_extensions: Option<bool>,
2290 pub extension_sdk_path: Option<String>,
2294 pub extension_info: Option<ExtensionInfo>,
2296 pub available_tools: Option<Vec<String>>,
2298 pub excluded_tools: Option<Vec<String>>,
2300 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
2302 pub mcp_oauth_token_storage: Option<String>,
2305 pub enable_config_discovery: Option<bool>,
2307 pub skip_embedding_retrieval: Option<bool>,
2309 pub embedding_cache_storage: Option<String>,
2311 pub organization_custom_instructions: Option<String>,
2313 pub enable_on_demand_instruction_discovery: Option<bool>,
2315 pub enable_file_hooks: Option<bool>,
2317 pub enable_host_git_operations: Option<bool>,
2319 pub enable_session_store: Option<bool>,
2321 pub enable_skills: Option<bool>,
2323 pub enable_mcp_apps: Option<bool>,
2329 pub skill_directories: Option<Vec<PathBuf>>,
2331 pub instruction_directories: Option<Vec<PathBuf>>,
2334 pub plugin_directories: Option<Vec<PathBuf>>,
2336 pub large_output: Option<LargeToolOutputConfig>,
2338 pub disabled_skills: Option<Vec<String>>,
2340 pub hooks: Option<bool>,
2342 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2344 pub default_agent: Option<DefaultAgentConfig>,
2346 pub agent: Option<String>,
2348 pub infinite_sessions: Option<InfiniteSessionConfig>,
2350 pub provider: Option<ProviderConfig>,
2352 pub enable_session_telemetry: Option<bool>,
2360 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2362 pub memory: Option<MemoryConfiguration>,
2364 pub config_directory: Option<PathBuf>,
2366 pub working_directory: Option<PathBuf>,
2368 pub github_token: Option<String>,
2371 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2374 pub include_sub_agent_streaming_events: Option<bool>,
2376 pub commands: Option<Vec<CommandDefinition>>,
2380 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2385 pub suppress_resume_event: Option<bool>,
2388 pub continue_pending_work: Option<bool>,
2396 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2399 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2402 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2405 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2408 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2411 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2413 pub(crate) permission_policy: Option<crate::permission::Policy>,
2415 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2417 pub skip_custom_instructions: Option<bool>,
2419 pub custom_agents_local_only: Option<bool>,
2421 pub coauthor_enabled: Option<bool>,
2423 pub manage_schedule_enabled: Option<bool>,
2425}
2426
2427impl std::fmt::Debug for ResumeSessionConfig {
2428 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2429 f.debug_struct("ResumeSessionConfig")
2430 .field("session_id", &self.session_id)
2431 .field("client_name", &self.client_name)
2432 .field("reasoning_effort", &self.reasoning_effort)
2433 .field("reasoning_summary", &self.reasoning_summary)
2434 .field("context_tier", &self.context_tier)
2435 .field("streaming", &self.streaming)
2436 .field("system_message", &self.system_message)
2437 .field("tools", &self.tools)
2438 .field("canvases", &self.canvases)
2439 .field(
2440 "canvas_handler",
2441 &self.canvas_handler.as_ref().map(|_| "<set>"),
2442 )
2443 .field("open_canvases", &self.open_canvases)
2444 .field("request_canvas_renderer", &self.request_canvas_renderer)
2445 .field("request_extensions", &self.request_extensions)
2446 .field("extension_sdk_path", &self.extension_sdk_path)
2447 .field("extension_info", &self.extension_info)
2448 .field("available_tools", &self.available_tools)
2449 .field("excluded_tools", &self.excluded_tools)
2450 .field("mcp_servers", &self.mcp_servers)
2451 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2452 .field("embedding_cache_storage", &self.embedding_cache_storage)
2453 .field("enable_config_discovery", &self.enable_config_discovery)
2454 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2455 .field(
2456 "organization_custom_instructions",
2457 &self
2458 .organization_custom_instructions
2459 .as_ref()
2460 .map(|_| "<redacted>"),
2461 )
2462 .field(
2463 "enable_on_demand_instruction_discovery",
2464 &self.enable_on_demand_instruction_discovery,
2465 )
2466 .field("enable_file_hooks", &self.enable_file_hooks)
2467 .field(
2468 "enable_host_git_operations",
2469 &self.enable_host_git_operations,
2470 )
2471 .field("enable_session_store", &self.enable_session_store)
2472 .field("enable_skills", &self.enable_skills)
2473 .field("enable_mcp_apps", &self.enable_mcp_apps)
2474 .field("skill_directories", &self.skill_directories)
2475 .field("instruction_directories", &self.instruction_directories)
2476 .field("plugin_directories", &self.plugin_directories)
2477 .field("large_output", &self.large_output)
2478 .field("disabled_skills", &self.disabled_skills)
2479 .field("hooks", &self.hooks)
2480 .field("custom_agents", &self.custom_agents)
2481 .field("default_agent", &self.default_agent)
2482 .field("agent", &self.agent)
2483 .field("infinite_sessions", &self.infinite_sessions)
2484 .field("provider", &self.provider)
2485 .field("enable_session_telemetry", &self.enable_session_telemetry)
2486 .field("model_capabilities", &self.model_capabilities)
2487 .field("memory", &self.memory)
2488 .field("config_directory", &self.config_directory)
2489 .field("working_directory", &self.working_directory)
2490 .field(
2491 "github_token",
2492 &self.github_token.as_ref().map(|_| "<redacted>"),
2493 )
2494 .field("remote_session", &self.remote_session)
2495 .field(
2496 "include_sub_agent_streaming_events",
2497 &self.include_sub_agent_streaming_events,
2498 )
2499 .field("commands", &self.commands)
2500 .field(
2501 "session_fs_provider",
2502 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2503 )
2504 .field(
2505 "permission_handler",
2506 &self.permission_handler.as_ref().map(|_| "<set>"),
2507 )
2508 .field(
2509 "elicitation_handler",
2510 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2511 )
2512 .field(
2513 "user_input_handler",
2514 &self.user_input_handler.as_ref().map(|_| "<set>"),
2515 )
2516 .field(
2517 "exit_plan_mode_handler",
2518 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2519 )
2520 .field(
2521 "auto_mode_switch_handler",
2522 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2523 )
2524 .field(
2525 "hooks_handler",
2526 &self.hooks_handler.as_ref().map(|_| "<set>"),
2527 )
2528 .field(
2529 "system_message_transform",
2530 &self.system_message_transform.as_ref().map(|_| "<set>"),
2531 )
2532 .field("suppress_resume_event", &self.suppress_resume_event)
2533 .field("continue_pending_work", &self.continue_pending_work)
2534 .finish()
2535 }
2536}
2537
2538impl ResumeSessionConfig {
2539 pub(crate) fn into_wire(
2547 mut self,
2548 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
2549 let permission_active =
2550 self.permission_handler.is_some() || self.permission_policy.is_some();
2551 let request_user_input = self.user_input_handler.is_some();
2552 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2553 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2554 let request_elicitation = self.elicitation_handler.is_some();
2555 let hooks_flag = self.hooks_handler.is_some();
2556
2557 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2558 if let Some(tools) = self.tools.as_mut() {
2559 for tool in tools.iter_mut() {
2560 if let Some(handler) = tool.handler.take()
2561 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2562 {
2563 return Err(crate::Error::with_message(
2564 crate::ErrorKind::InvalidConfig,
2565 format!("duplicate tool handler registered for name {:?}", tool.name),
2566 ));
2567 }
2568 }
2569 }
2570
2571 let wire_commands = self.commands.as_ref().map(|cmds| {
2572 cmds.iter()
2573 .map(|c| crate::wire::CommandWireDefinition {
2574 name: c.name.clone(),
2575 description: c.description.clone(),
2576 })
2577 .collect()
2578 });
2579 let wire_canvases = self.canvases.clone();
2580 let canvas_handler = self.canvas_handler.clone();
2581
2582 let wire = crate::wire::SessionResumeWire {
2583 session_id: self.session_id,
2584 client_name: self.client_name,
2585 reasoning_effort: self.reasoning_effort,
2586 reasoning_summary: self.reasoning_summary,
2587 context_tier: self.context_tier,
2588 streaming: self.streaming,
2589 system_message: self.system_message,
2590 tools: self.tools,
2591 canvases: wire_canvases,
2592 open_canvases: self.open_canvases,
2593 request_canvas_renderer: self.request_canvas_renderer,
2594 request_extensions: self.request_extensions,
2595 extension_sdk_path: self.extension_sdk_path,
2596 extension_info: self.extension_info,
2597 available_tools: self.available_tools,
2598 excluded_tools: self.excluded_tools,
2599 tool_filter_precedence: "excluded",
2600 mcp_servers: self.mcp_servers,
2601 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2602 embedding_cache_storage: self.embedding_cache_storage,
2603 env_value_mode: "direct",
2604 enable_config_discovery: self.enable_config_discovery,
2605 skip_embedding_retrieval: self.skip_embedding_retrieval,
2606 organization_custom_instructions: self.organization_custom_instructions,
2607 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2608 enable_file_hooks: self.enable_file_hooks,
2609 enable_host_git_operations: self.enable_host_git_operations,
2610 enable_session_store: self.enable_session_store,
2611 enable_skills: self.enable_skills,
2612 request_user_input,
2613 request_permission: permission_active,
2614 request_exit_plan_mode,
2615 request_auto_mode_switch,
2616 request_elicitation,
2617 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2618 hooks: hooks_flag,
2619 skill_directories: self.skill_directories,
2620 instruction_directories: self.instruction_directories,
2621 plugin_directories: self.plugin_directories,
2622 large_output: self.large_output,
2623 disabled_skills: self.disabled_skills,
2624 custom_agents: self.custom_agents,
2625 default_agent: self.default_agent,
2626 agent: self.agent,
2627 infinite_sessions: self.infinite_sessions,
2628 provider: self.provider,
2629 enable_session_telemetry: self.enable_session_telemetry,
2630 model_capabilities: self.model_capabilities,
2631 memory: self.memory,
2632 config_dir: self.config_directory,
2633 working_directory: self.working_directory,
2634 github_token: self.github_token,
2635 remote_session: self.remote_session,
2636 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2637 commands: wire_commands,
2638 suppress_resume_event: self.suppress_resume_event,
2639 continue_pending_work: self.continue_pending_work,
2640 };
2641
2642 let runtime = SessionConfigRuntime {
2643 permission_handler: self.permission_handler,
2644 permission_policy: self.permission_policy,
2645 elicitation_handler: self.elicitation_handler,
2646 user_input_handler: self.user_input_handler,
2647 exit_plan_mode_handler: self.exit_plan_mode_handler,
2648 auto_mode_switch_handler: self.auto_mode_switch_handler,
2649 hooks_handler: self.hooks_handler,
2650 system_message_transform: self.system_message_transform,
2651 tool_handlers,
2652 canvas_handler,
2653 session_fs_provider: self.session_fs_provider,
2654 commands: self.commands,
2655 };
2656
2657 Ok((wire, runtime))
2658 }
2659
2660 pub fn new(session_id: SessionId) -> Self {
2665 Self {
2666 session_id,
2667 client_name: None,
2668 reasoning_effort: None,
2669 reasoning_summary: None,
2670 context_tier: None,
2671 streaming: None,
2672 system_message: None,
2673 tools: None,
2674 canvases: None,
2675 canvas_handler: None,
2676 open_canvases: None,
2677 request_canvas_renderer: None,
2678 request_extensions: None,
2679 extension_sdk_path: None,
2680 extension_info: None,
2681 available_tools: None,
2682 excluded_tools: None,
2683 mcp_servers: None,
2684 mcp_oauth_token_storage: None,
2685 enable_config_discovery: None,
2686 skip_embedding_retrieval: None,
2687 organization_custom_instructions: None,
2688 enable_on_demand_instruction_discovery: None,
2689 enable_file_hooks: None,
2690 enable_host_git_operations: None,
2691 enable_session_store: None,
2692 enable_skills: None,
2693 embedding_cache_storage: None,
2694 enable_mcp_apps: None,
2695 skill_directories: None,
2696 instruction_directories: None,
2697 plugin_directories: None,
2698 large_output: None,
2699 disabled_skills: None,
2700 hooks: None,
2701 custom_agents: None,
2702 default_agent: None,
2703 agent: None,
2704 infinite_sessions: None,
2705 provider: None,
2706 enable_session_telemetry: None,
2707 model_capabilities: None,
2708 memory: None,
2709 config_directory: None,
2710 working_directory: None,
2711 github_token: None,
2712 remote_session: None,
2713 include_sub_agent_streaming_events: None,
2714 commands: None,
2715 session_fs_provider: None,
2716 suppress_resume_event: None,
2717 continue_pending_work: None,
2718 permission_handler: None,
2719 elicitation_handler: None,
2720 user_input_handler: None,
2721 exit_plan_mode_handler: None,
2722 auto_mode_switch_handler: None,
2723 hooks_handler: None,
2724 permission_policy: None,
2725 system_message_transform: None,
2726 skip_custom_instructions: None,
2727 custom_agents_local_only: None,
2728 coauthor_enabled: None,
2729 manage_schedule_enabled: None,
2730 }
2731 }
2732
2733 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2735 self.permission_handler = Some(handler);
2736 self
2737 }
2738
2739 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2741 self.elicitation_handler = Some(handler);
2742 self
2743 }
2744
2745 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2747 self.user_input_handler = Some(handler);
2748 self
2749 }
2750
2751 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2753 self.exit_plan_mode_handler = Some(handler);
2754 self
2755 }
2756
2757 pub fn with_auto_mode_switch_handler(
2759 mut self,
2760 handler: Arc<dyn AutoModeSwitchHandler>,
2761 ) -> Self {
2762 self.auto_mode_switch_handler = Some(handler);
2763 self
2764 }
2765
2766 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2769 self.hooks_handler = Some(hooks);
2770 self
2771 }
2772
2773 pub fn with_system_message_transform(
2775 mut self,
2776 transform: Arc<dyn SystemMessageTransform>,
2777 ) -> Self {
2778 self.system_message_transform = Some(transform);
2779 self
2780 }
2781
2782 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2786 self.commands = Some(commands);
2787 self
2788 }
2789
2790 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2793 self.session_fs_provider = Some(provider);
2794 self
2795 }
2796
2797 pub fn approve_all_permissions(mut self) -> Self {
2800 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2801 self
2802 }
2803
2804 pub fn deny_all_permissions(mut self) -> Self {
2807 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2808 self
2809 }
2810
2811 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2814 where
2815 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2816 {
2817 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2818 self
2819 }
2820
2821 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2823 self.client_name = Some(name.into());
2824 self
2825 }
2826
2827 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2829 self.reasoning_effort = Some(effort.into());
2830 self
2831 }
2832
2833 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2835 self.reasoning_summary = Some(summary);
2836 self
2837 }
2838
2839 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2842 self.context_tier = Some(tier.into());
2843 self
2844 }
2845
2846 pub fn with_streaming(mut self, streaming: bool) -> Self {
2848 self.streaming = Some(streaming);
2849 self
2850 }
2851
2852 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2855 self.system_message = Some(system_message);
2856 self
2857 }
2858
2859 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2861 self.tools = Some(tools.into_iter().collect());
2862 self
2863 }
2864
2865 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2867 self.canvases = Some(canvases.into_iter().collect());
2868 self
2869 }
2870
2871 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2873 self.canvas_handler = Some(handler);
2874 self
2875 }
2876
2877 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
2879 mut self,
2880 open_canvases: I,
2881 ) -> Self {
2882 self.open_canvases = Some(open_canvases.into_iter().collect());
2883 self
2884 }
2885
2886 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2888 self.request_canvas_renderer = Some(request);
2889 self
2890 }
2891
2892 pub fn with_request_extensions(mut self, request: bool) -> Self {
2894 self.request_extensions = Some(request);
2895 self
2896 }
2897
2898 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2902 self.extension_sdk_path = Some(path.into());
2903 self
2904 }
2905
2906 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2908 self.extension_info = Some(extension_info);
2909 self
2910 }
2911
2912 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2914 where
2915 I: IntoIterator<Item = S>,
2916 S: Into<String>,
2917 {
2918 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2919 self
2920 }
2921
2922 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2924 where
2925 I: IntoIterator<Item = S>,
2926 S: Into<String>,
2927 {
2928 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2929 self
2930 }
2931
2932 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
2934 self.mcp_servers = Some(servers);
2935 self
2936 }
2937
2938 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2941 self.mcp_oauth_token_storage = Some(mode.into());
2942 self
2943 }
2944
2945 pub fn with_embedding_cache_storage(
2947 mut self,
2948 embedding_cache_storage: impl Into<String>,
2949 ) -> Self {
2950 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2951 self
2952 }
2953
2954 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2956 self.enable_config_discovery = Some(enable);
2957 self
2958 }
2959
2960 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2962 self.skip_embedding_retrieval = Some(value);
2963 self
2964 }
2965
2966 pub fn with_organization_custom_instructions(
2968 mut self,
2969 instructions: impl Into<String>,
2970 ) -> Self {
2971 self.organization_custom_instructions = Some(instructions.into());
2972 self
2973 }
2974
2975 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2977 self.enable_on_demand_instruction_discovery = Some(value);
2978 self
2979 }
2980
2981 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2983 self.enable_file_hooks = Some(value);
2984 self
2985 }
2986
2987 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2989 self.enable_host_git_operations = Some(value);
2990 self
2991 }
2992
2993 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2995 self.enable_session_store = Some(value);
2996 self
2997 }
2998
2999 pub fn with_enable_skills(mut self, value: bool) -> Self {
3001 self.enable_skills = Some(value);
3002 self
3003 }
3004
3005 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3011 self.enable_mcp_apps = Some(enable);
3012 self
3013 }
3014
3015 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3017 where
3018 I: IntoIterator<Item = P>,
3019 P: Into<PathBuf>,
3020 {
3021 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3022 self
3023 }
3024
3025 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3029 where
3030 I: IntoIterator<Item = P>,
3031 P: Into<PathBuf>,
3032 {
3033 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3034 self
3035 }
3036
3037 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3039 where
3040 I: IntoIterator<Item = P>,
3041 P: Into<PathBuf>,
3042 {
3043 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3044 self
3045 }
3046
3047 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3049 self.large_output = Some(config);
3050 self
3051 }
3052
3053 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3055 where
3056 I: IntoIterator<Item = S>,
3057 S: Into<String>,
3058 {
3059 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3060 self
3061 }
3062
3063 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3065 mut self,
3066 agents: I,
3067 ) -> Self {
3068 self.custom_agents = Some(agents.into_iter().collect());
3069 self
3070 }
3071
3072 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3074 self.default_agent = Some(agent);
3075 self
3076 }
3077
3078 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3080 self.agent = Some(name.into());
3081 self
3082 }
3083
3084 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3086 self.infinite_sessions = Some(config);
3087 self
3088 }
3089
3090 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3092 self.provider = Some(provider);
3093 self
3094 }
3095
3096 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3100 self.enable_session_telemetry = Some(enable);
3101 self
3102 }
3103
3104 pub fn with_model_capabilities(
3106 mut self,
3107 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3108 ) -> Self {
3109 self.model_capabilities = Some(capabilities);
3110 self
3111 }
3112
3113 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3115 self.memory = Some(memory);
3116 self
3117 }
3118
3119 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3121 self.config_directory = Some(dir.into());
3122 self
3123 }
3124
3125 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3127 self.working_directory = Some(dir.into());
3128 self
3129 }
3130
3131 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3135 self.github_token = Some(token.into());
3136 self
3137 }
3138
3139 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3141 self.include_sub_agent_streaming_events = Some(include);
3142 self
3143 }
3144
3145 pub fn with_remote_session(
3147 mut self,
3148 mode: crate::generated::api_types::RemoteSessionMode,
3149 ) -> Self {
3150 self.remote_session = Some(mode);
3151 self
3152 }
3153
3154 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
3157 self.suppress_resume_event = Some(suppress);
3158 self
3159 }
3160
3161 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
3167 self.continue_pending_work = Some(continue_pending);
3168 self
3169 }
3170
3171 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3173 self.skip_custom_instructions = Some(value);
3174 self
3175 }
3176
3177 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3179 self.custom_agents_local_only = Some(value);
3180 self
3181 }
3182
3183 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3185 self.coauthor_enabled = Some(value);
3186 self
3187 }
3188
3189 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3191 self.manage_schedule_enabled = Some(value);
3192 self
3193 }
3194}
3195
3196#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3202#[serde(rename_all = "camelCase")]
3203#[non_exhaustive]
3204pub struct SystemMessageConfig {
3205 #[serde(skip_serializing_if = "Option::is_none")]
3207 pub mode: Option<String>,
3208 #[serde(skip_serializing_if = "Option::is_none")]
3210 pub content: Option<String>,
3211 #[serde(skip_serializing_if = "Option::is_none")]
3213 pub sections: Option<HashMap<String, SectionOverride>>,
3214}
3215
3216impl SystemMessageConfig {
3217 pub fn new() -> Self {
3220 Self::default()
3221 }
3222
3223 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
3226 self.mode = Some(mode.into());
3227 self
3228 }
3229
3230 pub fn with_content(mut self, content: impl Into<String>) -> Self {
3233 self.content = Some(content.into());
3234 self
3235 }
3236
3237 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
3239 self.sections = Some(sections);
3240 self
3241 }
3242}
3243
3244#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3250#[serde(rename_all = "camelCase")]
3251pub struct SectionOverride {
3252 #[serde(skip_serializing_if = "Option::is_none")]
3254 pub action: Option<String>,
3255 #[serde(skip_serializing_if = "Option::is_none")]
3257 pub content: Option<String>,
3258}
3259
3260#[derive(Debug, Clone, Serialize, Deserialize)]
3262#[serde(rename_all = "camelCase")]
3263pub struct CreateSessionResult {
3264 pub session_id: SessionId,
3266 #[serde(skip_serializing_if = "Option::is_none")]
3268 pub workspace_path: Option<PathBuf>,
3269 #[serde(default, alias = "remote_url")]
3271 pub remote_url: Option<String>,
3272 #[serde(skip_serializing_if = "Option::is_none")]
3274 pub capabilities: Option<SessionCapabilities>,
3275}
3276
3277#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3279#[serde(rename_all = "camelCase")]
3280pub(crate) struct ResumeSessionResult {
3281 #[serde(default)]
3283 pub session_id: Option<SessionId>,
3284 #[serde(default, skip_serializing_if = "Option::is_none")]
3286 pub workspace_path: Option<PathBuf>,
3287 #[serde(default, alias = "remote_url")]
3289 pub remote_url: Option<String>,
3290 #[serde(default, skip_serializing_if = "Option::is_none")]
3292 pub capabilities: Option<SessionCapabilities>,
3293 #[serde(
3295 default,
3296 alias = "openCanvasInstances",
3297 skip_serializing_if = "Option::is_none"
3298 )]
3299 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3300}
3301
3302#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3304#[serde(rename_all = "lowercase")]
3305pub enum LogLevel {
3306 #[default]
3308 Info,
3309 Warning,
3311 Error,
3313}
3314
3315#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
3320#[serde(rename_all = "camelCase")]
3321pub struct LogOptions {
3322 #[serde(skip_serializing_if = "Option::is_none")]
3324 pub level: Option<LogLevel>,
3325 #[serde(skip_serializing_if = "Option::is_none")]
3328 pub ephemeral: Option<bool>,
3329}
3330
3331impl LogOptions {
3332 pub fn with_level(mut self, level: LogLevel) -> Self {
3334 self.level = Some(level);
3335 self
3336 }
3337
3338 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
3340 self.ephemeral = Some(ephemeral);
3341 self
3342 }
3343}
3344
3345#[derive(Debug, Clone, Default)]
3349pub struct SetModelOptions {
3350 pub reasoning_effort: Option<String>,
3353 pub reasoning_summary: Option<ReasoningSummary>,
3357 pub context_tier: Option<ContextTier>,
3360 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3364}
3365
3366impl SetModelOptions {
3367 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3369 self.reasoning_effort = Some(effort.into());
3370 self
3371 }
3372
3373 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3375 self.reasoning_summary = Some(summary);
3376 self
3377 }
3378
3379 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
3381 self.context_tier = Some(tier);
3382 self
3383 }
3384
3385 pub fn with_model_capabilities(
3387 mut self,
3388 caps: crate::generated::api_types::ModelCapabilitiesOverride,
3389 ) -> Self {
3390 self.model_capabilities = Some(caps);
3391 self
3392 }
3393}
3394
3395#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
3402#[serde(rename_all = "camelCase")]
3403pub struct PingResponse {
3404 #[serde(default)]
3406 pub message: String,
3407 #[serde(default)]
3409 pub timestamp: String,
3410 #[serde(skip_serializing_if = "Option::is_none")]
3412 pub protocol_version: Option<u32>,
3413}
3414
3415#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3417#[serde(rename_all = "camelCase")]
3418pub struct AttachmentLineRange {
3419 pub start: u32,
3421 pub end: u32,
3423}
3424
3425#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3427#[serde(rename_all = "camelCase")]
3428pub struct AttachmentSelectionPosition {
3429 pub line: u32,
3431 pub character: u32,
3433}
3434
3435#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3437#[serde(rename_all = "camelCase")]
3438pub struct AttachmentSelectionRange {
3439 pub start: AttachmentSelectionPosition,
3441 pub end: AttachmentSelectionPosition,
3443}
3444
3445#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3447#[serde(rename_all = "snake_case")]
3448#[non_exhaustive]
3449pub enum GitHubReferenceType {
3450 Issue,
3452 Pr,
3454 Discussion,
3456}
3457
3458#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3460#[serde(
3461 tag = "type",
3462 rename_all = "camelCase",
3463 rename_all_fields = "camelCase"
3464)]
3465#[non_exhaustive]
3466pub enum Attachment {
3467 File {
3469 path: PathBuf,
3471 #[serde(skip_serializing_if = "Option::is_none")]
3473 display_name: Option<String>,
3474 #[serde(skip_serializing_if = "Option::is_none")]
3476 line_range: Option<AttachmentLineRange>,
3477 },
3478 Directory {
3480 path: PathBuf,
3482 #[serde(skip_serializing_if = "Option::is_none")]
3484 display_name: Option<String>,
3485 },
3486 Selection {
3488 file_path: PathBuf,
3490 text: String,
3492 #[serde(skip_serializing_if = "Option::is_none")]
3494 display_name: Option<String>,
3495 selection: AttachmentSelectionRange,
3497 },
3498 Blob {
3500 data: String,
3502 mime_type: String,
3504 #[serde(skip_serializing_if = "Option::is_none")]
3506 display_name: Option<String>,
3507 },
3508 #[serde(rename = "github_reference")]
3510 GitHubReference {
3511 number: u64,
3513 title: String,
3515 reference_type: GitHubReferenceType,
3517 state: String,
3519 url: String,
3521 },
3522}
3523
3524impl Attachment {
3525 pub fn display_name(&self) -> Option<&str> {
3527 match self {
3528 Self::File { display_name, .. }
3529 | Self::Directory { display_name, .. }
3530 | Self::Selection { display_name, .. }
3531 | Self::Blob { display_name, .. } => display_name.as_deref(),
3532 Self::GitHubReference { .. } => None,
3533 }
3534 }
3535
3536 pub fn label(&self) -> Option<String> {
3538 if let Some(display_name) = self
3539 .display_name()
3540 .map(str::trim)
3541 .filter(|name| !name.is_empty())
3542 {
3543 return Some(display_name.to_string());
3544 }
3545
3546 match self {
3547 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
3548 format!("#{}", number)
3549 } else {
3550 title.trim().to_string()
3551 }),
3552 _ => self.derived_display_name(),
3553 }
3554 }
3555
3556 pub fn ensure_display_name(&mut self) {
3558 if self
3559 .display_name()
3560 .map(str::trim)
3561 .is_some_and(|name| !name.is_empty())
3562 {
3563 return;
3564 }
3565
3566 let Some(derived_display_name) = self.derived_display_name() else {
3567 return;
3568 };
3569
3570 match self {
3571 Self::File { display_name, .. }
3572 | Self::Directory { display_name, .. }
3573 | Self::Selection { display_name, .. }
3574 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
3575 Self::GitHubReference { .. } => {}
3576 }
3577 }
3578
3579 fn derived_display_name(&self) -> Option<String> {
3580 match self {
3581 Self::File { path, .. } | Self::Directory { path, .. } => {
3582 Some(attachment_name_from_path(path))
3583 }
3584 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
3585 Self::Blob { .. } => Some("attachment".to_string()),
3586 Self::GitHubReference { .. } => None,
3587 }
3588 }
3589}
3590
3591fn attachment_name_from_path(path: &Path) -> String {
3592 path.file_name()
3593 .map(|name| name.to_string_lossy().into_owned())
3594 .filter(|name| !name.is_empty())
3595 .unwrap_or_else(|| {
3596 let full = path.to_string_lossy();
3597 if full.is_empty() {
3598 "attachment".to_string()
3599 } else {
3600 full.into_owned()
3601 }
3602 })
3603}
3604
3605pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
3607 for attachment in attachments {
3608 attachment.ensure_display_name();
3609 }
3610}
3611
3612#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3617#[serde(rename_all = "lowercase")]
3618#[non_exhaustive]
3619pub enum DeliveryMode {
3620 Enqueue,
3622 Immediate,
3624}
3625
3626#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3631#[serde(rename_all = "lowercase")]
3632#[non_exhaustive]
3633pub enum AgentMode {
3634 Interactive,
3636 Plan,
3638 Autopilot,
3640 Shell,
3642}
3643
3644#[derive(Debug, Clone)]
3673#[non_exhaustive]
3674pub struct MessageOptions {
3675 pub prompt: String,
3677 pub mode: Option<DeliveryMode>,
3683 pub agent_mode: Option<AgentMode>,
3687 pub attachments: Option<Vec<Attachment>>,
3689 pub wait_timeout: Option<Duration>,
3692 pub request_headers: Option<HashMap<String, String>>,
3696 pub traceparent: Option<String>,
3703 pub tracestate: Option<String>,
3707 pub display_prompt: Option<String>,
3709}
3710
3711impl MessageOptions {
3712 pub fn new(prompt: impl Into<String>) -> Self {
3714 Self {
3715 prompt: prompt.into(),
3716 mode: None,
3717 agent_mode: None,
3718 attachments: None,
3719 wait_timeout: None,
3720 request_headers: None,
3721 traceparent: None,
3722 tracestate: None,
3723 display_prompt: None,
3724 }
3725 }
3726
3727 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
3733 self.mode = Some(mode);
3734 self
3735 }
3736
3737 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
3741 self.agent_mode = Some(agent_mode);
3742 self
3743 }
3744
3745 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
3747 self.attachments = Some(attachments);
3748 self
3749 }
3750
3751 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
3753 self.wait_timeout = Some(timeout);
3754 self
3755 }
3756
3757 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
3759 self.request_headers = Some(headers);
3760 self
3761 }
3762
3763 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
3768 self.traceparent = ctx.traceparent;
3769 self.tracestate = ctx.tracestate;
3770 self
3771 }
3772
3773 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
3775 self.traceparent = Some(traceparent.into());
3776 self
3777 }
3778
3779 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
3781 self.tracestate = Some(tracestate.into());
3782 self
3783 }
3784
3785 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
3787 self.display_prompt = Some(display_prompt.into());
3788 self
3789 }
3790}
3791
3792impl From<&str> for MessageOptions {
3793 fn from(prompt: &str) -> Self {
3794 Self::new(prompt)
3795 }
3796}
3797
3798impl From<String> for MessageOptions {
3799 fn from(prompt: String) -> Self {
3800 Self::new(prompt)
3801 }
3802}
3803
3804impl From<&String> for MessageOptions {
3805 fn from(prompt: &String) -> Self {
3806 Self::new(prompt.clone())
3807 }
3808}
3809
3810#[derive(Debug, Clone, Serialize, Deserialize)]
3812#[serde(rename_all = "camelCase")]
3813#[non_exhaustive]
3814pub struct GetStatusResponse {
3815 pub version: String,
3817 pub protocol_version: u32,
3819}
3820
3821#[derive(Debug, Clone, Serialize, Deserialize)]
3823#[serde(rename_all = "camelCase")]
3824#[non_exhaustive]
3825pub struct GetAuthStatusResponse {
3826 pub is_authenticated: bool,
3828 #[serde(skip_serializing_if = "Option::is_none")]
3831 pub auth_type: Option<String>,
3832 #[serde(skip_serializing_if = "Option::is_none")]
3834 pub host: Option<String>,
3835 #[serde(skip_serializing_if = "Option::is_none")]
3837 pub login: Option<String>,
3838 #[serde(skip_serializing_if = "Option::is_none")]
3840 pub status_message: Option<String>,
3841}
3842
3843#[derive(Debug, Clone, Serialize, Deserialize)]
3847#[serde(rename_all = "camelCase")]
3848pub struct SessionEventNotification {
3849 pub session_id: SessionId,
3851 pub event: SessionEvent,
3853}
3854
3855#[derive(Debug, Clone, Serialize, Deserialize)]
3862#[serde(rename_all = "camelCase")]
3863pub struct SessionEvent {
3864 pub id: String,
3866 pub timestamp: String,
3868 pub parent_id: Option<String>,
3870 #[serde(skip_serializing_if = "Option::is_none")]
3872 pub ephemeral: Option<bool>,
3873 #[serde(skip_serializing_if = "Option::is_none")]
3876 pub agent_id: Option<String>,
3877 #[serde(skip_serializing_if = "Option::is_none")]
3879 pub debug_cli_received_at_ms: Option<i64>,
3880 #[serde(skip_serializing_if = "Option::is_none")]
3882 pub debug_ws_forwarded_at_ms: Option<i64>,
3883 #[serde(rename = "type")]
3885 pub event_type: String,
3886 pub data: Value,
3888}
3889
3890impl SessionEvent {
3891 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
3896 use serde::de::IntoDeserializer;
3897 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
3898 self.event_type.as_str().into_deserializer();
3899 crate::generated::SessionEventType::deserialize(deserializer)
3900 .unwrap_or(crate::generated::SessionEventType::Unknown)
3901 }
3902
3903 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
3909 serde_json::from_value(self.data.clone()).ok()
3910 }
3911
3912 pub fn is_transient_error(&self) -> bool {
3916 self.event_type == "session.error"
3917 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
3918 }
3919}
3920
3921#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3926#[serde(rename_all = "camelCase")]
3927#[non_exhaustive]
3928pub struct ToolInvocation {
3929 pub session_id: SessionId,
3931 pub tool_call_id: String,
3933 pub tool_name: String,
3935 pub arguments: Value,
3937 #[serde(default, skip_serializing_if = "Option::is_none")]
3942 pub traceparent: Option<String>,
3943 #[serde(default, skip_serializing_if = "Option::is_none")]
3946 pub tracestate: Option<String>,
3947}
3948
3949impl ToolInvocation {
3950 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
3971 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
3972 }
3973
3974 pub fn trace_context(&self) -> TraceContext {
3977 TraceContext {
3978 traceparent: self.traceparent.clone(),
3979 tracestate: self.tracestate.clone(),
3980 }
3981 }
3982}
3983
3984#[derive(Debug, Clone, Serialize, Deserialize)]
3986#[serde(rename_all = "camelCase")]
3987pub struct ToolBinaryResult {
3988 pub data: String,
3990 pub mime_type: String,
3992 pub r#type: String,
3994 #[serde(default, skip_serializing_if = "Option::is_none")]
3996 pub description: Option<String>,
3997}
3998
3999#[derive(Debug, Clone, Serialize, Deserialize)]
4001#[serde(rename_all = "camelCase")]
4002pub struct ToolResultExpanded {
4003 pub text_result_for_llm: String,
4005 pub result_type: String,
4007 #[serde(default, skip_serializing_if = "Option::is_none")]
4009 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
4010 #[serde(skip_serializing_if = "Option::is_none")]
4012 pub session_log: Option<String>,
4013 #[serde(skip_serializing_if = "Option::is_none")]
4015 pub error: Option<String>,
4016 #[serde(default, skip_serializing_if = "Option::is_none")]
4018 pub tool_telemetry: Option<HashMap<String, Value>>,
4019}
4020
4021#[derive(Debug, Clone, Serialize, Deserialize)]
4023#[serde(untagged)]
4024#[non_exhaustive]
4025pub enum ToolResult {
4026 Text(String),
4028 Expanded(ToolResultExpanded),
4030}
4031
4032#[derive(Debug, Clone, Serialize, Deserialize)]
4034#[serde(rename_all = "camelCase")]
4035pub struct ToolResultResponse {
4036 pub result: ToolResult,
4038}
4039
4040#[derive(Debug, Clone, Serialize, Deserialize)]
4042#[serde(rename_all = "camelCase")]
4043pub struct SessionMetadata {
4044 pub session_id: SessionId,
4046 pub start_time: String,
4048 pub modified_time: String,
4050 #[serde(skip_serializing_if = "Option::is_none")]
4052 pub summary: Option<String>,
4053 pub is_remote: bool,
4055}
4056
4057#[derive(Debug, Clone, Serialize, Deserialize)]
4059#[serde(rename_all = "camelCase")]
4060pub struct ListSessionsResponse {
4061 pub sessions: Vec<SessionMetadata>,
4063}
4064
4065#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4069#[serde(rename_all = "camelCase")]
4070pub struct SessionListFilter {
4071 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
4073 pub working_directory: Option<String>,
4074 #[serde(default, skip_serializing_if = "Option::is_none")]
4076 pub git_root: Option<String>,
4077 #[serde(default, skip_serializing_if = "Option::is_none")]
4079 pub repository: Option<String>,
4080 #[serde(default, skip_serializing_if = "Option::is_none")]
4082 pub branch: Option<String>,
4083}
4084
4085#[derive(Debug, Clone, Serialize, Deserialize)]
4087#[serde(rename_all = "camelCase")]
4088pub struct GetSessionMetadataResponse {
4089 #[serde(skip_serializing_if = "Option::is_none")]
4091 pub session: Option<SessionMetadata>,
4092}
4093
4094#[derive(Debug, Clone, Serialize, Deserialize)]
4096#[serde(rename_all = "camelCase")]
4097pub struct GetLastSessionIdResponse {
4098 #[serde(skip_serializing_if = "Option::is_none")]
4100 pub session_id: Option<SessionId>,
4101}
4102
4103#[derive(Debug, Clone, Serialize, Deserialize)]
4105#[serde(rename_all = "camelCase")]
4106pub struct GetForegroundSessionResponse {
4107 #[serde(skip_serializing_if = "Option::is_none")]
4109 pub session_id: Option<SessionId>,
4110}
4111
4112#[derive(Debug, Clone, Serialize, Deserialize)]
4114#[serde(rename_all = "camelCase")]
4115pub struct GetMessagesResponse {
4116 pub events: Vec<SessionEvent>,
4118}
4119
4120#[derive(Debug, Clone, Serialize, Deserialize)]
4122#[serde(rename_all = "camelCase")]
4123pub struct ElicitationResult {
4124 pub action: String,
4126 #[serde(skip_serializing_if = "Option::is_none")]
4128 pub content: Option<Value>,
4129}
4130
4131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4137#[serde(rename_all = "camelCase")]
4138#[non_exhaustive]
4139pub enum ElicitationMode {
4140 Form,
4142 Url,
4144 #[serde(other)]
4146 Unknown,
4147}
4148
4149#[derive(Debug, Clone, Serialize, Deserialize)]
4156#[serde(rename_all = "camelCase")]
4157pub struct ElicitationRequest {
4158 pub message: String,
4160 #[serde(skip_serializing_if = "Option::is_none")]
4162 pub requested_schema: Option<Value>,
4163 #[serde(skip_serializing_if = "Option::is_none")]
4165 pub mode: Option<ElicitationMode>,
4166 #[serde(skip_serializing_if = "Option::is_none")]
4168 pub elicitation_source: Option<String>,
4169 #[serde(skip_serializing_if = "Option::is_none")]
4171 pub url: Option<String>,
4172}
4173
4174#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4179#[serde(rename_all = "camelCase")]
4180pub struct SessionCapabilities {
4181 #[serde(skip_serializing_if = "Option::is_none")]
4183 pub ui: Option<UiCapabilities>,
4184}
4185
4186#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4188#[serde(rename_all = "camelCase")]
4189pub struct UiCapabilities {
4190 #[serde(skip_serializing_if = "Option::is_none")]
4192 pub elicitation: Option<bool>,
4193 #[serde(skip_serializing_if = "Option::is_none")]
4204 pub mcp_apps: Option<bool>,
4205 #[serde(skip_serializing_if = "Option::is_none")]
4207 pub canvases: Option<bool>,
4208}
4209
4210#[derive(Debug, Clone, Default)]
4212pub struct UiInputOptions<'a> {
4213 pub title: Option<&'a str>,
4215 pub description: Option<&'a str>,
4217 pub min_length: Option<u64>,
4219 pub max_length: Option<u64>,
4221 pub format: Option<InputFormat>,
4223 pub default: Option<&'a str>,
4225}
4226
4227#[derive(Debug, Clone, Copy)]
4229#[non_exhaustive]
4230pub enum InputFormat {
4231 Email,
4233 Uri,
4235 Date,
4237 DateTime,
4239}
4240
4241impl InputFormat {
4242 pub fn as_str(&self) -> &'static str {
4244 match self {
4245 Self::Email => "email",
4246 Self::Uri => "uri",
4247 Self::Date => "date",
4248 Self::DateTime => "date-time",
4249 }
4250 }
4251}
4252
4253pub use crate::generated::api_types::{
4258 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
4259 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
4260 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
4261 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
4262};
4263
4264#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4270#[serde(rename_all = "kebab-case")]
4271#[non_exhaustive]
4272pub enum PermissionRequestKind {
4273 Shell,
4275 Write,
4277 Read,
4279 Url,
4281 Mcp,
4283 CustomTool,
4285 Memory,
4287 Hook,
4289 #[serde(other)]
4292 Unknown,
4293}
4294
4295#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4301#[serde(rename_all = "camelCase")]
4302pub struct PermissionRequestData {
4303 #[serde(default, skip_serializing_if = "Option::is_none")]
4307 pub kind: Option<PermissionRequestKind>,
4308 #[serde(default, skip_serializing_if = "Option::is_none")]
4311 pub tool_call_id: Option<String>,
4312 #[serde(flatten)]
4315 pub extra: Value,
4316}
4317
4318#[derive(Debug, Clone, Serialize, Deserialize)]
4320#[serde(rename_all = "camelCase")]
4321pub struct ExitPlanModeData {
4322 #[serde(default)]
4324 pub summary: String,
4325 #[serde(default, skip_serializing_if = "Option::is_none")]
4327 pub plan_content: Option<String>,
4328 #[serde(default)]
4330 pub actions: Vec<String>,
4331 #[serde(default = "default_recommended_action")]
4333 pub recommended_action: String,
4334}
4335
4336fn default_recommended_action() -> String {
4337 "autopilot".to_string()
4338}
4339
4340impl Default for ExitPlanModeData {
4341 fn default() -> Self {
4342 Self {
4343 summary: String::new(),
4344 plan_content: None,
4345 actions: Vec::new(),
4346 recommended_action: default_recommended_action(),
4347 }
4348 }
4349}
4350
4351#[cfg(test)]
4352mod tests {
4353 use std::path::PathBuf;
4354
4355 use serde_json::json;
4356
4357 use super::{
4358 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
4359 AttachmentSelectionRange, ConnectionState, CustomAgentConfig, DeliveryMode, ExtensionInfo,
4360 GitHubReferenceType, InfiniteSessionConfig, LargeToolOutputConfig, MemoryConfiguration,
4361 ProviderConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
4362 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
4363 ToolResultResponse, ensure_attachment_display_names,
4364 };
4365 use crate::generated::session_events::TypedSessionEvent;
4366
4367 #[test]
4368 fn tool_builder_composes() {
4369 let tool = Tool::new("greet")
4370 .with_description("Say hello")
4371 .with_namespaced_name("hello/greet")
4372 .with_instructions("Pass the user's name")
4373 .with_parameters(json!({
4374 "type": "object",
4375 "properties": { "name": { "type": "string" } },
4376 "required": ["name"]
4377 }))
4378 .with_overrides_built_in_tool(true)
4379 .with_skip_permission(true);
4380 assert_eq!(tool.name, "greet");
4381 assert_eq!(tool.description, "Say hello");
4382 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
4383 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
4384 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
4385 assert!(tool.overrides_built_in_tool);
4386 assert!(tool.skip_permission);
4387 }
4388
4389 #[test]
4390 fn tool_defer_serialization() {
4391 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
4392 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
4393 let value = serde_json::to_value(&tool).unwrap();
4394 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
4395
4396 let plain = Tool::new("plain");
4397 let value = serde_json::to_value(&plain).unwrap();
4398 assert!(value.get("defer").is_none());
4399 }
4400
4401 #[test]
4402 fn custom_agent_config_builder_with_model() {
4403 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
4404 .with_model("claude-haiku-4.5")
4405 .with_display_name("My Agent");
4406 assert_eq!(agent.name, "my-agent");
4407 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
4408 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
4409 }
4410
4411 #[test]
4412 fn custom_agent_config_serializes_model() {
4413 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
4414 let wire = serde_json::to_value(&agent).unwrap();
4415 assert_eq!(wire["model"], "claude-haiku-4.5");
4416 assert_eq!(wire["name"], "model-agent");
4417 }
4418
4419 #[test]
4420 fn custom_agent_config_omits_model_when_none() {
4421 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
4422 let wire = serde_json::to_value(&agent).unwrap();
4423 assert!(wire.get("model").is_none());
4424 }
4425
4426 #[test]
4427 #[should_panic(expected = "tool parameter schema must be a JSON object")]
4428 fn tool_with_parameters_panics_on_non_object_value() {
4429 let _ = Tool::new("noop").with_parameters(json!(null));
4430 }
4431
4432 #[test]
4433 fn tool_result_expanded_serializes_binary_results_for_llm() {
4434 let response = ToolResultResponse {
4435 result: ToolResult::Expanded(ToolResultExpanded {
4436 text_result_for_llm: "rendered chart".to_string(),
4437 result_type: "success".to_string(),
4438 binary_results_for_llm: Some(vec![ToolBinaryResult {
4439 data: "aW1n".to_string(),
4440 mime_type: "image/png".to_string(),
4441 r#type: "image".to_string(),
4442 description: Some("chart preview".to_string()),
4443 }]),
4444 session_log: None,
4445 error: None,
4446 tool_telemetry: None,
4447 }),
4448 };
4449
4450 let wire = serde_json::to_value(&response).unwrap();
4451
4452 assert_eq!(
4453 wire,
4454 json!({
4455 "result": {
4456 "textResultForLlm": "rendered chart",
4457 "resultType": "success",
4458 "binaryResultsForLlm": [
4459 {
4460 "data": "aW1n",
4461 "mimeType": "image/png",
4462 "type": "image",
4463 "description": "chart preview"
4464 }
4465 ]
4466 }
4467 })
4468 );
4469 }
4470
4471 #[test]
4472 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
4473 let response = ToolResultResponse {
4474 result: ToolResult::Expanded(ToolResultExpanded {
4475 text_result_for_llm: "ok".to_string(),
4476 result_type: "success".to_string(),
4477 binary_results_for_llm: None,
4478 session_log: None,
4479 error: None,
4480 tool_telemetry: None,
4481 }),
4482 };
4483
4484 let wire = serde_json::to_value(&response).unwrap();
4485
4486 assert_eq!(wire["result"]["textResultForLlm"], "ok");
4487 assert!(wire["result"].get("binaryResultsForLlm").is_none());
4488 }
4489
4490 #[test]
4491 fn session_config_default_wire_flags_off_without_handlers() {
4492 let cfg = SessionConfig::default();
4493 assert_eq!(cfg.mcp_oauth_token_storage, None);
4494 let (wire, _runtime) = cfg
4498 .into_wire(Some(SessionId::from("default-flags")))
4499 .expect("default config has no duplicate handlers");
4500 assert!(!wire.request_user_input);
4501 assert!(!wire.request_permission);
4502 assert!(!wire.request_elicitation);
4503 assert!(!wire.request_exit_plan_mode);
4504 assert!(!wire.request_auto_mode_switch);
4505 assert!(!wire.hooks);
4506 assert!(!wire.request_mcp_apps);
4507 }
4508
4509 #[test]
4510 fn resume_session_config_new_wire_flags_off_without_handlers() {
4511 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
4512 assert_eq!(cfg.mcp_oauth_token_storage, None);
4513 let (wire, _runtime) = cfg
4514 .into_wire()
4515 .expect("default resume config has no duplicate handlers");
4516 assert!(!wire.request_user_input);
4517 assert!(!wire.request_permission);
4518 assert!(!wire.request_elicitation);
4519 assert!(!wire.request_exit_plan_mode);
4520 assert!(!wire.request_auto_mode_switch);
4521 assert!(!wire.hooks);
4522 assert!(!wire.request_mcp_apps);
4523 }
4524
4525 #[test]
4526 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
4527 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
4528 assert_eq!(cfg.enable_mcp_apps, Some(true));
4529
4530 let (wire, _runtime) = cfg
4531 .into_wire(Some(SessionId::from("enable-mcp-apps")))
4532 .expect("enable_mcp_apps config has no duplicate handlers");
4533 assert!(wire.request_mcp_apps);
4534
4535 let json = serde_json::to_value(&wire).unwrap();
4536 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
4537 }
4538
4539 #[test]
4540 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
4541 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
4542 .with_enable_mcp_apps(true);
4543 assert_eq!(cfg.enable_mcp_apps, Some(true));
4544
4545 let (wire, _runtime) = cfg
4546 .into_wire()
4547 .expect("resume enable_mcp_apps config has no duplicate handlers");
4548 assert!(wire.request_mcp_apps);
4549
4550 let json = serde_json::to_value(&wire).unwrap();
4551 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
4552 }
4553
4554 #[test]
4555 fn memory_configuration_constructors_and_serde() {
4556 assert!(MemoryConfiguration::enabled().enabled);
4557 assert!(!MemoryConfiguration::disabled().enabled);
4558 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
4559
4560 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
4561 assert_eq!(json, serde_json::json!({ "enabled": true }));
4562 }
4563
4564 #[test]
4565 fn session_config_with_memory_serializes() {
4566 let (wire, _runtime) = SessionConfig::default()
4567 .with_memory(MemoryConfiguration::enabled())
4568 .into_wire(Some(SessionId::from("memory-on")))
4569 .expect("no duplicate handlers");
4570 let json = serde_json::to_value(&wire).unwrap();
4571 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
4572
4573 let (wire_off, _) = SessionConfig::default()
4574 .with_memory(MemoryConfiguration::disabled())
4575 .into_wire(Some(SessionId::from("memory-off")))
4576 .expect("no duplicate handlers");
4577 let json_off = serde_json::to_value(&wire_off).unwrap();
4578 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
4579
4580 let (empty_wire, _) = SessionConfig::default()
4582 .into_wire(Some(SessionId::from("memory-unset")))
4583 .expect("no duplicate handlers");
4584 let empty_json = serde_json::to_value(&empty_wire).unwrap();
4585 assert!(empty_json.get("memory").is_none());
4586 }
4587
4588 #[test]
4589 fn resume_session_config_with_memory_serializes() {
4590 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
4591 .with_memory(MemoryConfiguration::enabled())
4592 .into_wire()
4593 .expect("no duplicate handlers");
4594 let json = serde_json::to_value(&wire).unwrap();
4595 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
4596
4597 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
4599 .into_wire()
4600 .expect("no duplicate handlers");
4601 let empty_json = serde_json::to_value(&empty_wire).unwrap();
4602 assert!(empty_json.get("memory").is_none());
4603 }
4604
4605 #[test]
4606 #[allow(clippy::field_reassign_with_default)]
4607 fn session_config_into_wire_serializes_bucket_b_fields() {
4608 use std::path::PathBuf;
4609
4610 use super::{CloudSessionOptions, CloudSessionRepository};
4611
4612 let mut cfg = SessionConfig::default();
4613 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
4614 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
4615 cfg.github_token = Some("ghs_secret".to_string());
4616 cfg.include_sub_agent_streaming_events = Some(false);
4617 cfg.enable_session_telemetry = Some(false);
4618 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
4619 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
4620 cfg.enable_on_demand_instruction_discovery = Some(false);
4621 cfg.cloud = Some(CloudSessionOptions::with_repository(
4622 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
4623 ));
4624
4625 let (wire, _runtime) = cfg
4626 .into_wire(Some(SessionId::from("custom-id")))
4627 .expect("no duplicate handlers");
4628 let wire_json = serde_json::to_value(&wire).unwrap();
4629 assert_eq!(wire_json["sessionId"], "custom-id");
4630 assert_eq!(wire_json["configDir"], "/tmp/cfg");
4631 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
4632 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
4633 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
4634 assert_eq!(wire_json["enableSessionTelemetry"], false);
4635 assert_eq!(wire_json["reasoningSummary"], "concise");
4636 assert_eq!(wire_json["remoteSession"], "export");
4637 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
4638 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
4639 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
4640 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
4641
4642 let (empty_wire, _) = SessionConfig::default()
4644 .into_wire(Some(SessionId::from("empty")))
4645 .expect("default has no duplicate handlers");
4646 let empty_json = serde_json::to_value(&empty_wire).unwrap();
4647 assert!(empty_json.get("gitHubToken").is_none());
4648 assert!(empty_json.get("enableSessionTelemetry").is_none());
4649 assert!(empty_json.get("reasoningSummary").is_none());
4650 assert!(empty_json.get("remoteSession").is_none());
4651 assert!(
4652 empty_json
4653 .get("enableOnDemandInstructionDiscovery")
4654 .is_none()
4655 );
4656 assert!(empty_json.get("cloud").is_none());
4657 }
4658
4659 #[test]
4660 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
4661 use std::path::PathBuf;
4662
4663 let cfg = SessionConfig {
4664 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
4665 large_output: Some(
4666 LargeToolOutputConfig::new()
4667 .with_enabled(true)
4668 .with_max_size_bytes(1024)
4669 .with_output_directory(PathBuf::from("/tmp/large-output")),
4670 ),
4671 ..Default::default()
4672 };
4673
4674 let (wire, _) = cfg
4675 .into_wire(Some(SessionId::from("sess-1")))
4676 .expect("no duplicate handlers");
4677 let wire_json = serde_json::to_value(&wire).unwrap();
4678 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
4679 assert_eq!(wire_json["largeOutput"]["enabled"], true);
4680 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
4681 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
4682
4683 let (empty_wire, _) = SessionConfig::default()
4684 .into_wire(Some(SessionId::from("empty")))
4685 .expect("default has no duplicate handlers");
4686 let empty_json = serde_json::to_value(&empty_wire).unwrap();
4687 assert!(empty_json.get("pluginDirectories").is_none());
4688 assert!(empty_json.get("largeOutput").is_none());
4689 }
4690
4691 #[test]
4692 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
4693 use std::path::PathBuf;
4694
4695 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
4696 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
4697 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
4698 cfg.github_token = Some("ghs_secret".to_string());
4699 cfg.include_sub_agent_streaming_events = Some(true);
4700 cfg.enable_session_telemetry = Some(false);
4701 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
4702 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
4703 cfg.enable_on_demand_instruction_discovery = Some(false);
4704
4705 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
4706 let wire_json = serde_json::to_value(&wire).unwrap();
4707 assert_eq!(wire_json["sessionId"], "sess-1");
4708 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
4709 assert_eq!(wire_json["configDir"], "/tmp/cfg");
4710 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
4711 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
4712 assert_eq!(wire_json["enableSessionTelemetry"], false);
4713 assert_eq!(wire_json["reasoningSummary"], "detailed");
4714 assert_eq!(wire_json["remoteSession"], "on");
4715 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
4716
4717 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
4719 .into_wire()
4720 .expect("default resume has no duplicate handlers");
4721 let empty_json = serde_json::to_value(&empty_wire).unwrap();
4722 assert!(empty_json.get("reasoningSummary").is_none());
4723 assert!(empty_json.get("remoteSession").is_none());
4724 assert!(
4725 empty_json
4726 .get("enableOnDemandInstructionDiscovery")
4727 .is_none()
4728 );
4729 }
4730
4731 #[test]
4732 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
4733 use std::path::PathBuf;
4734
4735 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
4736 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
4737 cfg.large_output = Some(
4738 LargeToolOutputConfig::new()
4739 .with_enabled(false)
4740 .with_max_size_bytes(2048)
4741 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
4742 );
4743
4744 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
4745 let wire_json = serde_json::to_value(&wire).unwrap();
4746 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
4747 assert_eq!(wire_json["largeOutput"]["enabled"], false);
4748 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
4749 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
4750
4751 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
4752 .into_wire()
4753 .expect("default resume has no duplicate handlers");
4754 let empty_json = serde_json::to_value(&empty_wire).unwrap();
4755 assert!(empty_json.get("pluginDirectories").is_none());
4756 assert!(empty_json.get("largeOutput").is_none());
4757 }
4758
4759 #[test]
4760 fn session_config_builder_composes() {
4761 use std::collections::HashMap;
4762
4763 let cfg = SessionConfig::default()
4764 .with_session_id(SessionId::from("sess-1"))
4765 .with_model("claude-sonnet-4")
4766 .with_client_name("test-app")
4767 .with_reasoning_effort("medium")
4768 .with_reasoning_summary(ReasoningSummary::Concise)
4769 .with_context_tier("long_context")
4770 .with_streaming(true)
4771 .with_tools([Tool::new("greet")])
4772 .with_available_tools(["bash", "view"])
4773 .with_excluded_tools(["dangerous"])
4774 .with_mcp_servers(HashMap::new())
4775 .with_mcp_oauth_token_storage("persistent")
4776 .with_enable_config_discovery(true)
4777 .with_enable_on_demand_instruction_discovery(true)
4778 .with_skill_directories([PathBuf::from("/tmp/skills")])
4779 .with_disabled_skills(["broken-skill"])
4780 .with_agent("researcher")
4781 .with_config_directory(PathBuf::from("/tmp/config"))
4782 .with_working_directory(PathBuf::from("/tmp/work"))
4783 .with_github_token("ghp_test")
4784 .with_enable_session_telemetry(false)
4785 .with_include_sub_agent_streaming_events(false)
4786 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
4787
4788 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
4789 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
4790 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
4791 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
4792 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
4793 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
4794 assert_eq!(cfg.streaming, Some(true));
4795 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
4796 assert_eq!(
4797 cfg.available_tools.as_deref(),
4798 Some(&["bash".to_string(), "view".to_string()][..])
4799 );
4800 assert_eq!(
4801 cfg.excluded_tools.as_deref(),
4802 Some(&["dangerous".to_string()][..])
4803 );
4804 assert!(cfg.mcp_servers.is_some());
4805 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
4806 assert_eq!(cfg.enable_config_discovery, Some(true));
4807 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
4808 assert_eq!(
4809 cfg.skill_directories.as_deref(),
4810 Some(&[PathBuf::from("/tmp/skills")][..])
4811 );
4812 assert_eq!(
4813 cfg.disabled_skills.as_deref(),
4814 Some(&["broken-skill".to_string()][..])
4815 );
4816 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
4817 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
4818 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
4819 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
4820 assert_eq!(cfg.enable_session_telemetry, Some(false));
4821 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
4822 assert_eq!(
4823 cfg.extension_info,
4824 Some(ExtensionInfo::new("github-app", "counter"))
4825 );
4826 }
4827
4828 #[test]
4829 fn resume_session_config_builder_composes() {
4830 use std::collections::HashMap;
4831
4832 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
4833 .with_client_name("test-app")
4834 .with_reasoning_summary(ReasoningSummary::None)
4835 .with_context_tier("default")
4836 .with_streaming(true)
4837 .with_tools([Tool::new("greet")])
4838 .with_available_tools(["bash", "view"])
4839 .with_excluded_tools(["dangerous"])
4840 .with_mcp_servers(HashMap::new())
4841 .with_mcp_oauth_token_storage("persistent")
4842 .with_enable_config_discovery(true)
4843 .with_enable_on_demand_instruction_discovery(false)
4844 .with_skill_directories([PathBuf::from("/tmp/skills")])
4845 .with_disabled_skills(["broken-skill"])
4846 .with_agent("researcher")
4847 .with_config_directory(PathBuf::from("/tmp/config"))
4848 .with_working_directory(PathBuf::from("/tmp/work"))
4849 .with_github_token("ghp_test")
4850 .with_enable_session_telemetry(false)
4851 .with_include_sub_agent_streaming_events(true)
4852 .with_suppress_resume_event(true)
4853 .with_continue_pending_work(true)
4854 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
4855
4856 assert_eq!(cfg.session_id.as_str(), "sess-2");
4857 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
4858 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
4859 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
4860 assert_eq!(cfg.streaming, Some(true));
4861 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
4862 assert_eq!(
4863 cfg.available_tools.as_deref(),
4864 Some(&["bash".to_string(), "view".to_string()][..])
4865 );
4866 assert_eq!(
4867 cfg.excluded_tools.as_deref(),
4868 Some(&["dangerous".to_string()][..])
4869 );
4870 assert!(cfg.mcp_servers.is_some());
4871 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
4872 assert_eq!(cfg.enable_config_discovery, Some(true));
4873 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
4874 assert_eq!(
4875 cfg.skill_directories.as_deref(),
4876 Some(&[PathBuf::from("/tmp/skills")][..])
4877 );
4878 assert_eq!(
4879 cfg.disabled_skills.as_deref(),
4880 Some(&["broken-skill".to_string()][..])
4881 );
4882 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
4883 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
4884 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
4885 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
4886 assert_eq!(cfg.enable_session_telemetry, Some(false));
4887 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
4888 assert_eq!(cfg.suppress_resume_event, Some(true));
4889 assert_eq!(cfg.continue_pending_work, Some(true));
4890 assert_eq!(
4891 cfg.extension_info,
4892 Some(ExtensionInfo::new("github-app", "counter"))
4893 );
4894 }
4895
4896 #[test]
4900 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
4901 let cfg =
4902 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
4903 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
4904 let json = serde_json::to_value(&wire).unwrap();
4905 assert_eq!(json["continuePendingWork"], true);
4906
4907 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
4909 .into_wire()
4910 .expect("no duplicate handlers");
4911 let json = serde_json::to_value(&wire).unwrap();
4912 assert!(json.get("continuePendingWork").is_none());
4913 }
4914
4915 #[test]
4919 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
4920 let cfg =
4921 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
4922 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
4923 let json = serde_json::to_value(&wire).unwrap();
4924 assert_eq!(json["disableResume"], true);
4925 assert!(json.get("suppressResumeEvent").is_none());
4926 }
4927
4928 #[test]
4931 fn session_config_serializes_instruction_directories_to_camel_case() {
4932 let cfg =
4933 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
4934 let (wire, _) = cfg
4935 .into_wire(Some(SessionId::from("instr-on")))
4936 .expect("no duplicate handlers");
4937 let json = serde_json::to_value(&wire).unwrap();
4938 assert_eq!(
4939 json["instructionDirectories"],
4940 serde_json::json!(["/tmp/instr"])
4941 );
4942
4943 let (wire, _) = SessionConfig::default()
4945 .into_wire(Some(SessionId::from("instr-off")))
4946 .expect("no duplicate handlers");
4947 let json = serde_json::to_value(&wire).unwrap();
4948 assert!(json.get("instructionDirectories").is_none());
4949 }
4950
4951 #[test]
4954 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
4955 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
4956 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
4957 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
4958 let json = serde_json::to_value(&wire).unwrap();
4959 assert_eq!(
4960 json["instructionDirectories"],
4961 serde_json::json!(["/tmp/instr"])
4962 );
4963
4964 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
4965 .into_wire()
4966 .expect("no duplicate handlers");
4967 let json = serde_json::to_value(&wire).unwrap();
4968 assert!(json.get("instructionDirectories").is_none());
4969 }
4970
4971 #[test]
4972 fn custom_agent_config_builder_composes() {
4973 use std::collections::HashMap;
4974
4975 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
4976 .with_display_name("Research Assistant")
4977 .with_description("Investigates technical questions.")
4978 .with_tools(["bash", "view"])
4979 .with_mcp_servers(HashMap::new())
4980 .with_infer(true)
4981 .with_skills(["rust-coding-skill"]);
4982
4983 assert_eq!(cfg.name, "researcher");
4984 assert_eq!(cfg.prompt, "You are a research assistant.");
4985 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
4986 assert_eq!(
4987 cfg.description.as_deref(),
4988 Some("Investigates technical questions.")
4989 );
4990 assert_eq!(
4991 cfg.tools.as_deref(),
4992 Some(&["bash".to_string(), "view".to_string()][..])
4993 );
4994 assert!(cfg.mcp_servers.is_some());
4995 assert_eq!(cfg.infer, Some(true));
4996 assert_eq!(
4997 cfg.skills.as_deref(),
4998 Some(&["rust-coding-skill".to_string()][..])
4999 );
5000 }
5001
5002 #[test]
5003 fn infinite_session_config_builder_composes() {
5004 let cfg = InfiniteSessionConfig::new()
5005 .with_enabled(true)
5006 .with_background_compaction_threshold(0.75)
5007 .with_buffer_exhaustion_threshold(0.92);
5008
5009 assert_eq!(cfg.enabled, Some(true));
5010 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
5011 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
5012 }
5013
5014 #[test]
5015 fn provider_config_builder_composes() {
5016 use std::collections::HashMap;
5017
5018 let mut headers = HashMap::new();
5019 headers.insert("X-Custom".to_string(), "value".to_string());
5020
5021 let cfg = ProviderConfig::new("https://api.example.com")
5022 .with_provider_type("openai")
5023 .with_wire_api("completions")
5024 .with_api_key("sk-test")
5025 .with_bearer_token("bearer-test")
5026 .with_headers(headers)
5027 .with_model_id("gpt-4")
5028 .with_wire_model("azure-gpt-4-deployment")
5029 .with_max_prompt_tokens(8192)
5030 .with_max_output_tokens(2048);
5031
5032 assert_eq!(cfg.base_url, "https://api.example.com");
5033 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
5034 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
5035 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
5036 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
5037 assert_eq!(
5038 cfg.headers
5039 .as_ref()
5040 .and_then(|h| h.get("X-Custom"))
5041 .map(String::as_str),
5042 Some("value"),
5043 );
5044 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
5045 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
5046 assert_eq!(cfg.max_prompt_tokens, Some(8192));
5047 assert_eq!(cfg.max_output_tokens, Some(2048));
5048
5049 let wire = serde_json::to_value(&cfg).unwrap();
5051 assert_eq!(wire["modelId"], "gpt-4");
5052 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
5053 assert_eq!(wire["maxPromptTokens"], 8192);
5054 assert_eq!(wire["maxOutputTokens"], 2048);
5055
5056 let unset = ProviderConfig::new("https://api.example.com");
5057 let wire_unset = serde_json::to_value(&unset).unwrap();
5058 assert!(wire_unset.get("modelId").is_none());
5059 assert!(wire_unset.get("wireModel").is_none());
5060 assert!(wire_unset.get("maxPromptTokens").is_none());
5061 assert!(wire_unset.get("maxOutputTokens").is_none());
5062 }
5063
5064 #[test]
5065 fn system_message_config_builder_composes() {
5066 use std::collections::HashMap;
5067
5068 let cfg = SystemMessageConfig::new()
5069 .with_mode("replace")
5070 .with_content("Custom system message.")
5071 .with_sections(HashMap::new());
5072
5073 assert_eq!(cfg.mode.as_deref(), Some("replace"));
5074 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
5075 assert!(cfg.sections.is_some());
5076 }
5077
5078 #[test]
5079 fn delivery_mode_serializes_to_kebab_case_strings() {
5080 assert_eq!(
5081 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
5082 "\"enqueue\""
5083 );
5084 assert_eq!(
5085 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
5086 "\"immediate\""
5087 );
5088 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
5089 assert_eq!(parsed, DeliveryMode::Immediate);
5090 }
5091
5092 #[test]
5093 fn agent_mode_serializes_to_kebab_case_strings() {
5094 assert_eq!(
5095 serde_json::to_string(&AgentMode::Interactive).unwrap(),
5096 "\"interactive\""
5097 );
5098 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
5099 assert_eq!(
5100 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
5101 "\"autopilot\""
5102 );
5103 assert_eq!(
5104 serde_json::to_string(&AgentMode::Shell).unwrap(),
5105 "\"shell\""
5106 );
5107 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
5108 assert_eq!(parsed, AgentMode::Plan);
5109 }
5110
5111 #[test]
5112 fn connection_state_distinguishes_variants() {
5113 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
5116 }
5117
5118 #[test]
5124 fn session_event_round_trips_agent_id_on_envelope() {
5125 let wire = json!({
5126 "id": "evt-1",
5127 "timestamp": "2026-04-30T12:00:00Z",
5128 "parentId": null,
5129 "agentId": "sub-agent-42",
5130 "type": "assistant.message",
5131 "data": { "message": "hi" }
5132 });
5133
5134 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
5135 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
5136
5137 let roundtripped = serde_json::to_value(&event).unwrap();
5139 assert_eq!(roundtripped["agentId"], "sub-agent-42");
5140
5141 let main_agent_event: SessionEvent = serde_json::from_value(json!({
5143 "id": "evt-2",
5144 "timestamp": "2026-04-30T12:00:01Z",
5145 "parentId": null,
5146 "type": "session.idle",
5147 "data": {}
5148 }))
5149 .unwrap();
5150 assert!(main_agent_event.agent_id.is_none());
5151 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
5152 assert!(roundtripped.get("agentId").is_none());
5153 }
5154
5155 #[test]
5157 fn typed_session_event_round_trips_agent_id_on_envelope() {
5158 let wire = json!({
5159 "id": "evt-1",
5160 "timestamp": "2026-04-30T12:00:00Z",
5161 "parentId": null,
5162 "agentId": "sub-agent-42",
5163 "type": "session.idle",
5164 "data": {}
5165 });
5166
5167 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
5168 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
5169
5170 let roundtripped = serde_json::to_value(&event).unwrap();
5171 assert_eq!(roundtripped["agentId"], "sub-agent-42");
5172 }
5173
5174 #[test]
5175 fn connection_state_variants_compile() {
5176 let _ = ConnectionState::Disconnected;
5180 let _ = ConnectionState::Connecting;
5181 let _ = ConnectionState::Connected;
5182 let _ = ConnectionState::Error;
5183 }
5184
5185 #[test]
5186 fn deserializes_runtime_attachment_variants() {
5187 let attachments: Vec<Attachment> = serde_json::from_value(json!([
5188 {
5189 "type": "file",
5190 "path": "/tmp/file.rs",
5191 "displayName": "file.rs",
5192 "lineRange": { "start": 7, "end": 12 }
5193 },
5194 {
5195 "type": "directory",
5196 "path": "/tmp/project",
5197 "displayName": "project"
5198 },
5199 {
5200 "type": "selection",
5201 "filePath": "/tmp/lib.rs",
5202 "displayName": "lib.rs",
5203 "text": "fn main() {}",
5204 "selection": {
5205 "start": { "line": 1, "character": 2 },
5206 "end": { "line": 3, "character": 4 }
5207 }
5208 },
5209 {
5210 "type": "blob",
5211 "data": "Zm9v",
5212 "mimeType": "image/png",
5213 "displayName": "image.png"
5214 },
5215 {
5216 "type": "github_reference",
5217 "number": 42,
5218 "title": "Fix rendering",
5219 "referenceType": "issue",
5220 "state": "open",
5221 "url": "https://github.com/example/repo/issues/42"
5222 }
5223 ]))
5224 .expect("attachments should deserialize");
5225
5226 assert_eq!(attachments.len(), 5);
5227 assert!(matches!(
5228 &attachments[0],
5229 Attachment::File {
5230 path,
5231 display_name,
5232 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
5233 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
5234 ));
5235 assert!(matches!(
5236 &attachments[1],
5237 Attachment::Directory { path, display_name }
5238 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
5239 ));
5240 assert!(matches!(
5241 &attachments[2],
5242 Attachment::Selection {
5243 file_path,
5244 display_name,
5245 selection:
5246 AttachmentSelectionRange {
5247 start: AttachmentSelectionPosition { line: 1, character: 2 },
5248 end: AttachmentSelectionPosition { line: 3, character: 4 },
5249 },
5250 ..
5251 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
5252 ));
5253 assert!(matches!(
5254 &attachments[3],
5255 Attachment::Blob {
5256 data,
5257 mime_type,
5258 display_name,
5259 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
5260 ));
5261 assert!(matches!(
5262 &attachments[4],
5263 Attachment::GitHubReference {
5264 number: 42,
5265 title,
5266 reference_type: GitHubReferenceType::Issue,
5267 state,
5268 url,
5269 } if title == "Fix rendering"
5270 && state == "open"
5271 && url == "https://github.com/example/repo/issues/42"
5272 ));
5273 }
5274
5275 #[test]
5276 fn ensures_display_names_for_variants_that_support_them() {
5277 let mut attachments = vec![
5278 Attachment::File {
5279 path: PathBuf::from("/tmp/file.rs"),
5280 display_name: None,
5281 line_range: None,
5282 },
5283 Attachment::Selection {
5284 file_path: PathBuf::from("/tmp/src/lib.rs"),
5285 display_name: None,
5286 text: "fn main() {}".to_string(),
5287 selection: AttachmentSelectionRange {
5288 start: AttachmentSelectionPosition {
5289 line: 0,
5290 character: 0,
5291 },
5292 end: AttachmentSelectionPosition {
5293 line: 0,
5294 character: 10,
5295 },
5296 },
5297 },
5298 Attachment::Blob {
5299 data: "Zm9v".to_string(),
5300 mime_type: "image/png".to_string(),
5301 display_name: None,
5302 },
5303 Attachment::GitHubReference {
5304 number: 7,
5305 title: "Track regressions".to_string(),
5306 reference_type: GitHubReferenceType::Issue,
5307 state: "open".to_string(),
5308 url: "https://example.com/issues/7".to_string(),
5309 },
5310 ];
5311
5312 ensure_attachment_display_names(&mut attachments);
5313
5314 assert_eq!(attachments[0].display_name(), Some("file.rs"));
5315 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
5316 assert_eq!(attachments[2].display_name(), Some("attachment"));
5317 assert_eq!(attachments[3].display_name(), None);
5318 assert_eq!(
5319 attachments[3].label(),
5320 Some("Track regressions".to_string())
5321 );
5322 }
5323}
5324
5325#[cfg(test)]
5326mod permission_builder_tests {
5327 use std::sync::Arc;
5328
5329 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
5330 use crate::permission;
5331 use crate::types::{
5332 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
5333 SessionId,
5334 };
5335
5336 fn data() -> PermissionRequestData {
5337 PermissionRequestData {
5338 extra: serde_json::json!({"tool": "shell"}),
5339 ..Default::default()
5340 }
5341 }
5342
5343 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
5346 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
5347 }
5348
5349 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
5350 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
5351 }
5352
5353 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
5354 handler
5355 .handle(SessionId::from("s1"), RequestId::new("1"), data())
5356 .await
5357 }
5358
5359 #[tokio::test]
5360 async fn approve_all_with_handler_present_approves() {
5361 let cfg = SessionConfig::default()
5362 .with_permission_handler(Arc::new(ApproveAllHandler))
5363 .approve_all_permissions();
5364 let h = resolve_create(cfg).expect("policy + handler yields handler");
5365 assert!(matches!(
5366 dispatch(&h).await,
5367 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
5368 ));
5369 }
5370
5371 #[tokio::test]
5372 async fn approve_all_standalone_produces_handler() {
5373 let cfg = SessionConfig::default().approve_all_permissions();
5374 let h = resolve_create(cfg).expect("policy alone yields handler");
5375 assert!(matches!(
5376 dispatch(&h).await,
5377 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
5378 ));
5379 }
5380
5381 #[tokio::test]
5384 async fn approve_all_is_order_independent() {
5385 let a = SessionConfig::default()
5386 .with_permission_handler(Arc::new(ApproveAllHandler))
5387 .approve_all_permissions();
5388 let b = SessionConfig::default()
5389 .approve_all_permissions()
5390 .with_permission_handler(Arc::new(ApproveAllHandler));
5391 let ha = resolve_create(a).unwrap();
5392 let hb = resolve_create(b).unwrap();
5393 assert!(matches!(
5394 dispatch(&ha).await,
5395 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
5396 ));
5397 assert!(matches!(
5398 dispatch(&hb).await,
5399 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
5400 ));
5401 }
5402
5403 #[tokio::test]
5404 async fn deny_all_is_order_independent() {
5405 let a = SessionConfig::default()
5406 .with_permission_handler(Arc::new(ApproveAllHandler))
5407 .deny_all_permissions();
5408 let b = SessionConfig::default()
5409 .deny_all_permissions()
5410 .with_permission_handler(Arc::new(ApproveAllHandler));
5411 let ha = resolve_create(a).unwrap();
5412 let hb = resolve_create(b).unwrap();
5413 assert!(matches!(
5414 dispatch(&ha).await,
5415 PermissionResult::Decision(PermissionDecision::Reject(_))
5416 ));
5417 assert!(matches!(
5418 dispatch(&hb).await,
5419 PermissionResult::Decision(PermissionDecision::Reject(_))
5420 ));
5421 }
5422
5423 #[tokio::test]
5424 async fn approve_permissions_if_consults_predicate() {
5425 let cfg = SessionConfig::default().approve_permissions_if(|d| {
5426 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
5427 });
5428 let h = resolve_create(cfg).unwrap();
5429 assert!(matches!(
5430 dispatch(&h).await,
5431 PermissionResult::Decision(PermissionDecision::Reject(_))
5432 ));
5433 }
5434
5435 #[tokio::test]
5436 async fn approve_permissions_if_is_order_independent() {
5437 let predicate = |d: &PermissionRequestData| {
5438 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
5439 };
5440 let a = SessionConfig::default()
5441 .with_permission_handler(Arc::new(ApproveAllHandler))
5442 .approve_permissions_if(predicate);
5443 let b = SessionConfig::default()
5444 .approve_permissions_if(predicate)
5445 .with_permission_handler(Arc::new(ApproveAllHandler));
5446 let ha = resolve_create(a).unwrap();
5447 let hb = resolve_create(b).unwrap();
5448 assert!(matches!(
5449 dispatch(&ha).await,
5450 PermissionResult::Decision(PermissionDecision::Reject(_))
5451 ));
5452 assert!(matches!(
5453 dispatch(&hb).await,
5454 PermissionResult::Decision(PermissionDecision::Reject(_))
5455 ));
5456 }
5457
5458 #[tokio::test]
5459 async fn resume_session_config_approve_all_works() {
5460 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
5461 .with_permission_handler(Arc::new(ApproveAllHandler))
5462 .approve_all_permissions();
5463 let h = resolve_resume(cfg).unwrap();
5464 assert!(matches!(
5465 dispatch(&h).await,
5466 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
5467 ));
5468 }
5469
5470 #[tokio::test]
5471 async fn resume_session_config_approve_all_is_order_independent() {
5472 let a = ResumeSessionConfig::new(SessionId::from("s1"))
5473 .with_permission_handler(Arc::new(ApproveAllHandler))
5474 .approve_all_permissions();
5475 let b = ResumeSessionConfig::new(SessionId::from("s1"))
5476 .approve_all_permissions()
5477 .with_permission_handler(Arc::new(ApproveAllHandler));
5478 let ha = resolve_resume(a).unwrap();
5479 let hb = resolve_resume(b).unwrap();
5480 assert!(matches!(
5481 dispatch(&ha).await,
5482 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
5483 ));
5484 assert!(matches!(
5485 dispatch(&hb).await,
5486 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
5487 ));
5488 }
5489}