1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11
12use indexmap::IndexMap;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16use crate::canvas::{CanvasDeclaration, CanvasHandler};
17pub use crate::copilot_request_handler::{
18 CopilotHttpRequest, CopilotHttpResponse, CopilotHttpResponseBody, CopilotRequestContext,
19 CopilotRequestError, CopilotRequestHandler, CopilotRequestTransport, CopilotWebSocketForwarder,
20 CopilotWebSocketForwarderBuilder, CopilotWebSocketHandler, CopilotWebSocketMessage,
21 CopilotWebSocketResponse, WebSocketTransform, forward_http,
22};
23use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance};
24use crate::generated::session_events::ReasoningSummary;
25pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig};
27use crate::handler::{
28 AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler,
29 PermissionHandler, UserInputHandler,
30};
31use crate::hooks::SessionHooks;
32use crate::provider_token::BearerTokenProvider;
33pub use crate::session_fs::{
34 DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig,
35 SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult,
36 SessionFsSqliteQueryType, SessionFsSqliteTransactionError,
37 SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionStatement,
38};
39pub use crate::trace_context::{TraceContext, TraceContextProvider};
40use crate::transforms::SystemMessageTransform;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45#[allow(dead_code)]
46#[non_exhaustive]
47pub(crate) enum ConnectionState {
48 Disconnected,
50 Connecting,
52 Connected,
54 Error,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
63#[non_exhaustive]
64pub enum SessionLifecycleEventType {
65 #[serde(rename = "session.created")]
67 Created,
68 #[serde(rename = "session.deleted")]
70 Deleted,
71 #[serde(rename = "session.updated")]
73 Updated,
74 #[serde(rename = "session.foreground")]
76 Foreground,
77 #[serde(rename = "session.background")]
79 Background,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct SessionLifecycleEventMetadata {
85 #[serde(rename = "startTime")]
87 pub start_time: String,
88 #[serde(rename = "modifiedTime")]
90 pub modified_time: String,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub summary: Option<String>,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct SessionLifecycleEvent {
100 #[serde(rename = "type")]
102 pub event_type: SessionLifecycleEventType,
103 #[serde(rename = "sessionId")]
105 pub session_id: SessionId,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub metadata: Option<SessionLifecycleEventMetadata>,
109}
110
111#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
117#[serde(transparent)]
118pub struct SessionId(String);
119
120impl SessionId {
121 pub fn new(id: impl Into<String>) -> Self {
123 Self(id.into())
124 }
125
126 pub fn as_str(&self) -> &str {
128 &self.0
129 }
130
131 pub fn into_inner(self) -> String {
133 self.0
134 }
135}
136
137impl std::ops::Deref for SessionId {
138 type Target = str;
139
140 fn deref(&self) -> &str {
141 &self.0
142 }
143}
144
145impl std::fmt::Display for SessionId {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 f.write_str(&self.0)
148 }
149}
150
151impl From<String> for SessionId {
152 fn from(s: String) -> Self {
153 Self(s)
154 }
155}
156
157impl From<&str> for SessionId {
158 fn from(s: &str) -> Self {
159 Self(s.to_owned())
160 }
161}
162
163impl AsRef<str> for SessionId {
164 fn as_ref(&self) -> &str {
165 &self.0
166 }
167}
168
169impl std::borrow::Borrow<str> for SessionId {
170 fn borrow(&self) -> &str {
171 &self.0
172 }
173}
174
175impl From<SessionId> for String {
176 fn from(id: SessionId) -> String {
177 id.0
178 }
179}
180
181impl PartialEq<str> for SessionId {
182 fn eq(&self, other: &str) -> bool {
183 self.0 == other
184 }
185}
186
187impl PartialEq<String> for SessionId {
188 fn eq(&self, other: &String) -> bool {
189 &self.0 == other
190 }
191}
192
193impl PartialEq<SessionId> for String {
194 fn eq(&self, other: &SessionId) -> bool {
195 self == &other.0
196 }
197}
198
199impl PartialEq<&str> for SessionId {
200 fn eq(&self, other: &&str) -> bool {
201 self.0 == *other
202 }
203}
204
205impl PartialEq<&SessionId> for SessionId {
206 fn eq(&self, other: &&SessionId) -> bool {
207 self.0 == other.0
208 }
209}
210
211impl PartialEq<SessionId> for &SessionId {
212 fn eq(&self, other: &SessionId) -> bool {
213 self.0 == other.0
214 }
215}
216
217#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
223#[serde(transparent)]
224pub struct RequestId(String);
225
226impl RequestId {
227 pub fn new(id: impl Into<String>) -> Self {
229 Self(id.into())
230 }
231
232 pub fn into_inner(self) -> String {
234 self.0
235 }
236}
237
238impl std::ops::Deref for RequestId {
239 type Target = str;
240
241 fn deref(&self) -> &str {
242 &self.0
243 }
244}
245
246impl std::fmt::Display for RequestId {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 f.write_str(&self.0)
249 }
250}
251
252impl From<String> for RequestId {
253 fn from(s: String) -> Self {
254 Self(s)
255 }
256}
257
258impl From<&str> for RequestId {
259 fn from(s: &str) -> Self {
260 Self(s.to_owned())
261 }
262}
263
264impl AsRef<str> for RequestId {
265 fn as_ref(&self) -> &str {
266 &self.0
267 }
268}
269
270impl std::borrow::Borrow<str> for RequestId {
271 fn borrow(&self) -> &str {
272 &self.0
273 }
274}
275
276impl From<RequestId> for String {
277 fn from(id: RequestId) -> String {
278 id.0
279 }
280}
281
282impl PartialEq<str> for RequestId {
283 fn eq(&self, other: &str) -> bool {
284 self.0 == other
285 }
286}
287
288impl PartialEq<String> for RequestId {
289 fn eq(&self, other: &String) -> bool {
290 &self.0 == other
291 }
292}
293
294impl PartialEq<RequestId> for String {
295 fn eq(&self, other: &RequestId) -> bool {
296 self == &other.0
297 }
298}
299
300impl PartialEq<&str> for RequestId {
301 fn eq(&self, other: &&str) -> bool {
302 self.0 == *other
303 }
304}
305
306#[derive(Clone, Default, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322#[non_exhaustive]
323pub struct Tool {
324 pub name: String,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub namespaced_name: Option<String>,
330 #[serde(default)]
332 pub description: String,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub instructions: Option<String>,
336 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
338 pub parameters: IndexMap<String, Value>,
339 #[serde(default, skip_serializing_if = "is_false")]
343 pub overrides_built_in_tool: bool,
344 #[serde(default, skip_serializing_if = "is_false")]
348 pub skip_permission: bool,
349 #[serde(default, skip_serializing_if = "is_false")]
354 pub is_terminal: bool,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
361 pub defer: Option<DeferMode>,
362 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
367 pub metadata: IndexMap<String, Value>,
368 #[serde(skip)]
380 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
381}
382
383#[inline]
384fn is_false(b: &bool) -> bool {
385 !*b
386}
387
388#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
391#[serde(rename_all = "lowercase")]
392pub enum DeferMode {
393 Auto,
395 Never,
397}
398
399impl Tool {
400 pub fn new(name: impl Into<String>) -> Self {
420 Self {
421 name: name.into(),
422 ..Default::default()
423 }
424 }
425
426 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
429 self.namespaced_name = Some(namespaced_name.into());
430 self
431 }
432
433 pub fn with_description(mut self, description: impl Into<String>) -> Self {
435 self.description = description.into();
436 self
437 }
438
439 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
441 self.instructions = Some(instructions.into());
442 self
443 }
444
445 pub fn with_parameters(mut self, parameters: Value) -> Self {
459 self.parameters = crate::tool::tool_parameters(parameters);
460 self
461 }
462
463 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
467 self.overrides_built_in_tool = overrides;
468 self
469 }
470
471 pub fn with_skip_permission(mut self, skip: bool) -> Self {
475 self.skip_permission = skip;
476 self
477 }
478
479 #[must_use]
486 pub fn with_is_terminal(mut self, is_terminal: bool) -> Self {
487 self.is_terminal = is_terminal;
488 self
489 }
490
491 pub fn with_defer(mut self, defer: DeferMode) -> Self {
495 self.defer = Some(defer);
496 self
497 }
498
499 pub fn with_metadata(mut self, metadata: IndexMap<String, Value>) -> Self {
502 self.metadata = metadata;
503 self
504 }
505
506 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
510 self.handler = Some(handler);
511 self
512 }
513
514 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
519 self.handler.as_ref()
520 }
521}
522
523impl std::fmt::Debug for Tool {
524 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525 f.debug_struct("Tool")
526 .field("name", &self.name)
527 .field("namespaced_name", &self.namespaced_name)
528 .field("description", &self.description)
529 .field("instructions", &self.instructions)
530 .field("parameters", &self.parameters)
531 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
532 .field("skip_permission", &self.skip_permission)
533 .field("is_terminal", &self.is_terminal)
534 .field("defer", &self.defer)
535 .field("metadata", &self.metadata)
536 .field(
537 "handler",
538 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
539 )
540 .finish()
541 }
542}
543
544#[non_exhaustive]
547#[derive(Debug, Clone)]
548pub struct CommandContext {
549 pub session_id: SessionId,
551 pub command: String,
553 pub command_name: String,
555 pub args: String,
557}
558
559#[async_trait::async_trait]
565pub trait CommandHandler: Send + Sync {
566 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
568}
569
570#[non_exhaustive]
576#[derive(Clone)]
577pub struct CommandDefinition {
578 pub name: String,
580 pub description: Option<String>,
582 pub handler: Arc<dyn CommandHandler>,
584}
585
586impl CommandDefinition {
587 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
590 Self {
591 name: name.into(),
592 description: None,
593 handler,
594 }
595 }
596
597 pub fn with_description(mut self, description: impl Into<String>) -> Self {
599 self.description = Some(description.into());
600 self
601 }
602}
603
604impl std::fmt::Debug for CommandDefinition {
605 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
606 f.debug_struct("CommandDefinition")
607 .field("name", &self.name)
608 .field("description", &self.description)
609 .field("handler", &"<set>")
610 .finish()
611 }
612}
613
614impl Serialize for CommandDefinition {
615 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
616 use serde::ser::SerializeStruct;
617 let len = if self.description.is_some() { 2 } else { 1 };
618 let mut state = serializer.serialize_struct("CommandDefinition", len)?;
619 state.serialize_field("name", &self.name)?;
620 if let Some(description) = &self.description {
621 state.serialize_field("description", description)?;
622 }
623 state.end()
624 }
625}
626
627#[derive(Debug, Clone, Default, Serialize, Deserialize)]
634#[serde(rename_all = "camelCase")]
635#[non_exhaustive]
636pub struct CustomAgentConfig {
637 pub name: String,
639 #[serde(default, skip_serializing_if = "Option::is_none")]
641 pub display_name: Option<String>,
642 #[serde(default, skip_serializing_if = "Option::is_none")]
644 pub description: Option<String>,
645 #[serde(default, skip_serializing_if = "Option::is_none")]
647 pub tools: Option<Vec<String>>,
648 pub prompt: String,
650 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub infer: Option<bool>,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub skills: Option<Vec<String>>,
659 #[serde(default, skip_serializing_if = "Option::is_none")]
664 pub model: Option<String>,
665 #[serde(default, skip_serializing_if = "Option::is_none")]
670 pub reasoning_effort: Option<String>,
671}
672
673impl CustomAgentConfig {
674 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
681 Self {
682 name: name.into(),
683 prompt: prompt.into(),
684 ..Self::default()
685 }
686 }
687
688 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
690 self.display_name = Some(display_name.into());
691 self
692 }
693
694 pub fn with_description(mut self, description: impl Into<String>) -> Self {
696 self.description = Some(description.into());
697 self
698 }
699
700 pub fn with_tools<I, S>(mut self, tools: I) -> Self
703 where
704 I: IntoIterator<Item = S>,
705 S: Into<String>,
706 {
707 self.tools = Some(tools.into_iter().map(Into::into).collect());
708 self
709 }
710
711 pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
713 self.mcp_servers = Some(mcp_servers);
714 self
715 }
716
717 pub fn with_infer(mut self, infer: bool) -> Self {
719 self.infer = Some(infer);
720 self
721 }
722
723 pub fn with_skills<I, S>(mut self, skills: I) -> Self
725 where
726 I: IntoIterator<Item = S>,
727 S: Into<String>,
728 {
729 self.skills = Some(skills.into_iter().map(Into::into).collect());
730 self
731 }
732
733 pub fn with_model(mut self, model: impl Into<String>) -> Self {
735 self.model = Some(model.into());
736 self
737 }
738
739 pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into<String>) -> Self {
741 self.reasoning_effort = Some(reasoning_effort.into());
742 self
743 }
744}
745
746#[derive(Debug, Clone, Default, Serialize, Deserialize)]
753#[serde(rename_all = "camelCase")]
754pub struct DefaultAgentConfig {
755 #[serde(default, skip_serializing_if = "Option::is_none")]
757 pub excluded_tools: Option<Vec<String>>,
758}
759
760#[derive(Debug, Clone, Default, Serialize, Deserialize)]
766#[serde(rename_all = "camelCase")]
767#[non_exhaustive]
768pub struct LargeToolOutputConfig {
769 #[serde(default, skip_serializing_if = "Option::is_none")]
771 pub enabled: Option<bool>,
772 #[serde(default, skip_serializing_if = "Option::is_none")]
775 pub max_size_bytes: Option<u64>,
776 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
779 pub output_directory: Option<PathBuf>,
780}
781
782impl LargeToolOutputConfig {
783 pub fn new() -> Self {
786 Self::default()
787 }
788
789 pub fn with_enabled(mut self, enabled: bool) -> Self {
791 self.enabled = Some(enabled);
792 self
793 }
794
795 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
797 self.max_size_bytes = Some(max_size_bytes);
798 self
799 }
800
801 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
803 self.output_directory = Some(output_directory.into());
804 self
805 }
806}
807
808#[derive(Debug, Clone, Default, Serialize, Deserialize)]
814#[serde(rename_all = "camelCase")]
815#[non_exhaustive]
816pub struct ToolSearchConfig {
817 #[serde(default, skip_serializing_if = "Option::is_none")]
819 pub enabled: Option<bool>,
820 #[serde(default, skip_serializing_if = "Option::is_none")]
823 pub defer_threshold: Option<u32>,
824}
825
826impl ToolSearchConfig {
827 pub fn new() -> Self {
830 Self::default()
831 }
832
833 pub fn with_enabled(mut self, enabled: bool) -> Self {
835 self.enabled = Some(enabled);
836 self
837 }
838
839 pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
842 self.defer_threshold = Some(defer_threshold);
843 self
844 }
845}
846
847#[derive(Debug, Clone, Default, Serialize, Deserialize)]
852#[serde(rename_all = "camelCase")]
853#[non_exhaustive]
854pub struct GitHubMcpToolConfig {
855 #[serde(default, skip_serializing_if = "Option::is_none")]
857 pub enable_all_tools: Option<bool>,
858 #[serde(default, skip_serializing_if = "Option::is_none")]
860 pub additional_toolsets: Option<Vec<String>>,
861 #[serde(default, skip_serializing_if = "Option::is_none")]
863 pub additional_tools: Option<Vec<String>>,
864 #[serde(default, skip_serializing_if = "Option::is_none")]
866 pub enable_insiders_mode: Option<bool>,
867 #[serde(default, skip_serializing_if = "Option::is_none")]
871 pub disable_form_deferral: Option<bool>,
872}
873
874impl GitHubMcpToolConfig {
875 pub fn new() -> Self {
877 Self::default()
878 }
879
880 pub fn with_enable_all_tools(mut self, value: bool) -> Self {
882 self.enable_all_tools = Some(value);
883 self
884 }
885
886 pub fn with_additional_toolsets<I, S>(mut self, values: I) -> Self
888 where
889 I: IntoIterator<Item = S>,
890 S: Into<String>,
891 {
892 self.additional_toolsets = Some(values.into_iter().map(Into::into).collect());
893 self
894 }
895
896 pub fn with_additional_tools<I, S>(mut self, values: I) -> Self
898 where
899 I: IntoIterator<Item = S>,
900 S: Into<String>,
901 {
902 self.additional_tools = Some(values.into_iter().map(Into::into).collect());
903 self
904 }
905
906 pub fn with_enable_insiders_mode(mut self, value: bool) -> Self {
908 self.enable_insiders_mode = Some(value);
909 self
910 }
911
912 pub fn with_disable_form_deferral(mut self, value: bool) -> Self {
916 self.disable_form_deferral = Some(value);
917 self
918 }
919}
920
921#[derive(Debug, Clone, Default, Serialize, Deserialize)]
928#[serde(rename_all = "camelCase")]
929#[non_exhaustive]
930pub struct InfiniteSessionConfig {
931 #[serde(default, skip_serializing_if = "Option::is_none")]
933 pub enabled: Option<bool>,
934 #[serde(default, skip_serializing_if = "Option::is_none")]
937 pub background_compaction_threshold: Option<f64>,
938 #[serde(default, skip_serializing_if = "Option::is_none")]
941 pub buffer_exhaustion_threshold: Option<f64>,
942}
943
944impl InfiniteSessionConfig {
945 pub fn new() -> Self {
948 Self::default()
949 }
950
951 pub fn with_enabled(mut self, enabled: bool) -> Self {
954 self.enabled = Some(enabled);
955 self
956 }
957
958 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
961 self.background_compaction_threshold = Some(threshold);
962 self
963 }
964
965 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
968 self.buffer_exhaustion_threshold = Some(threshold);
969 self
970 }
971}
972
973#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
984#[serde(rename_all = "camelCase")]
985#[non_exhaustive]
986pub struct MemoryConfiguration {
987 pub enabled: bool,
989}
990
991impl MemoryConfiguration {
992 pub fn enabled() -> Self {
994 Self { enabled: true }
995 }
996
997 pub fn disabled() -> Self {
999 Self { enabled: false }
1000 }
1001
1002 pub fn with_enabled(mut self, enabled: bool) -> Self {
1004 self.enabled = enabled;
1005 self
1006 }
1007}
1008
1009#[derive(Debug, Clone, Serialize, Deserialize)]
1011#[serde(rename_all = "camelCase")]
1012#[non_exhaustive]
1013pub struct CloudSessionRepository {
1014 pub owner: String,
1016 pub name: String,
1018 #[serde(skip_serializing_if = "Option::is_none")]
1020 pub branch: Option<String>,
1021}
1022
1023impl CloudSessionRepository {
1024 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
1026 Self {
1027 owner: owner.into(),
1028 name: name.into(),
1029 branch: None,
1030 }
1031 }
1032
1033 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
1035 self.branch = Some(branch.into());
1036 self
1037 }
1038}
1039
1040#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1042#[serde(rename_all = "camelCase")]
1043#[non_exhaustive]
1044pub struct CloudSessionOptions {
1045 #[serde(skip_serializing_if = "Option::is_none")]
1047 pub repository: Option<CloudSessionRepository>,
1048}
1049
1050impl CloudSessionOptions {
1051 pub fn with_repository(repository: CloudSessionRepository) -> Self {
1053 Self {
1054 repository: Some(repository),
1055 }
1056 }
1057}
1058
1059#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1061#[serde(rename_all = "camelCase")]
1062pub struct ExtensionInfo {
1063 pub source: String,
1065 pub name: String,
1067}
1068
1069impl ExtensionInfo {
1070 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
1072 Self {
1073 source: source.into(),
1074 name: name.into(),
1075 }
1076 }
1077}
1078
1079#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1090#[serde(rename_all = "camelCase")]
1091pub struct CanvasProviderIdentity {
1092 pub id: String,
1094 #[serde(skip_serializing_if = "Option::is_none")]
1096 pub name: Option<String>,
1097}
1098
1099impl CanvasProviderIdentity {
1100 pub fn new(id: impl Into<String>) -> Self {
1102 Self {
1103 id: id.into(),
1104 name: None,
1105 }
1106 }
1107
1108 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1110 self.name = Some(name.into());
1111 self
1112 }
1113}
1114
1115#[derive(Debug, Clone, Serialize, Deserialize)]
1149#[serde(tag = "type", rename_all = "lowercase")]
1150#[non_exhaustive]
1151pub enum McpServerConfig {
1152 #[serde(alias = "local")]
1156 Stdio(McpStdioServerConfig),
1157 Http(McpHttpServerConfig),
1159 Sse(McpHttpServerConfig),
1161}
1162
1163#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1167#[serde(rename_all = "camelCase")]
1168pub struct McpStdioServerConfig {
1169 #[serde(default, skip_serializing_if = "Option::is_none")]
1175 pub tools: Option<Vec<String>>,
1176 #[serde(default, skip_serializing_if = "Option::is_none")]
1178 pub timeout: Option<i64>,
1179 pub command: String,
1181 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1183 pub args: Vec<String>,
1184 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1187 pub env: HashMap<String, String>,
1188 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1190 pub working_directory: Option<String>,
1191}
1192
1193#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1197#[serde(rename_all = "camelCase")]
1198pub struct McpHttpServerConfig {
1199 #[serde(default, skip_serializing_if = "Option::is_none")]
1205 pub tools: Option<Vec<String>>,
1206 #[serde(default, skip_serializing_if = "Option::is_none")]
1208 pub timeout: Option<i64>,
1209 pub url: String,
1211 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1213 pub headers: HashMap<String, String>,
1214}
1215
1216#[derive(Clone, Default, Serialize, Deserialize)]
1222#[serde(rename_all = "camelCase")]
1223#[non_exhaustive]
1224pub struct ProviderConfig {
1225 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1228 pub provider_type: Option<String>,
1229 #[serde(default, skip_serializing_if = "Option::is_none")]
1232 pub wire_api: Option<String>,
1233 #[serde(default, skip_serializing_if = "Option::is_none")]
1238 pub transport: Option<String>,
1239 pub base_url: String,
1241 #[serde(default, skip_serializing_if = "Option::is_none")]
1243 pub api_key: Option<String>,
1244 #[serde(default, skip_serializing_if = "Option::is_none")]
1248 pub bearer_token: Option<String>,
1249 #[serde(skip)]
1252 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1253 #[serde(default, skip_serializing_if = "Option::is_none")]
1254 pub(crate) has_bearer_token_provider: Option<bool>,
1255 #[serde(default, skip_serializing_if = "Option::is_none")]
1257 pub azure: Option<AzureProviderOptions>,
1258 #[serde(default, skip_serializing_if = "Option::is_none")]
1260 pub headers: Option<HashMap<String, String>>,
1261 #[serde(default, skip_serializing_if = "Option::is_none")]
1265 pub model_id: Option<String>,
1266 #[serde(default, skip_serializing_if = "Option::is_none")]
1273 pub wire_model: Option<String>,
1274 #[serde(default, skip_serializing_if = "Option::is_none")]
1279 pub max_prompt_tokens: Option<i64>,
1280 #[serde(default, skip_serializing_if = "Option::is_none")]
1283 pub max_output_tokens: Option<i64>,
1284}
1285
1286impl std::fmt::Debug for ProviderConfig {
1287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1288 f.debug_struct("ProviderConfig")
1289 .field("provider_type", &self.provider_type)
1290 .field("wire_api", &self.wire_api)
1291 .field("transport", &self.transport)
1292 .field("base_url", &self.base_url)
1293 .field("api_key", &self.api_key)
1294 .field("bearer_token", &self.bearer_token)
1295 .field(
1296 "bearer_token_provider",
1297 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1298 )
1299 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1300 .field("azure", &self.azure)
1301 .field("headers", &self.headers)
1302 .field("model_id", &self.model_id)
1303 .field("wire_model", &self.wire_model)
1304 .field("max_prompt_tokens", &self.max_prompt_tokens)
1305 .field("max_output_tokens", &self.max_output_tokens)
1306 .finish()
1307 }
1308}
1309
1310impl ProviderConfig {
1311 pub fn new(base_url: impl Into<String>) -> Self {
1314 Self {
1315 base_url: base_url.into(),
1316 ..Self::default()
1317 }
1318 }
1319
1320 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1322 self.provider_type = Some(provider_type.into());
1323 self
1324 }
1325
1326 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1328 self.wire_api = Some(wire_api.into());
1329 self
1330 }
1331
1332 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1335 self.transport = Some(transport.into());
1336 self
1337 }
1338
1339 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1341 self.api_key = Some(api_key.into());
1342 self
1343 }
1344
1345 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1348 self.bearer_token = Some(bearer_token.into());
1349 self
1350 }
1351
1352 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1358 self.bearer_token_provider = Some(provider);
1359 self
1360 }
1361
1362 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1364 self.azure = Some(azure);
1365 self
1366 }
1367
1368 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1370 self.headers = Some(headers);
1371 self
1372 }
1373
1374 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1377 self.model_id = Some(model_id.into());
1378 self
1379 }
1380
1381 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1386 self.wire_model = Some(wire_model.into());
1387 self
1388 }
1389
1390 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1394 self.max_prompt_tokens = Some(max);
1395 self
1396 }
1397
1398 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1401 self.max_output_tokens = Some(max);
1402 self
1403 }
1404}
1405
1406#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1419#[serde(rename_all = "camelCase")]
1420#[non_exhaustive]
1421pub struct CapiSessionOptions {
1422 #[serde(default, skip_serializing_if = "Option::is_none")]
1428 pub enable_web_socket_responses: Option<bool>,
1429}
1430
1431impl CapiSessionOptions {
1432 pub fn new() -> Self {
1434 Self::default()
1435 }
1436
1437 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1439 self.enable_web_socket_responses = Some(enable);
1440 self
1441 }
1442}
1443
1444#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1446#[serde(rename_all = "camelCase")]
1447pub struct AzureProviderOptions {
1448 #[serde(default, skip_serializing_if = "Option::is_none")]
1450 pub api_version: Option<String>,
1451}
1452
1453#[derive(Clone, Default, Serialize, Deserialize)]
1464#[serde(rename_all = "camelCase")]
1465#[non_exhaustive]
1466pub struct NamedProviderConfig {
1467 pub name: String,
1470 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1473 pub provider_type: Option<String>,
1474 #[serde(default, skip_serializing_if = "Option::is_none")]
1477 pub wire_api: Option<String>,
1478 pub base_url: String,
1480 #[serde(default, skip_serializing_if = "Option::is_none")]
1482 pub api_key: Option<String>,
1483 #[serde(default, skip_serializing_if = "Option::is_none")]
1486 pub bearer_token: Option<String>,
1487 #[serde(skip)]
1490 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1491 #[serde(default, skip_serializing_if = "Option::is_none")]
1492 pub(crate) has_bearer_token_provider: Option<bool>,
1493 #[serde(default, skip_serializing_if = "Option::is_none")]
1495 pub azure: Option<AzureProviderOptions>,
1496 #[serde(default, skip_serializing_if = "Option::is_none")]
1498 pub headers: Option<HashMap<String, String>>,
1499}
1500
1501impl std::fmt::Debug for NamedProviderConfig {
1502 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1503 f.debug_struct("NamedProviderConfig")
1504 .field("name", &self.name)
1505 .field("provider_type", &self.provider_type)
1506 .field("wire_api", &self.wire_api)
1507 .field("base_url", &self.base_url)
1508 .field("api_key", &self.api_key)
1509 .field("bearer_token", &self.bearer_token)
1510 .field(
1511 "bearer_token_provider",
1512 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1513 )
1514 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1515 .field("azure", &self.azure)
1516 .field("headers", &self.headers)
1517 .finish()
1518 }
1519}
1520
1521impl NamedProviderConfig {
1522 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1525 Self {
1526 name: name.into(),
1527 base_url: base_url.into(),
1528 ..Self::default()
1529 }
1530 }
1531
1532 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1534 self.provider_type = Some(provider_type.into());
1535 self
1536 }
1537
1538 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1540 self.wire_api = Some(wire_api.into());
1541 self
1542 }
1543
1544 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1546 self.api_key = Some(api_key.into());
1547 self
1548 }
1549
1550 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1553 self.bearer_token = Some(bearer_token.into());
1554 self
1555 }
1556
1557 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1563 self.bearer_token_provider = Some(provider);
1564 self
1565 }
1566
1567 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1569 self.azure = Some(azure);
1570 self
1571 }
1572
1573 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1575 self.headers = Some(headers);
1576 self
1577 }
1578}
1579
1580fn prepare_bearer_token_providers(
1581 provider: &mut Option<ProviderConfig>,
1582 providers: &mut Option<Vec<NamedProviderConfig>>,
1583) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1584 let mut bearer_token_providers = HashMap::new();
1585
1586 if let Some(provider) = provider.as_mut()
1587 && let Some(token_provider) = provider.bearer_token_provider.take()
1588 {
1589 provider.has_bearer_token_provider = Some(true);
1590 bearer_token_providers.insert("default".to_string(), token_provider);
1591 }
1592
1593 if let Some(providers) = providers.as_mut() {
1594 for provider in providers {
1595 if let Some(token_provider) = provider.bearer_token_provider.take() {
1596 provider.has_bearer_token_provider = Some(true);
1597 bearer_token_providers.insert(provider.name.clone(), token_provider);
1598 }
1599 }
1600 }
1601
1602 bearer_token_providers
1603}
1604
1605#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1613#[serde(rename_all = "camelCase")]
1614#[non_exhaustive]
1615pub struct ProviderModelConfig {
1616 pub id: String,
1619 pub provider: String,
1621 #[serde(default, skip_serializing_if = "Option::is_none")]
1624 pub wire_model: Option<String>,
1625 #[serde(default, skip_serializing_if = "Option::is_none")]
1628 pub model_id: Option<String>,
1629 #[serde(default, skip_serializing_if = "Option::is_none")]
1631 pub name: Option<String>,
1632 #[serde(default, skip_serializing_if = "Option::is_none")]
1634 pub max_prompt_tokens: Option<i64>,
1635 #[serde(default, skip_serializing_if = "Option::is_none")]
1637 pub max_context_window_tokens: Option<i64>,
1638 #[serde(default, skip_serializing_if = "Option::is_none")]
1640 pub max_output_tokens: Option<i64>,
1641 #[serde(default, skip_serializing_if = "Option::is_none")]
1644 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1645}
1646
1647impl ProviderModelConfig {
1648 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1651 Self {
1652 id: id.into(),
1653 provider: provider.into(),
1654 ..Self::default()
1655 }
1656 }
1657
1658 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1660 self.wire_model = Some(wire_model.into());
1661 self
1662 }
1663
1664 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1667 self.model_id = Some(model_id.into());
1668 self
1669 }
1670
1671 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1673 self.name = Some(name.into());
1674 self
1675 }
1676
1677 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1679 self.max_prompt_tokens = Some(max);
1680 self
1681 }
1682
1683 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1685 self.max_context_window_tokens = Some(max);
1686 self
1687 }
1688
1689 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1691 self.max_output_tokens = Some(max);
1692 self
1693 }
1694
1695 pub fn with_capabilities(
1697 mut self,
1698 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1699 ) -> Self {
1700 self.capabilities = Some(capabilities);
1701 self
1702 }
1703}
1704
1705#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1709#[serde(untagged)]
1710pub enum ExpFlagValue {
1711 Bool(bool),
1713 Integer(i64),
1715 Float(f64),
1717 String(String),
1719 Null,
1721}
1722
1723#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1727#[serde(rename_all = "PascalCase")]
1728pub struct ExpConfigEntry {
1729 pub id: String,
1731 pub parameters: HashMap<String, ExpFlagValue>,
1733}
1734
1735#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1741#[serde(rename_all = "PascalCase")]
1742pub struct CopilotExpAssignmentResponse {
1743 #[serde(default)]
1745 pub features: Vec<String>,
1746 #[serde(default)]
1748 pub flights: HashMap<String, String>,
1749 #[serde(default)]
1751 pub configs: Vec<ExpConfigEntry>,
1752 #[serde(default, skip_serializing_if = "Option::is_none")]
1754 pub parameter_groups: Option<Value>,
1755 #[serde(default, skip_serializing_if = "Option::is_none")]
1757 pub flighting_version: Option<i64>,
1758 #[serde(default, skip_serializing_if = "Option::is_none")]
1760 pub impression_id: Option<String>,
1761 #[serde(default)]
1763 pub assignment_context: String,
1764}
1765
1766#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1768#[serde(rename_all = "lowercase")]
1769#[non_exhaustive]
1770pub enum DisableBypassPermissionsMode {
1771 Disable,
1773}
1774
1775#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1785#[serde(rename_all = "camelCase")]
1786#[non_exhaustive]
1787pub struct ManagedSettingsPermissions {
1788 #[serde(default, skip_serializing_if = "Option::is_none")]
1792 pub disable_bypass_permissions_mode: Option<DisableBypassPermissionsMode>,
1793 #[serde(default, skip_serializing_if = "Option::is_none")]
1795 pub deny: Option<Vec<String>>,
1796 #[serde(default, skip_serializing_if = "Option::is_none")]
1798 pub ask: Option<Vec<String>>,
1799 #[serde(default, skip_serializing_if = "Option::is_none")]
1801 pub allow: Option<Vec<String>>,
1802}
1803
1804impl ManagedSettingsPermissions {
1805 pub fn with_disable_bypass_permissions_mode(
1807 mut self,
1808 value: DisableBypassPermissionsMode,
1809 ) -> Self {
1810 self.disable_bypass_permissions_mode = Some(value);
1811 self
1812 }
1813
1814 pub fn with_deny(mut self, rules: Vec<String>) -> Self {
1816 self.deny = Some(rules);
1817 self
1818 }
1819
1820 pub fn with_ask(mut self, rules: Vec<String>) -> Self {
1822 self.ask = Some(rules);
1823 self
1824 }
1825
1826 pub fn with_allow(mut self, rules: Vec<String>) -> Self {
1828 self.allow = Some(rules);
1829 self
1830 }
1831}
1832
1833#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1843#[serde(rename_all = "camelCase")]
1844#[non_exhaustive]
1845pub struct ManagedSettings {
1846 #[serde(default, skip_serializing_if = "Option::is_none")]
1848 pub permissions: Option<ManagedSettingsPermissions>,
1849}
1850
1851impl ManagedSettings {
1852 pub fn with_permissions(mut self, permissions: ManagedSettingsPermissions) -> Self {
1854 self.permissions = Some(permissions);
1855 self
1856 }
1857}
1858
1859#[derive(Clone)]
1911#[non_exhaustive]
1912pub struct SessionConfig {
1913 pub session_id: Option<SessionId>,
1915 pub model: Option<String>,
1917 pub client_name: Option<String>,
1919 pub reasoning_effort: Option<String>,
1921 pub reasoning_summary: Option<ReasoningSummary>,
1925 pub context_tier: Option<String>,
1928 pub streaming: Option<bool>,
1930 pub system_message: Option<SystemMessageConfig>,
1932 pub tools: Option<Vec<Tool>>,
1934 pub canvases: Option<Vec<CanvasDeclaration>>,
1936 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1941 pub request_canvas_renderer: Option<bool>,
1943 pub request_extensions: Option<bool>,
1945 pub extension_sdk_path: Option<String>,
1949 pub extension_info: Option<ExtensionInfo>,
1951 pub canvas_provider: Option<CanvasProviderIdentity>,
1954 pub available_tools: Option<Vec<String>>,
1956 pub excluded_tools: Option<Vec<String>>,
1958 pub excluded_builtin_agents: Option<Vec<String>>,
1964 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
1966 pub mcp_oauth_token_storage: Option<String>,
1975 pub enable_config_discovery: Option<bool>,
1978 pub skip_embedding_retrieval: Option<bool>,
1980 pub embedding_cache_storage: Option<String>,
1983 pub organization_custom_instructions: Option<String>,
1985 pub enable_on_demand_instruction_discovery: Option<bool>,
1987 pub enable_file_hooks: Option<bool>,
1989 pub enable_host_git_operations: Option<bool>,
1991 pub enable_session_store: Option<bool>,
1993 pub enable_skills: Option<bool>,
1995 pub enable_mcp_apps: Option<bool>,
2022 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
2027 pub skill_directories: Option<Vec<PathBuf>>,
2029 pub instruction_directories: Option<Vec<PathBuf>>,
2032 pub plugin_directories: Option<Vec<PathBuf>>,
2034 pub large_output: Option<LargeToolOutputConfig>,
2036 pub tool_search: Option<ToolSearchConfig>,
2040 pub disabled_skills: Option<Vec<String>>,
2043 pub disabled_mcp_servers: Option<Vec<String>>,
2047 pub hooks: Option<bool>,
2051 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2053 pub default_agent: Option<DefaultAgentConfig>,
2057 pub agent: Option<String>,
2060 pub infinite_sessions: Option<InfiniteSessionConfig>,
2063 pub provider: Option<ProviderConfig>,
2067 pub capi: Option<CapiSessionOptions>,
2073 pub providers: Option<Vec<NamedProviderConfig>>,
2080 pub models: Option<Vec<ProviderModelConfig>>,
2086 pub enable_session_telemetry: Option<bool>,
2094 pub enable_citations: Option<bool>,
2096 pub enable_file_change_tracking: Option<bool>,
2099 pub session_limits: Option<SessionLimitsConfig>,
2101 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2104 pub memory: Option<MemoryConfiguration>,
2106 pub config_directory: Option<PathBuf>,
2109 pub working_directory: Option<PathBuf>,
2112 pub additional_directories: Option<Vec<PathBuf>>,
2116 pub github_token: Option<String>,
2122 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2128 pub cloud: Option<CloudSessionOptions>,
2131 pub include_sub_agent_streaming_events: Option<bool>,
2135 pub commands: Option<Vec<CommandDefinition>>,
2139 #[doc(hidden)]
2146 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
2147 pub enable_managed_settings: Option<bool>,
2154 pub managed_settings: Option<ManagedSettings>,
2163 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2168 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2172 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2175 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2178 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2182 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2185 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2188 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2192 pub(crate) permission_policy: Option<crate::permission::Policy>,
2196 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2201 pub skip_custom_instructions: Option<bool>,
2205 pub custom_agents_local_only: Option<bool>,
2209 pub enable_experimental_mode: Option<bool>,
2214 pub coauthor_enabled: Option<bool>,
2218 pub manage_schedule_enabled: Option<bool>,
2222}
2223
2224impl std::fmt::Debug for SessionConfig {
2225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2226 f.debug_struct("SessionConfig")
2227 .field("session_id", &self.session_id)
2228 .field("model", &self.model)
2229 .field("client_name", &self.client_name)
2230 .field("reasoning_effort", &self.reasoning_effort)
2231 .field("reasoning_summary", &self.reasoning_summary)
2232 .field("context_tier", &self.context_tier)
2233 .field("streaming", &self.streaming)
2234 .field("system_message", &self.system_message)
2235 .field("tools", &self.tools)
2236 .field("canvases", &self.canvases)
2237 .field(
2238 "canvas_handler",
2239 &self.canvas_handler.as_ref().map(|_| "<set>"),
2240 )
2241 .field("request_canvas_renderer", &self.request_canvas_renderer)
2242 .field("request_extensions", &self.request_extensions)
2243 .field("extension_sdk_path", &self.extension_sdk_path)
2244 .field("extension_info", &self.extension_info)
2245 .field("canvas_provider", &self.canvas_provider)
2246 .field("available_tools", &self.available_tools)
2247 .field("excluded_tools", &self.excluded_tools)
2248 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2249 .field("mcp_servers", &self.mcp_servers)
2250 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2251 .field("embedding_cache_storage", &self.embedding_cache_storage)
2252 .field("enable_config_discovery", &self.enable_config_discovery)
2253 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2254 .field(
2255 "organization_custom_instructions",
2256 &self
2257 .organization_custom_instructions
2258 .as_ref()
2259 .map(|_| "<redacted>"),
2260 )
2261 .field(
2262 "enable_on_demand_instruction_discovery",
2263 &self.enable_on_demand_instruction_discovery,
2264 )
2265 .field("enable_file_hooks", &self.enable_file_hooks)
2266 .field(
2267 "enable_host_git_operations",
2268 &self.enable_host_git_operations,
2269 )
2270 .field("enable_session_store", &self.enable_session_store)
2271 .field("enable_skills", &self.enable_skills)
2272 .field("enable_mcp_apps", &self.enable_mcp_apps)
2273 .field("skill_directories", &self.skill_directories)
2274 .field("instruction_directories", &self.instruction_directories)
2275 .field("plugin_directories", &self.plugin_directories)
2276 .field("large_output", &self.large_output)
2277 .field("tool_search", &self.tool_search)
2278 .field("disabled_skills", &self.disabled_skills)
2279 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
2280 .field("hooks", &self.hooks)
2281 .field("custom_agents", &self.custom_agents)
2282 .field("default_agent", &self.default_agent)
2283 .field("agent", &self.agent)
2284 .field("infinite_sessions", &self.infinite_sessions)
2285 .field("provider", &self.provider)
2286 .field("capi", &self.capi)
2287 .field("enable_session_telemetry", &self.enable_session_telemetry)
2288 .field("enable_citations", &self.enable_citations)
2289 .field(
2290 "enable_file_change_tracking",
2291 &self.enable_file_change_tracking,
2292 )
2293 .field("session_limits", &self.session_limits)
2294 .field("model_capabilities", &self.model_capabilities)
2295 .field("memory", &self.memory)
2296 .field("config_directory", &self.config_directory)
2297 .field("working_directory", &self.working_directory)
2298 .field("additional_directories", &self.additional_directories)
2299 .field(
2300 "github_token",
2301 &self.github_token.as_ref().map(|_| "<redacted>"),
2302 )
2303 .field("remote_session", &self.remote_session)
2304 .field("cloud", &self.cloud)
2305 .field(
2306 "include_sub_agent_streaming_events",
2307 &self.include_sub_agent_streaming_events,
2308 )
2309 .field("commands", &self.commands)
2310 .field("exp_assignments", &self.exp_assignments)
2311 .field("enable_managed_settings", &self.enable_managed_settings)
2312 .field("enable_experimental_mode", &self.enable_experimental_mode)
2313 .field("managed_settings", &self.managed_settings)
2314 .field(
2315 "session_fs_provider",
2316 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2317 )
2318 .field(
2319 "permission_handler",
2320 &self.permission_handler.as_ref().map(|_| "<set>"),
2321 )
2322 .field(
2323 "elicitation_handler",
2324 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2325 )
2326 .field(
2327 "mcp_auth_handler",
2328 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2329 )
2330 .field(
2331 "user_input_handler",
2332 &self.user_input_handler.as_ref().map(|_| "<set>"),
2333 )
2334 .field(
2335 "exit_plan_mode_handler",
2336 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2337 )
2338 .field(
2339 "auto_mode_switch_handler",
2340 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2341 )
2342 .field(
2343 "hooks_handler",
2344 &self.hooks_handler.as_ref().map(|_| "<set>"),
2345 )
2346 .field(
2347 "system_message_transform",
2348 &self.system_message_transform.as_ref().map(|_| "<set>"),
2349 )
2350 .finish()
2351 }
2352}
2353
2354impl Default for SessionConfig {
2355 fn default() -> Self {
2361 Self {
2362 session_id: None,
2363 model: None,
2364 client_name: None,
2365 reasoning_effort: None,
2366 reasoning_summary: None,
2367 context_tier: None,
2368 streaming: None,
2369 system_message: None,
2370 tools: None,
2371 canvases: None,
2372 canvas_handler: None,
2373 request_canvas_renderer: None,
2374 request_extensions: None,
2375 extension_sdk_path: None,
2376 extension_info: None,
2377 canvas_provider: None,
2378 available_tools: None,
2379 excluded_tools: None,
2380 excluded_builtin_agents: None,
2381 mcp_servers: None,
2382 mcp_oauth_token_storage: None,
2383 enable_config_discovery: None,
2384 skip_embedding_retrieval: None,
2385 organization_custom_instructions: None,
2386 enable_on_demand_instruction_discovery: None,
2387 enable_file_hooks: None,
2388 enable_host_git_operations: None,
2389 enable_session_store: None,
2390 enable_skills: None,
2391 embedding_cache_storage: None,
2392 enable_mcp_apps: None,
2393 github_mcp_tool_config: None,
2394 skill_directories: None,
2395 instruction_directories: None,
2396 plugin_directories: None,
2397 large_output: None,
2398 tool_search: None,
2399 disabled_skills: None,
2400 disabled_mcp_servers: None,
2401 hooks: None,
2402 custom_agents: None,
2403 default_agent: None,
2404 agent: None,
2405 infinite_sessions: None,
2406 provider: None,
2407 capi: None,
2408 providers: None,
2409 models: None,
2410 enable_session_telemetry: None,
2411 enable_citations: None,
2412 enable_file_change_tracking: None,
2413 session_limits: None,
2414 model_capabilities: None,
2415 memory: None,
2416 config_directory: None,
2417 working_directory: None,
2418 additional_directories: None,
2419 github_token: None,
2420 remote_session: None,
2421 cloud: None,
2422 include_sub_agent_streaming_events: None,
2423 commands: None,
2424 exp_assignments: None,
2425 enable_managed_settings: None,
2426 managed_settings: None,
2427 session_fs_provider: None,
2428 permission_handler: None,
2429 elicitation_handler: None,
2430 mcp_auth_handler: None,
2431 user_input_handler: None,
2432 exit_plan_mode_handler: None,
2433 auto_mode_switch_handler: None,
2434 hooks_handler: None,
2435 permission_policy: None,
2436 system_message_transform: None,
2437 skip_custom_instructions: None,
2438 custom_agents_local_only: None,
2439 enable_experimental_mode: None,
2440 coauthor_enabled: None,
2441 manage_schedule_enabled: None,
2442 }
2443 }
2444}
2445
2446pub(crate) struct SessionConfigRuntime {
2452 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2453 pub permission_policy: Option<crate::permission::Policy>,
2454 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2455 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2456 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2457 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2458 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2459 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2460 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2461 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2462 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2463 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2464 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2465 pub commands: Option<Vec<CommandDefinition>>,
2466}
2467
2468impl SessionConfig {
2469 pub(crate) fn into_wire(
2481 mut self,
2482 session_id: Option<SessionId>,
2483 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2484 let permission_active =
2485 self.permission_handler.is_some() || self.permission_policy.is_some();
2486 let request_user_input = self.user_input_handler.is_some();
2487 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2488 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2489 let request_elicitation = self.elicitation_handler.is_some();
2490 let hooks_flag = self.hooks_handler.is_some();
2491
2492 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2493 if let Some(tools) = self.tools.as_mut() {
2494 for tool in tools.iter_mut() {
2495 if let Some(handler) = tool.handler.take()
2496 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2497 {
2498 return Err(crate::Error::with_message(
2499 crate::ErrorKind::InvalidConfig,
2500 format!("duplicate tool handler registered for name {:?}", tool.name),
2501 ));
2502 }
2503 }
2504 }
2505
2506 let wire_commands = self.commands.as_ref().map(|cmds| {
2507 cmds.iter()
2508 .map(|c| crate::wire::CommandWireDefinition {
2509 name: c.name.clone(),
2510 description: c.description.clone(),
2511 })
2512 .collect()
2513 });
2514 let wire_canvases = self.canvases.clone();
2515 let canvas_handler = self.canvas_handler.clone();
2516 let bearer_token_providers =
2517 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2518
2519 let wire = crate::wire::SessionCreateWire {
2520 session_id,
2521 model: self.model,
2522 client_name: self.client_name,
2523 reasoning_effort: self.reasoning_effort,
2524 reasoning_summary: self.reasoning_summary,
2525 context_tier: self.context_tier,
2526 streaming: self.streaming,
2527 system_message: self.system_message,
2528 tools: self.tools,
2529 canvases: wire_canvases,
2530 request_canvas_renderer: self.request_canvas_renderer,
2531 request_extensions: self.request_extensions,
2532 extension_sdk_path: self.extension_sdk_path,
2533 extension_info: self.extension_info,
2534 canvas_provider: self.canvas_provider,
2535 available_tools: self.available_tools,
2536 excluded_tools: self.excluded_tools,
2537 excluded_builtin_agents: self.excluded_builtin_agents,
2538 tool_filter_precedence: "excluded",
2539 mcp_servers: self.mcp_servers,
2540 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2541 embedding_cache_storage: self.embedding_cache_storage,
2542 env_value_mode: "direct",
2543 enable_config_discovery: self.enable_config_discovery,
2544 skip_embedding_retrieval: self.skip_embedding_retrieval,
2545 organization_custom_instructions: self.organization_custom_instructions,
2546 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2547 enable_file_hooks: self.enable_file_hooks,
2548 enable_host_git_operations: self.enable_host_git_operations,
2549 enable_session_store: self.enable_session_store,
2550 enable_skills: self.enable_skills,
2551 request_user_input,
2552 request_permission: permission_active,
2553 request_exit_plan_mode,
2554 request_auto_mode_switch,
2555 request_elicitation,
2556 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2557 github_mcp_tool_config: self.github_mcp_tool_config,
2558 hooks: hooks_flag,
2559 skill_directories: self.skill_directories,
2560 instruction_directories: self.instruction_directories,
2561 plugin_directories: self.plugin_directories,
2562 large_output: self.large_output,
2563 tool_search: self.tool_search,
2564 disabled_skills: self.disabled_skills,
2565 disabled_mcp_servers: self.disabled_mcp_servers,
2566 custom_agents: self.custom_agents,
2567 custom_agents_local_only: self.custom_agents_local_only,
2568 default_agent: self.default_agent,
2569 agent: self.agent,
2570 infinite_sessions: self.infinite_sessions,
2571 provider: self.provider,
2572 capi: self.capi,
2573 providers: self.providers,
2574 models: self.models,
2575 enable_session_telemetry: self.enable_session_telemetry,
2576 enable_citations: self.enable_citations,
2577 enable_file_change_tracking: self.enable_file_change_tracking,
2578 session_limits: self.session_limits,
2579 model_capabilities: self.model_capabilities,
2580 memory: self.memory,
2581 config_dir: self.config_directory,
2582 working_directory: self.working_directory,
2583 additional_directories: self.additional_directories,
2584 github_token: self.github_token,
2585 remote_session: self.remote_session,
2586 cloud: self.cloud,
2587 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2588 enable_github_telemetry_forwarding: None,
2589 commands: wire_commands,
2590 exp_assignments: self.exp_assignments,
2591 enable_managed_settings: self.enable_managed_settings,
2592 is_experimental_mode: self.enable_experimental_mode,
2593 managed_settings: self.managed_settings,
2594 };
2595
2596 let runtime = SessionConfigRuntime {
2597 permission_handler: self.permission_handler,
2598 permission_policy: self.permission_policy,
2599 elicitation_handler: self.elicitation_handler,
2600 mcp_auth_handler: self.mcp_auth_handler,
2601 user_input_handler: self.user_input_handler,
2602 exit_plan_mode_handler: self.exit_plan_mode_handler,
2603 auto_mode_switch_handler: self.auto_mode_switch_handler,
2604 hooks_handler: self.hooks_handler,
2605 system_message_transform: self.system_message_transform,
2606 tool_handlers,
2607 canvas_handler,
2608 session_fs_provider: self.session_fs_provider,
2609 bearer_token_providers,
2610 commands: self.commands,
2611 };
2612
2613 Ok((wire, runtime))
2614 }
2615
2616 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2620 self.permission_handler = Some(handler);
2621 self
2622 }
2623
2624 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2627 self.elicitation_handler = Some(handler);
2628 self
2629 }
2630
2631 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2633 self.mcp_auth_handler = Some(handler);
2634 self
2635 }
2636
2637 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2640 self.user_input_handler = Some(handler);
2641 self
2642 }
2643
2644 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2646 self.exit_plan_mode_handler = Some(handler);
2647 self
2648 }
2649
2650 pub fn with_auto_mode_switch_handler(
2652 mut self,
2653 handler: Arc<dyn AutoModeSwitchHandler>,
2654 ) -> Self {
2655 self.auto_mode_switch_handler = Some(handler);
2656 self
2657 }
2658
2659 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2664 self.commands = Some(commands);
2665 self
2666 }
2667
2668 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2672 self.session_fs_provider = Some(provider);
2673 self
2674 }
2675
2676 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2679 self.hooks_handler = Some(hooks);
2680 self
2681 }
2682
2683 pub fn with_system_message_transform(
2687 mut self,
2688 transform: Arc<dyn SystemMessageTransform>,
2689 ) -> Self {
2690 self.system_message_transform = Some(transform);
2691 self
2692 }
2693
2694 pub fn approve_all_permissions(mut self) -> Self {
2700 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2701 self
2702 }
2703
2704 pub fn deny_all_permissions(mut self) -> Self {
2707 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2708 self
2709 }
2710
2711 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2716 where
2717 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2718 {
2719 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2720 self
2721 }
2722
2723 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2725 self.session_id = Some(id.into());
2726 self
2727 }
2728
2729 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2731 self.model = Some(model.into());
2732 self
2733 }
2734
2735 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2737 self.client_name = Some(name.into());
2738 self
2739 }
2740
2741 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2743 self.reasoning_effort = Some(effort.into());
2744 self
2745 }
2746
2747 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2749 self.reasoning_summary = Some(summary);
2750 self
2751 }
2752
2753 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2755 self.context_tier = Some(tier.into());
2756 self
2757 }
2758
2759 pub fn with_streaming(mut self, streaming: bool) -> Self {
2761 self.streaming = Some(streaming);
2762 self
2763 }
2764
2765 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2767 self.system_message = Some(system_message);
2768 self
2769 }
2770
2771 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2773 self.tools = Some(tools.into_iter().collect());
2774 self
2775 }
2776
2777 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2782 self.canvases = Some(canvases.into_iter().collect());
2783 self
2784 }
2785
2786 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2788 self.canvas_handler = Some(handler);
2789 self
2790 }
2791
2792 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2794 self.request_canvas_renderer = Some(request);
2795 self
2796 }
2797
2798 pub fn with_request_extensions(mut self, request: bool) -> Self {
2800 self.request_extensions = Some(request);
2801 self
2802 }
2803
2804 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2808 self.extension_sdk_path = Some(path.into());
2809 self
2810 }
2811
2812 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2814 self.extension_info = Some(extension_info);
2815 self
2816 }
2817
2818 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2821 self.canvas_provider = Some(canvas_provider);
2822 self
2823 }
2824
2825 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2827 where
2828 I: IntoIterator<Item = S>,
2829 S: Into<String>,
2830 {
2831 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2832 self
2833 }
2834
2835 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2837 where
2838 I: IntoIterator<Item = S>,
2839 S: Into<String>,
2840 {
2841 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2842 self
2843 }
2844
2845 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2847 where
2848 I: IntoIterator<Item = S>,
2849 S: Into<String>,
2850 {
2851 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2852 self
2853 }
2854
2855 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2857 self.mcp_servers = Some(servers);
2858 self
2859 }
2860
2861 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2869 self.mcp_oauth_token_storage = Some(mode.into());
2870 self
2871 }
2872
2873 pub fn with_embedding_cache_storage(
2875 mut self,
2876 embedding_cache_storage: impl Into<String>,
2877 ) -> Self {
2878 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2879 self
2880 }
2881
2882 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2885 self.enable_config_discovery = Some(enable);
2886 self
2887 }
2888
2889 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2891 self.skip_embedding_retrieval = Some(value);
2892 self
2893 }
2894
2895 pub fn with_organization_custom_instructions(
2897 mut self,
2898 instructions: impl Into<String>,
2899 ) -> Self {
2900 self.organization_custom_instructions = Some(instructions.into());
2901 self
2902 }
2903
2904 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2906 self.enable_on_demand_instruction_discovery = Some(value);
2907 self
2908 }
2909
2910 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2912 self.enable_file_hooks = Some(value);
2913 self
2914 }
2915
2916 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2918 self.enable_host_git_operations = Some(value);
2919 self
2920 }
2921
2922 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2924 self.enable_session_store = Some(value);
2925 self
2926 }
2927
2928 pub fn with_enable_skills(mut self, value: bool) -> Self {
2930 self.enable_skills = Some(value);
2931 self
2932 }
2933
2934 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2940 self.enable_mcp_apps = Some(enable);
2941 self
2942 }
2943
2944 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
2946 self.github_mcp_tool_config = Some(config);
2947 self
2948 }
2949
2950 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2952 where
2953 I: IntoIterator<Item = P>,
2954 P: Into<PathBuf>,
2955 {
2956 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2957 self
2958 }
2959
2960 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2964 where
2965 I: IntoIterator<Item = P>,
2966 P: Into<PathBuf>,
2967 {
2968 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2969 self
2970 }
2971
2972 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2974 where
2975 I: IntoIterator<Item = P>,
2976 P: Into<PathBuf>,
2977 {
2978 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2979 self
2980 }
2981
2982 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2984 self.large_output = Some(config);
2985 self
2986 }
2987
2988 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
2991 self.tool_search = Some(config);
2992 self
2993 }
2994
2995 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2997 where
2998 I: IntoIterator<Item = S>,
2999 S: Into<String>,
3000 {
3001 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3002 self
3003 }
3004
3005 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
3007 where
3008 I: IntoIterator<Item = S>,
3009 S: Into<String>,
3010 {
3011 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
3012 self
3013 }
3014
3015 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3017 mut self,
3018 agents: I,
3019 ) -> Self {
3020 self.custom_agents = Some(agents.into_iter().collect());
3021 self
3022 }
3023
3024 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3026 self.default_agent = Some(agent);
3027 self
3028 }
3029
3030 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3033 self.agent = Some(name.into());
3034 self
3035 }
3036
3037 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3040 self.infinite_sessions = Some(config);
3041 self
3042 }
3043
3044 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3046 self.provider = Some(provider);
3047 self
3048 }
3049
3050 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3052 self.capi = Some(capi);
3053 self
3054 }
3055
3056 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3062 self.providers = Some(providers);
3063 self
3064 }
3065
3066 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3072 self.models = Some(models);
3073 self
3074 }
3075
3076 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3080 self.enable_session_telemetry = Some(enable);
3081 self
3082 }
3083
3084 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3086 self.enable_citations = Some(enable);
3087 self
3088 }
3089
3090 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
3093 self.enable_file_change_tracking = Some(enable);
3094 self
3095 }
3096
3097 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3099 self.session_limits = Some(limits);
3100 self
3101 }
3102
3103 pub fn with_model_capabilities(
3105 mut self,
3106 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3107 ) -> Self {
3108 self.model_capabilities = Some(capabilities);
3109 self
3110 }
3111
3112 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3114 self.memory = Some(memory);
3115 self
3116 }
3117
3118 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3120 self.config_directory = Some(dir.into());
3121 self
3122 }
3123
3124 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_additional_directories<I, P>(mut self, paths: I) -> Self
3133 where
3134 I: IntoIterator<Item = P>,
3135 P: Into<PathBuf>,
3136 {
3137 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
3138 self
3139 }
3140
3141 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3146 self.github_token = Some(token.into());
3147 self
3148 }
3149
3150 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3153 self.include_sub_agent_streaming_events = Some(include);
3154 self
3155 }
3156
3157 pub fn with_remote_session(
3159 mut self,
3160 mode: crate::generated::api_types::RemoteSessionMode,
3161 ) -> Self {
3162 self.remote_session = Some(mode);
3163 self
3164 }
3165
3166 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
3168 self.cloud = Some(cloud);
3169 self
3170 }
3171
3172 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3174 self.skip_custom_instructions = Some(value);
3175 self
3176 }
3177
3178 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3180 self.custom_agents_local_only = Some(value);
3181 self
3182 }
3183
3184 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
3186 self.enable_experimental_mode = Some(enable_experimental_mode);
3187 self
3188 }
3189
3190 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3192 self.coauthor_enabled = Some(value);
3193 self
3194 }
3195
3196 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3198 self.manage_schedule_enabled = Some(value);
3199 self
3200 }
3201
3202 #[doc(hidden)]
3210 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
3211 self.exp_assignments = Some(assignments);
3212 self
3213 }
3214
3215 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3221 self.enable_managed_settings = Some(enabled);
3222 self
3223 }
3224
3225 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
3230 self.managed_settings = Some(managed_settings);
3231 self
3232 }
3233}
3234#[derive(Clone)]
3241#[non_exhaustive]
3242pub struct ResumeSessionConfig {
3243 pub session_id: SessionId,
3245 pub model: Option<String>,
3248 pub client_name: Option<String>,
3250 pub reasoning_effort: Option<String>,
3252 pub reasoning_summary: Option<ReasoningSummary>,
3256 pub context_tier: Option<String>,
3259 pub streaming: Option<bool>,
3261 pub system_message: Option<SystemMessageConfig>,
3264 pub tools: Option<Vec<Tool>>,
3266 pub canvases: Option<Vec<CanvasDeclaration>>,
3268 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
3271 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3273 pub request_canvas_renderer: Option<bool>,
3275 pub request_extensions: Option<bool>,
3277 pub extension_sdk_path: Option<String>,
3281 pub extension_info: Option<ExtensionInfo>,
3283 pub canvas_provider: Option<CanvasProviderIdentity>,
3286 pub available_tools: Option<Vec<String>>,
3288 pub excluded_tools: Option<Vec<String>>,
3290 pub excluded_builtin_agents: Option<Vec<String>>,
3296 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3298 pub mcp_oauth_token_storage: Option<String>,
3301 pub enable_config_discovery: Option<bool>,
3304 pub skip_embedding_retrieval: Option<bool>,
3306 pub embedding_cache_storage: Option<String>,
3308 pub organization_custom_instructions: Option<String>,
3310 pub enable_on_demand_instruction_discovery: Option<bool>,
3312 pub enable_file_hooks: Option<bool>,
3314 pub enable_host_git_operations: Option<bool>,
3316 pub enable_session_store: Option<bool>,
3318 pub enable_skills: Option<bool>,
3320 pub enable_mcp_apps: Option<bool>,
3326 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
3331 pub skill_directories: Option<Vec<PathBuf>>,
3333 pub instruction_directories: Option<Vec<PathBuf>>,
3336 pub plugin_directories: Option<Vec<PathBuf>>,
3338 pub large_output: Option<LargeToolOutputConfig>,
3340 pub tool_search: Option<ToolSearchConfig>,
3343 pub disabled_skills: Option<Vec<String>>,
3345 pub disabled_mcp_servers: Option<Vec<String>>,
3348 pub hooks: Option<bool>,
3350 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3352 pub default_agent: Option<DefaultAgentConfig>,
3354 pub agent: Option<String>,
3356 pub infinite_sessions: Option<InfiniteSessionConfig>,
3358 pub provider: Option<ProviderConfig>,
3360 pub capi: Option<CapiSessionOptions>,
3366 pub providers: Option<Vec<NamedProviderConfig>>,
3372 pub models: Option<Vec<ProviderModelConfig>>,
3378 pub enable_session_telemetry: Option<bool>,
3386 pub enable_citations: Option<bool>,
3388 pub enable_file_change_tracking: Option<bool>,
3392 pub session_limits: Option<SessionLimitsConfig>,
3394 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3396 pub memory: Option<MemoryConfiguration>,
3398 pub config_directory: Option<PathBuf>,
3400 pub working_directory: Option<PathBuf>,
3402 pub additional_directories: Option<Vec<PathBuf>>,
3405 pub github_token: Option<String>,
3408 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3411 pub include_sub_agent_streaming_events: Option<bool>,
3413 pub commands: Option<Vec<CommandDefinition>>,
3417 #[doc(hidden)]
3422 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3423 pub enable_managed_settings: Option<bool>,
3429 pub managed_settings: Option<ManagedSettings>,
3435 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3440 pub suppress_resume_event: Option<bool>,
3443 pub continue_pending_work: Option<bool>,
3451 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3454 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3457 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3459 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3462 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3465 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3468 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3470 pub(crate) permission_policy: Option<crate::permission::Policy>,
3472 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3474 pub skip_custom_instructions: Option<bool>,
3476 pub custom_agents_local_only: Option<bool>,
3478 pub enable_experimental_mode: Option<bool>,
3483 pub coauthor_enabled: Option<bool>,
3485 pub manage_schedule_enabled: Option<bool>,
3487}
3488
3489impl std::fmt::Debug for ResumeSessionConfig {
3490 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3491 f.debug_struct("ResumeSessionConfig")
3492 .field("session_id", &self.session_id)
3493 .field("model", &self.model)
3494 .field("client_name", &self.client_name)
3495 .field("reasoning_effort", &self.reasoning_effort)
3496 .field("reasoning_summary", &self.reasoning_summary)
3497 .field("context_tier", &self.context_tier)
3498 .field("streaming", &self.streaming)
3499 .field("system_message", &self.system_message)
3500 .field("tools", &self.tools)
3501 .field("canvases", &self.canvases)
3502 .field(
3503 "canvas_handler",
3504 &self.canvas_handler.as_ref().map(|_| "<set>"),
3505 )
3506 .field("open_canvases", &self.open_canvases)
3507 .field("request_canvas_renderer", &self.request_canvas_renderer)
3508 .field("request_extensions", &self.request_extensions)
3509 .field("extension_sdk_path", &self.extension_sdk_path)
3510 .field("extension_info", &self.extension_info)
3511 .field("canvas_provider", &self.canvas_provider)
3512 .field("available_tools", &self.available_tools)
3513 .field("excluded_tools", &self.excluded_tools)
3514 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3515 .field("mcp_servers", &self.mcp_servers)
3516 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3517 .field("embedding_cache_storage", &self.embedding_cache_storage)
3518 .field("enable_config_discovery", &self.enable_config_discovery)
3519 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3520 .field(
3521 "organization_custom_instructions",
3522 &self
3523 .organization_custom_instructions
3524 .as_ref()
3525 .map(|_| "<redacted>"),
3526 )
3527 .field(
3528 "enable_on_demand_instruction_discovery",
3529 &self.enable_on_demand_instruction_discovery,
3530 )
3531 .field("enable_file_hooks", &self.enable_file_hooks)
3532 .field(
3533 "enable_host_git_operations",
3534 &self.enable_host_git_operations,
3535 )
3536 .field("enable_session_store", &self.enable_session_store)
3537 .field("enable_skills", &self.enable_skills)
3538 .field("enable_mcp_apps", &self.enable_mcp_apps)
3539 .field("skill_directories", &self.skill_directories)
3540 .field("instruction_directories", &self.instruction_directories)
3541 .field("plugin_directories", &self.plugin_directories)
3542 .field("large_output", &self.large_output)
3543 .field("tool_search", &self.tool_search)
3544 .field("disabled_skills", &self.disabled_skills)
3545 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
3546 .field("hooks", &self.hooks)
3547 .field("custom_agents", &self.custom_agents)
3548 .field("default_agent", &self.default_agent)
3549 .field("agent", &self.agent)
3550 .field("infinite_sessions", &self.infinite_sessions)
3551 .field("provider", &self.provider)
3552 .field("capi", &self.capi)
3553 .field("enable_session_telemetry", &self.enable_session_telemetry)
3554 .field("enable_citations", &self.enable_citations)
3555 .field(
3556 "enable_file_change_tracking",
3557 &self.enable_file_change_tracking,
3558 )
3559 .field("session_limits", &self.session_limits)
3560 .field("model_capabilities", &self.model_capabilities)
3561 .field("memory", &self.memory)
3562 .field("config_directory", &self.config_directory)
3563 .field("working_directory", &self.working_directory)
3564 .field("additional_directories", &self.additional_directories)
3565 .field(
3566 "github_token",
3567 &self.github_token.as_ref().map(|_| "<redacted>"),
3568 )
3569 .field("remote_session", &self.remote_session)
3570 .field(
3571 "include_sub_agent_streaming_events",
3572 &self.include_sub_agent_streaming_events,
3573 )
3574 .field("commands", &self.commands)
3575 .field("exp_assignments", &self.exp_assignments)
3576 .field("enable_managed_settings", &self.enable_managed_settings)
3577 .field("enable_experimental_mode", &self.enable_experimental_mode)
3578 .field("managed_settings", &self.managed_settings)
3579 .field(
3580 "session_fs_provider",
3581 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3582 )
3583 .field(
3584 "permission_handler",
3585 &self.permission_handler.as_ref().map(|_| "<set>"),
3586 )
3587 .field(
3588 "elicitation_handler",
3589 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3590 )
3591 .field(
3592 "user_input_handler",
3593 &self.user_input_handler.as_ref().map(|_| "<set>"),
3594 )
3595 .field(
3596 "exit_plan_mode_handler",
3597 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3598 )
3599 .field(
3600 "auto_mode_switch_handler",
3601 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3602 )
3603 .field(
3604 "hooks_handler",
3605 &self.hooks_handler.as_ref().map(|_| "<set>"),
3606 )
3607 .field(
3608 "system_message_transform",
3609 &self.system_message_transform.as_ref().map(|_| "<set>"),
3610 )
3611 .field("suppress_resume_event", &self.suppress_resume_event)
3612 .field("continue_pending_work", &self.continue_pending_work)
3613 .finish()
3614 }
3615}
3616
3617impl ResumeSessionConfig {
3618 pub(crate) fn into_wire(
3626 mut self,
3627 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3628 let permission_active =
3629 self.permission_handler.is_some() || self.permission_policy.is_some();
3630 let request_user_input = self.user_input_handler.is_some();
3631 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3632 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3633 let request_elicitation = self.elicitation_handler.is_some();
3634 let hooks_flag = self.hooks_handler.is_some();
3635
3636 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3637 if let Some(tools) = self.tools.as_mut() {
3638 for tool in tools.iter_mut() {
3639 if let Some(handler) = tool.handler.take()
3640 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3641 {
3642 return Err(crate::Error::with_message(
3643 crate::ErrorKind::InvalidConfig,
3644 format!("duplicate tool handler registered for name {:?}", tool.name),
3645 ));
3646 }
3647 }
3648 }
3649
3650 let wire_commands = self.commands.as_ref().map(|cmds| {
3651 cmds.iter()
3652 .map(|c| crate::wire::CommandWireDefinition {
3653 name: c.name.clone(),
3654 description: c.description.clone(),
3655 })
3656 .collect()
3657 });
3658 let wire_canvases = self.canvases.clone();
3659 let canvas_handler = self.canvas_handler.clone();
3660 let bearer_token_providers =
3661 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3662
3663 let wire = crate::wire::SessionResumeWire {
3664 session_id: self.session_id,
3665 model: self.model,
3666 client_name: self.client_name,
3667 reasoning_effort: self.reasoning_effort,
3668 reasoning_summary: self.reasoning_summary,
3669 context_tier: self.context_tier,
3670 streaming: self.streaming,
3671 system_message: self.system_message,
3672 tools: self.tools,
3673 canvases: wire_canvases,
3674 open_canvases: self.open_canvases,
3675 request_canvas_renderer: self.request_canvas_renderer,
3676 request_extensions: self.request_extensions,
3677 extension_sdk_path: self.extension_sdk_path,
3678 extension_info: self.extension_info,
3679 canvas_provider: self.canvas_provider,
3680 available_tools: self.available_tools,
3681 excluded_tools: self.excluded_tools,
3682 excluded_builtin_agents: self.excluded_builtin_agents,
3683 tool_filter_precedence: "excluded",
3684 mcp_servers: self.mcp_servers,
3685 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3686 embedding_cache_storage: self.embedding_cache_storage,
3687 env_value_mode: "direct",
3688 enable_config_discovery: self.enable_config_discovery,
3689 skip_embedding_retrieval: self.skip_embedding_retrieval,
3690 organization_custom_instructions: self.organization_custom_instructions,
3691 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3692 enable_file_hooks: self.enable_file_hooks,
3693 enable_host_git_operations: self.enable_host_git_operations,
3694 enable_session_store: self.enable_session_store,
3695 enable_skills: self.enable_skills,
3696 request_user_input,
3697 request_permission: permission_active,
3698 request_exit_plan_mode,
3699 request_auto_mode_switch,
3700 request_elicitation,
3701 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3702 github_mcp_tool_config: self.github_mcp_tool_config,
3703 hooks: hooks_flag,
3704 skill_directories: self.skill_directories,
3705 instruction_directories: self.instruction_directories,
3706 plugin_directories: self.plugin_directories,
3707 large_output: self.large_output,
3708 tool_search: self.tool_search,
3709 disabled_skills: self.disabled_skills,
3710 disabled_mcp_servers: self.disabled_mcp_servers,
3711 custom_agents: self.custom_agents,
3712 custom_agents_local_only: self.custom_agents_local_only,
3713 default_agent: self.default_agent,
3714 agent: self.agent,
3715 infinite_sessions: self.infinite_sessions,
3716 provider: self.provider,
3717 capi: self.capi,
3718 providers: self.providers,
3719 models: self.models,
3720 enable_session_telemetry: self.enable_session_telemetry,
3721 enable_citations: self.enable_citations,
3722 enable_file_change_tracking: self.enable_file_change_tracking,
3723 session_limits: self.session_limits,
3724 model_capabilities: self.model_capabilities,
3725 memory: self.memory,
3726 config_dir: self.config_directory,
3727 working_directory: self.working_directory,
3728 additional_directories: self.additional_directories,
3729 github_token: self.github_token,
3730 remote_session: self.remote_session,
3731 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3732 enable_github_telemetry_forwarding: None,
3733 commands: wire_commands,
3734 exp_assignments: self.exp_assignments,
3735 enable_managed_settings: self.enable_managed_settings,
3736 is_experimental_mode: self.enable_experimental_mode,
3737 managed_settings: self.managed_settings,
3738 suppress_resume_event: self.suppress_resume_event,
3739 continue_pending_work: self.continue_pending_work,
3740 };
3741
3742 let runtime = SessionConfigRuntime {
3743 permission_handler: self.permission_handler,
3744 permission_policy: self.permission_policy,
3745 elicitation_handler: self.elicitation_handler,
3746 mcp_auth_handler: self.mcp_auth_handler,
3747 user_input_handler: self.user_input_handler,
3748 exit_plan_mode_handler: self.exit_plan_mode_handler,
3749 auto_mode_switch_handler: self.auto_mode_switch_handler,
3750 hooks_handler: self.hooks_handler,
3751 system_message_transform: self.system_message_transform,
3752 tool_handlers,
3753 canvas_handler,
3754 session_fs_provider: self.session_fs_provider,
3755 bearer_token_providers,
3756 commands: self.commands,
3757 };
3758
3759 Ok((wire, runtime))
3760 }
3761
3762 pub fn new(session_id: SessionId) -> Self {
3767 Self {
3768 session_id,
3769 model: None,
3770 client_name: None,
3771 reasoning_effort: None,
3772 reasoning_summary: None,
3773 context_tier: None,
3774 streaming: None,
3775 system_message: None,
3776 tools: None,
3777 canvases: None,
3778 canvas_handler: None,
3779 open_canvases: None,
3780 request_canvas_renderer: None,
3781 request_extensions: None,
3782 extension_sdk_path: None,
3783 extension_info: None,
3784 canvas_provider: None,
3785 available_tools: None,
3786 excluded_tools: None,
3787 excluded_builtin_agents: None,
3788 mcp_servers: None,
3789 mcp_oauth_token_storage: None,
3790 enable_config_discovery: None,
3791 skip_embedding_retrieval: None,
3792 organization_custom_instructions: None,
3793 enable_on_demand_instruction_discovery: None,
3794 enable_file_hooks: None,
3795 enable_host_git_operations: None,
3796 enable_session_store: None,
3797 enable_skills: None,
3798 embedding_cache_storage: None,
3799 enable_mcp_apps: None,
3800 github_mcp_tool_config: None,
3801 skill_directories: None,
3802 instruction_directories: None,
3803 plugin_directories: None,
3804 large_output: None,
3805 tool_search: None,
3806 disabled_skills: None,
3807 disabled_mcp_servers: None,
3808 hooks: None,
3809 custom_agents: None,
3810 default_agent: None,
3811 agent: None,
3812 infinite_sessions: None,
3813 provider: None,
3814 capi: None,
3815 providers: None,
3816 models: None,
3817 enable_session_telemetry: None,
3818 enable_citations: None,
3819 enable_file_change_tracking: None,
3820 session_limits: None,
3821 model_capabilities: None,
3822 memory: None,
3823 config_directory: None,
3824 working_directory: None,
3825 additional_directories: None,
3826 github_token: None,
3827 remote_session: None,
3828 include_sub_agent_streaming_events: None,
3829 commands: None,
3830 exp_assignments: None,
3831 enable_managed_settings: None,
3832 managed_settings: None,
3833 session_fs_provider: None,
3834 suppress_resume_event: None,
3835 continue_pending_work: None,
3836 permission_handler: None,
3837 elicitation_handler: None,
3838 mcp_auth_handler: None,
3839 user_input_handler: None,
3840 exit_plan_mode_handler: None,
3841 auto_mode_switch_handler: None,
3842 hooks_handler: None,
3843 permission_policy: None,
3844 system_message_transform: None,
3845 skip_custom_instructions: None,
3846 custom_agents_local_only: None,
3847 enable_experimental_mode: None,
3848 coauthor_enabled: None,
3849 manage_schedule_enabled: None,
3850 }
3851 }
3852
3853 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3855 self.permission_handler = Some(handler);
3856 self
3857 }
3858
3859 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3861 self.elicitation_handler = Some(handler);
3862 self
3863 }
3864
3865 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3867 self.mcp_auth_handler = Some(handler);
3868 self
3869 }
3870
3871 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3873 self.user_input_handler = Some(handler);
3874 self
3875 }
3876
3877 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3879 self.exit_plan_mode_handler = Some(handler);
3880 self
3881 }
3882
3883 pub fn with_auto_mode_switch_handler(
3885 mut self,
3886 handler: Arc<dyn AutoModeSwitchHandler>,
3887 ) -> Self {
3888 self.auto_mode_switch_handler = Some(handler);
3889 self
3890 }
3891
3892 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3895 self.hooks_handler = Some(hooks);
3896 self
3897 }
3898
3899 pub fn with_system_message_transform(
3901 mut self,
3902 transform: Arc<dyn SystemMessageTransform>,
3903 ) -> Self {
3904 self.system_message_transform = Some(transform);
3905 self
3906 }
3907
3908 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3912 self.commands = Some(commands);
3913 self
3914 }
3915
3916 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3919 self.session_fs_provider = Some(provider);
3920 self
3921 }
3922
3923 pub fn approve_all_permissions(mut self) -> Self {
3926 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3927 self
3928 }
3929
3930 pub fn deny_all_permissions(mut self) -> Self {
3933 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3934 self
3935 }
3936
3937 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3940 where
3941 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3942 {
3943 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3944 self
3945 }
3946
3947 pub fn with_model(mut self, model: impl Into<String>) -> Self {
3949 self.model = Some(model.into());
3950 self
3951 }
3952
3953 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3955 self.client_name = Some(name.into());
3956 self
3957 }
3958
3959 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3961 self.reasoning_effort = Some(effort.into());
3962 self
3963 }
3964
3965 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3967 self.reasoning_summary = Some(summary);
3968 self
3969 }
3970
3971 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3974 self.context_tier = Some(tier.into());
3975 self
3976 }
3977
3978 pub fn with_streaming(mut self, streaming: bool) -> Self {
3980 self.streaming = Some(streaming);
3981 self
3982 }
3983
3984 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3987 self.system_message = Some(system_message);
3988 self
3989 }
3990
3991 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3993 self.tools = Some(tools.into_iter().collect());
3994 self
3995 }
3996
3997 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3999 self.canvases = Some(canvases.into_iter().collect());
4000 self
4001 }
4002
4003 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
4005 self.canvas_handler = Some(handler);
4006 self
4007 }
4008
4009 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
4011 mut self,
4012 open_canvases: I,
4013 ) -> Self {
4014 self.open_canvases = Some(open_canvases.into_iter().collect());
4015 self
4016 }
4017
4018 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
4020 self.request_canvas_renderer = Some(request);
4021 self
4022 }
4023
4024 pub fn with_request_extensions(mut self, request: bool) -> Self {
4026 self.request_extensions = Some(request);
4027 self
4028 }
4029
4030 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
4034 self.extension_sdk_path = Some(path.into());
4035 self
4036 }
4037
4038 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
4040 self.extension_info = Some(extension_info);
4041 self
4042 }
4043
4044 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
4047 self.canvas_provider = Some(canvas_provider);
4048 self
4049 }
4050
4051 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
4053 where
4054 I: IntoIterator<Item = S>,
4055 S: Into<String>,
4056 {
4057 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
4058 self
4059 }
4060
4061 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
4063 where
4064 I: IntoIterator<Item = S>,
4065 S: Into<String>,
4066 {
4067 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
4068 self
4069 }
4070
4071 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
4073 where
4074 I: IntoIterator<Item = S>,
4075 S: Into<String>,
4076 {
4077 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
4078 self
4079 }
4080
4081 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
4083 self.mcp_servers = Some(servers);
4084 self
4085 }
4086
4087 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
4090 self.mcp_oauth_token_storage = Some(mode.into());
4091 self
4092 }
4093
4094 pub fn with_embedding_cache_storage(
4096 mut self,
4097 embedding_cache_storage: impl Into<String>,
4098 ) -> Self {
4099 self.embedding_cache_storage = Some(embedding_cache_storage.into());
4100 self
4101 }
4102
4103 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
4106 self.enable_config_discovery = Some(enable);
4107 self
4108 }
4109
4110 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
4112 self.skip_embedding_retrieval = Some(value);
4113 self
4114 }
4115
4116 pub fn with_organization_custom_instructions(
4118 mut self,
4119 instructions: impl Into<String>,
4120 ) -> Self {
4121 self.organization_custom_instructions = Some(instructions.into());
4122 self
4123 }
4124
4125 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
4127 self.enable_on_demand_instruction_discovery = Some(value);
4128 self
4129 }
4130
4131 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
4133 self.enable_file_hooks = Some(value);
4134 self
4135 }
4136
4137 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
4139 self.enable_host_git_operations = Some(value);
4140 self
4141 }
4142
4143 pub fn with_enable_session_store(mut self, value: bool) -> Self {
4145 self.enable_session_store = Some(value);
4146 self
4147 }
4148
4149 pub fn with_enable_skills(mut self, value: bool) -> Self {
4151 self.enable_skills = Some(value);
4152 self
4153 }
4154
4155 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
4161 self.enable_mcp_apps = Some(enable);
4162 self
4163 }
4164
4165 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
4167 self.github_mcp_tool_config = Some(config);
4168 self
4169 }
4170
4171 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
4173 where
4174 I: IntoIterator<Item = P>,
4175 P: Into<PathBuf>,
4176 {
4177 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
4178 self
4179 }
4180
4181 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
4185 where
4186 I: IntoIterator<Item = P>,
4187 P: Into<PathBuf>,
4188 {
4189 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
4190 self
4191 }
4192
4193 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
4195 where
4196 I: IntoIterator<Item = P>,
4197 P: Into<PathBuf>,
4198 {
4199 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
4200 self
4201 }
4202
4203 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
4205 self.large_output = Some(config);
4206 self
4207 }
4208
4209 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
4212 self.tool_search = Some(config);
4213 self
4214 }
4215
4216 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
4218 where
4219 I: IntoIterator<Item = S>,
4220 S: Into<String>,
4221 {
4222 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
4223 self
4224 }
4225
4226 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
4228 where
4229 I: IntoIterator<Item = S>,
4230 S: Into<String>,
4231 {
4232 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
4233 self
4234 }
4235
4236 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
4238 mut self,
4239 agents: I,
4240 ) -> Self {
4241 self.custom_agents = Some(agents.into_iter().collect());
4242 self
4243 }
4244
4245 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
4247 self.default_agent = Some(agent);
4248 self
4249 }
4250
4251 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
4253 self.agent = Some(name.into());
4254 self
4255 }
4256
4257 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
4259 self.infinite_sessions = Some(config);
4260 self
4261 }
4262
4263 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
4265 self.provider = Some(provider);
4266 self
4267 }
4268
4269 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
4271 self.capi = Some(capi);
4272 self
4273 }
4274
4275 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
4281 self.providers = Some(providers);
4282 self
4283 }
4284
4285 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
4291 self.models = Some(models);
4292 self
4293 }
4294
4295 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
4299 self.enable_session_telemetry = Some(enable);
4300 self
4301 }
4302
4303 pub fn with_enable_citations(mut self, enable: bool) -> Self {
4305 self.enable_citations = Some(enable);
4306 self
4307 }
4308
4309 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
4312 self.enable_file_change_tracking = Some(enable);
4313 self
4314 }
4315
4316 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
4318 self.session_limits = Some(limits);
4319 self
4320 }
4321
4322 pub fn with_model_capabilities(
4324 mut self,
4325 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
4326 ) -> Self {
4327 self.model_capabilities = Some(capabilities);
4328 self
4329 }
4330
4331 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
4333 self.memory = Some(memory);
4334 self
4335 }
4336
4337 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4339 self.config_directory = Some(dir.into());
4340 self
4341 }
4342
4343 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4345 self.working_directory = Some(dir.into());
4346 self
4347 }
4348
4349 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
4351 where
4352 I: IntoIterator<Item = P>,
4353 P: Into<PathBuf>,
4354 {
4355 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
4356 self
4357 }
4358
4359 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
4363 self.github_token = Some(token.into());
4364 self
4365 }
4366
4367 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4369 self.include_sub_agent_streaming_events = Some(include);
4370 self
4371 }
4372
4373 pub fn with_remote_session(
4375 mut self,
4376 mode: crate::generated::api_types::RemoteSessionMode,
4377 ) -> Self {
4378 self.remote_session = Some(mode);
4379 self
4380 }
4381
4382 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4385 self.suppress_resume_event = Some(suppress);
4386 self
4387 }
4388
4389 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4395 self.continue_pending_work = Some(continue_pending);
4396 self
4397 }
4398
4399 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4401 self.skip_custom_instructions = Some(value);
4402 self
4403 }
4404
4405 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4407 self.custom_agents_local_only = Some(value);
4408 self
4409 }
4410
4411 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
4413 self.enable_experimental_mode = Some(enable_experimental_mode);
4414 self
4415 }
4416
4417 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4419 self.coauthor_enabled = Some(value);
4420 self
4421 }
4422
4423 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4425 self.manage_schedule_enabled = Some(value);
4426 self
4427 }
4428
4429 #[doc(hidden)]
4433 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4434 self.exp_assignments = Some(assignments);
4435 self
4436 }
4437
4438 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4441 self.enable_managed_settings = Some(enabled);
4442 self
4443 }
4444
4445 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
4449 self.managed_settings = Some(managed_settings);
4450 self
4451 }
4452}
4453
4454#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4460#[serde(rename_all = "camelCase")]
4461#[non_exhaustive]
4462pub struct SystemMessageConfig {
4463 #[serde(skip_serializing_if = "Option::is_none")]
4465 pub mode: Option<String>,
4466 #[serde(skip_serializing_if = "Option::is_none")]
4468 pub content: Option<String>,
4469 #[serde(skip_serializing_if = "Option::is_none")]
4471 pub sections: Option<HashMap<String, SectionOverride>>,
4472}
4473
4474impl SystemMessageConfig {
4475 pub fn new() -> Self {
4478 Self::default()
4479 }
4480
4481 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4484 self.mode = Some(mode.into());
4485 self
4486 }
4487
4488 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4491 self.content = Some(content.into());
4492 self
4493 }
4494
4495 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4497 self.sections = Some(sections);
4498 self
4499 }
4500}
4501
4502#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4508#[serde(rename_all = "camelCase")]
4509pub struct SectionOverride {
4510 #[serde(skip_serializing_if = "Option::is_none")]
4513 pub action: Option<String>,
4514 #[serde(skip_serializing_if = "Option::is_none")]
4516 pub content: Option<String>,
4517}
4518
4519#[derive(Debug, Clone, Serialize, Deserialize)]
4521#[serde(rename_all = "camelCase")]
4522pub struct CreateSessionResult {
4523 pub session_id: SessionId,
4525 #[serde(skip_serializing_if = "Option::is_none")]
4527 pub workspace_path: Option<PathBuf>,
4528 #[serde(default, alias = "remote_url")]
4530 pub remote_url: Option<String>,
4531 #[serde(skip_serializing_if = "Option::is_none")]
4533 pub capabilities: Option<SessionCapabilities>,
4534}
4535
4536#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4538#[serde(rename_all = "camelCase")]
4539pub(crate) struct ResumeSessionResult {
4540 #[serde(default)]
4542 pub session_id: Option<SessionId>,
4543 #[serde(default, skip_serializing_if = "Option::is_none")]
4545 pub workspace_path: Option<PathBuf>,
4546 #[serde(default, alias = "remote_url")]
4548 pub remote_url: Option<String>,
4549 #[serde(default, skip_serializing_if = "Option::is_none")]
4551 pub capabilities: Option<SessionCapabilities>,
4552 #[serde(
4554 default,
4555 alias = "openCanvasInstances",
4556 skip_serializing_if = "Option::is_none"
4557 )]
4558 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4559}
4560
4561#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4563#[serde(rename_all = "lowercase")]
4564pub enum LogLevel {
4565 #[default]
4567 Info,
4568 Warning,
4570 Error,
4572}
4573
4574#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4579#[serde(rename_all = "camelCase")]
4580pub struct LogOptions {
4581 #[serde(skip_serializing_if = "Option::is_none")]
4583 pub level: Option<LogLevel>,
4584 #[serde(skip_serializing_if = "Option::is_none")]
4587 pub ephemeral: Option<bool>,
4588}
4589
4590impl LogOptions {
4591 pub fn with_level(mut self, level: LogLevel) -> Self {
4593 self.level = Some(level);
4594 self
4595 }
4596
4597 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4599 self.ephemeral = Some(ephemeral);
4600 self
4601 }
4602}
4603
4604#[derive(Debug, Clone, Default)]
4608pub struct SetModelOptions {
4609 pub reasoning_effort: Option<String>,
4612 pub reasoning_summary: Option<ReasoningSummary>,
4616 pub context_tier: Option<ContextTier>,
4619 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4623}
4624
4625impl SetModelOptions {
4626 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4628 self.reasoning_effort = Some(effort.into());
4629 self
4630 }
4631
4632 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4634 self.reasoning_summary = Some(summary);
4635 self
4636 }
4637
4638 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4640 self.context_tier = Some(tier);
4641 self
4642 }
4643
4644 pub fn with_model_capabilities(
4646 mut self,
4647 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4648 ) -> Self {
4649 self.model_capabilities = Some(caps);
4650 self
4651 }
4652}
4653
4654#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4661#[serde(rename_all = "camelCase")]
4662pub struct PingResponse {
4663 #[serde(default)]
4665 pub message: String,
4666 #[serde(default)]
4668 pub timestamp: String,
4669 #[serde(skip_serializing_if = "Option::is_none")]
4671 pub protocol_version: Option<u32>,
4672}
4673
4674#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4676#[serde(rename_all = "camelCase")]
4677pub struct AttachmentLineRange {
4678 pub start: u32,
4680 pub end: u32,
4682}
4683
4684#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4686#[serde(rename_all = "camelCase")]
4687pub struct AttachmentSelectionPosition {
4688 pub line: u32,
4690 pub character: u32,
4692}
4693
4694#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4696#[serde(rename_all = "camelCase")]
4697pub struct AttachmentSelectionRange {
4698 pub start: AttachmentSelectionPosition,
4700 pub end: AttachmentSelectionPosition,
4702}
4703
4704#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4706#[serde(rename_all = "snake_case")]
4707#[non_exhaustive]
4708pub enum GitHubReferenceType {
4709 Issue,
4711 Pr,
4713 Discussion,
4715}
4716
4717#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4723#[serde(rename_all = "camelCase")]
4724pub struct GitHubRepoPointer {
4725 #[serde(skip_serializing_if = "Option::is_none")]
4727 pub id: Option<i64>,
4728 pub name: String,
4730 pub owner: String,
4732}
4733
4734#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4736#[serde(rename_all = "camelCase")]
4737pub struct GitHubFileDiffSide {
4738 pub path: String,
4740 pub r#ref: String,
4742 pub repo: GitHubRepoPointer,
4744}
4745
4746#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4748#[serde(rename_all = "camelCase")]
4749pub struct GitHubTreeComparisonSide {
4750 pub repo: GitHubRepoPointer,
4752 pub revision: String,
4754}
4755
4756#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4758#[serde(rename_all = "camelCase")]
4759pub struct GitHubSnippetLineRange {
4760 pub start: i64,
4762 pub end: i64,
4764}
4765
4766#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4768#[serde(
4769 tag = "type",
4770 rename_all = "camelCase",
4771 rename_all_fields = "camelCase"
4772)]
4773#[non_exhaustive]
4774pub enum Attachment {
4775 File {
4777 path: PathBuf,
4779 #[serde(skip_serializing_if = "Option::is_none")]
4781 display_name: Option<String>,
4782 #[serde(skip_serializing_if = "Option::is_none")]
4784 line_range: Option<AttachmentLineRange>,
4785 },
4786 Directory {
4788 path: PathBuf,
4790 #[serde(skip_serializing_if = "Option::is_none")]
4792 display_name: Option<String>,
4793 },
4794 Selection {
4796 file_path: PathBuf,
4798 text: String,
4800 #[serde(skip_serializing_if = "Option::is_none")]
4802 display_name: Option<String>,
4803 selection: AttachmentSelectionRange,
4805 },
4806 Blob {
4808 data: String,
4810 mime_type: String,
4812 #[serde(skip_serializing_if = "Option::is_none")]
4814 display_name: Option<String>,
4815 },
4816 #[serde(rename = "github_reference")]
4818 GitHubReference {
4819 number: u64,
4821 title: String,
4823 reference_type: GitHubReferenceType,
4825 state: String,
4827 url: String,
4829 },
4830 #[serde(rename = "github_commit")]
4832 GitHubCommit {
4833 message: String,
4835 oid: String,
4837 repo: GitHubRepoPointer,
4839 url: String,
4841 },
4842 #[serde(rename = "github_release")]
4844 GitHubRelease {
4845 name: String,
4847 repo: GitHubRepoPointer,
4849 tag_name: String,
4851 url: String,
4853 },
4854 #[serde(rename = "github_actions_job")]
4856 GitHubActionsJob {
4857 #[serde(skip_serializing_if = "Option::is_none")]
4860 conclusion: Option<String>,
4861 job_id: i64,
4863 job_name: String,
4865 repo: GitHubRepoPointer,
4867 url: String,
4869 workflow_name: String,
4871 },
4872 #[serde(rename = "github_repository")]
4874 GitHubRepository {
4875 #[serde(skip_serializing_if = "Option::is_none")]
4877 description: Option<String>,
4878 #[serde(skip_serializing_if = "Option::is_none")]
4881 r#ref: Option<String>,
4882 repo: GitHubRepoPointer,
4884 url: String,
4886 },
4887 #[serde(rename = "github_file_diff")]
4889 GitHubFileDiff {
4890 #[serde(skip_serializing_if = "Option::is_none")]
4892 base: Option<GitHubFileDiffSide>,
4893 #[serde(skip_serializing_if = "Option::is_none")]
4895 head: Option<GitHubFileDiffSide>,
4896 url: String,
4898 },
4899 #[serde(rename = "github_tree_comparison")]
4901 GitHubTreeComparison {
4902 base: GitHubTreeComparisonSide,
4904 head: GitHubTreeComparisonSide,
4906 url: String,
4908 },
4909 #[serde(rename = "github_url")]
4911 GitHubUrl {
4912 url: String,
4914 },
4915 #[serde(rename = "github_file")]
4917 GitHubFile {
4918 path: String,
4920 r#ref: String,
4922 repo: GitHubRepoPointer,
4924 url: String,
4926 },
4927 #[serde(rename = "github_snippet")]
4929 GitHubSnippet {
4930 line_range: GitHubSnippetLineRange,
4932 path: String,
4934 r#ref: String,
4936 repo: GitHubRepoPointer,
4938 url: String,
4940 },
4941}
4942
4943impl Attachment {
4944 pub fn display_name(&self) -> Option<&str> {
4946 match self {
4947 Self::File { display_name, .. }
4948 | Self::Directory { display_name, .. }
4949 | Self::Selection { display_name, .. }
4950 | Self::Blob { display_name, .. } => display_name.as_deref(),
4951 Self::GitHubReference { .. }
4952 | Self::GitHubCommit { .. }
4953 | Self::GitHubRelease { .. }
4954 | Self::GitHubActionsJob { .. }
4955 | Self::GitHubRepository { .. }
4956 | Self::GitHubFileDiff { .. }
4957 | Self::GitHubTreeComparison { .. }
4958 | Self::GitHubUrl { .. }
4959 | Self::GitHubFile { .. }
4960 | Self::GitHubSnippet { .. } => None,
4961 }
4962 }
4963
4964 pub fn label(&self) -> Option<String> {
4966 if let Some(display_name) = self
4967 .display_name()
4968 .map(str::trim)
4969 .filter(|name| !name.is_empty())
4970 {
4971 return Some(display_name.to_string());
4972 }
4973
4974 match self {
4975 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4976 format!("#{}", number)
4977 } else {
4978 title.trim().to_string()
4979 }),
4980 _ => self.derived_display_name(),
4981 }
4982 }
4983
4984 pub fn ensure_display_name(&mut self) {
4986 if self
4987 .display_name()
4988 .map(str::trim)
4989 .is_some_and(|name| !name.is_empty())
4990 {
4991 return;
4992 }
4993
4994 let Some(derived_display_name) = self.derived_display_name() else {
4995 return;
4996 };
4997
4998 match self {
4999 Self::File { display_name, .. }
5000 | Self::Directory { display_name, .. }
5001 | Self::Selection { display_name, .. }
5002 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
5003 Self::GitHubReference { .. }
5004 | Self::GitHubCommit { .. }
5005 | Self::GitHubRelease { .. }
5006 | Self::GitHubActionsJob { .. }
5007 | Self::GitHubRepository { .. }
5008 | Self::GitHubFileDiff { .. }
5009 | Self::GitHubTreeComparison { .. }
5010 | Self::GitHubUrl { .. }
5011 | Self::GitHubFile { .. }
5012 | Self::GitHubSnippet { .. } => {}
5013 }
5014 }
5015
5016 fn derived_display_name(&self) -> Option<String> {
5017 match self {
5018 Self::File { path, .. } | Self::Directory { path, .. } => {
5019 Some(attachment_name_from_path(path))
5020 }
5021 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
5022 Self::Blob { .. } => Some("attachment".to_string()),
5023 Self::GitHubReference { .. }
5024 | Self::GitHubCommit { .. }
5025 | Self::GitHubRelease { .. }
5026 | Self::GitHubActionsJob { .. }
5027 | Self::GitHubRepository { .. }
5028 | Self::GitHubFileDiff { .. }
5029 | Self::GitHubTreeComparison { .. }
5030 | Self::GitHubUrl { .. }
5031 | Self::GitHubFile { .. }
5032 | Self::GitHubSnippet { .. } => None,
5033 }
5034 }
5035}
5036
5037fn attachment_name_from_path(path: &Path) -> String {
5038 path.file_name()
5039 .map(|name| name.to_string_lossy().into_owned())
5040 .filter(|name| !name.is_empty())
5041 .unwrap_or_else(|| {
5042 let full = path.to_string_lossy();
5043 if full.is_empty() {
5044 "attachment".to_string()
5045 } else {
5046 full.into_owned()
5047 }
5048 })
5049}
5050
5051pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
5053 for attachment in attachments {
5054 attachment.ensure_display_name();
5055 }
5056}
5057
5058#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5063#[serde(rename_all = "lowercase")]
5064#[non_exhaustive]
5065pub enum DeliveryMode {
5066 Enqueue,
5068 Immediate,
5070}
5071
5072#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5077#[serde(rename_all = "lowercase")]
5078#[non_exhaustive]
5079pub enum AgentMode {
5080 Interactive,
5082 Plan,
5084 Autopilot,
5086 Shell,
5088}
5089
5090#[derive(Debug, Clone)]
5119#[non_exhaustive]
5120pub struct MessageOptions {
5121 pub prompt: String,
5123 pub mode: Option<DeliveryMode>,
5129 pub agent_mode: Option<AgentMode>,
5133 pub attachments: Option<Vec<Attachment>>,
5135 pub wait_timeout: Option<Duration>,
5138 pub request_headers: Option<HashMap<String, String>>,
5142 pub traceparent: Option<String>,
5149 pub tracestate: Option<String>,
5153 pub display_prompt: Option<String>,
5155}
5156
5157impl MessageOptions {
5158 pub fn new(prompt: impl Into<String>) -> Self {
5160 Self {
5161 prompt: prompt.into(),
5162 mode: None,
5163 agent_mode: None,
5164 attachments: None,
5165 wait_timeout: None,
5166 request_headers: None,
5167 traceparent: None,
5168 tracestate: None,
5169 display_prompt: None,
5170 }
5171 }
5172
5173 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
5179 self.mode = Some(mode);
5180 self
5181 }
5182
5183 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
5187 self.agent_mode = Some(agent_mode);
5188 self
5189 }
5190
5191 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
5193 self.attachments = Some(attachments);
5194 self
5195 }
5196
5197 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
5199 self.wait_timeout = Some(timeout);
5200 self
5201 }
5202
5203 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
5205 self.request_headers = Some(headers);
5206 self
5207 }
5208
5209 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
5214 self.traceparent = ctx.traceparent;
5215 self.tracestate = ctx.tracestate;
5216 self
5217 }
5218
5219 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5221 self.traceparent = Some(traceparent.into());
5222 self
5223 }
5224
5225 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5227 self.tracestate = Some(tracestate.into());
5228 self
5229 }
5230
5231 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5233 self.display_prompt = Some(display_prompt.into());
5234 self
5235 }
5236}
5237
5238impl From<&str> for MessageOptions {
5239 fn from(prompt: &str) -> Self {
5240 Self::new(prompt)
5241 }
5242}
5243
5244impl From<String> for MessageOptions {
5245 fn from(prompt: String) -> Self {
5246 Self::new(prompt)
5247 }
5248}
5249
5250impl From<&String> for MessageOptions {
5251 fn from(prompt: &String) -> Self {
5252 Self::new(prompt.clone())
5253 }
5254}
5255
5256#[derive(Debug, Clone, Serialize, Deserialize)]
5258#[serde(rename_all = "camelCase")]
5259#[non_exhaustive]
5260pub struct GetStatusResponse {
5261 pub version: String,
5263 pub protocol_version: u32,
5265}
5266
5267#[derive(Debug, Clone, Serialize, Deserialize)]
5269#[serde(rename_all = "camelCase")]
5270#[non_exhaustive]
5271pub struct GetAuthStatusResponse {
5272 pub is_authenticated: bool,
5274 #[serde(skip_serializing_if = "Option::is_none")]
5277 pub auth_type: Option<String>,
5278 #[serde(skip_serializing_if = "Option::is_none")]
5280 pub host: Option<String>,
5281 #[serde(skip_serializing_if = "Option::is_none")]
5283 pub login: Option<String>,
5284 #[serde(skip_serializing_if = "Option::is_none")]
5286 pub status_message: Option<String>,
5287}
5288
5289#[derive(Debug, Clone, Serialize, Deserialize)]
5293#[serde(rename_all = "camelCase")]
5294pub struct SessionEventNotification {
5295 pub session_id: SessionId,
5297 pub event: SessionEvent,
5299}
5300
5301#[derive(Debug, Clone, Serialize, Deserialize)]
5308#[serde(rename_all = "camelCase")]
5309pub struct SessionEvent {
5310 pub id: String,
5312 pub timestamp: String,
5314 pub parent_id: Option<String>,
5316 #[serde(skip_serializing_if = "Option::is_none")]
5318 pub ephemeral: Option<bool>,
5319 #[serde(skip_serializing_if = "Option::is_none")]
5322 pub agent_id: Option<String>,
5323 #[serde(skip_serializing_if = "Option::is_none")]
5325 pub debug_cli_received_at_ms: Option<i64>,
5326 #[serde(skip_serializing_if = "Option::is_none")]
5328 pub debug_ws_forwarded_at_ms: Option<i64>,
5329 #[serde(rename = "type")]
5331 pub event_type: String,
5332 pub data: Value,
5334}
5335
5336impl SessionEvent {
5337 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5342 use serde::de::IntoDeserializer;
5343 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5344 self.event_type.as_str().into_deserializer();
5345 crate::generated::SessionEventType::deserialize(deserializer)
5346 .unwrap_or(crate::generated::SessionEventType::Unknown)
5347 }
5348
5349 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5355 serde_json::from_value(self.data.clone()).ok()
5356 }
5357
5358 pub fn is_transient_error(&self) -> bool {
5362 self.event_type == "session.error"
5363 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5364 }
5365}
5366
5367#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5372#[serde(rename_all = "camelCase")]
5373#[non_exhaustive]
5374pub struct ToolInvocation {
5375 pub session_id: SessionId,
5377 pub tool_call_id: String,
5379 pub tool_name: String,
5381 pub arguments: Value,
5383 #[serde(skip)]
5391 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5392 #[serde(default, skip_serializing_if = "Option::is_none")]
5397 pub traceparent: Option<String>,
5398 #[serde(default, skip_serializing_if = "Option::is_none")]
5401 pub tracestate: Option<String>,
5402}
5403
5404impl ToolInvocation {
5405 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5426 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5427 }
5428
5429 pub fn trace_context(&self) -> TraceContext {
5432 TraceContext {
5433 traceparent: self.traceparent.clone(),
5434 tracestate: self.tracestate.clone(),
5435 }
5436 }
5437}
5438
5439#[derive(Debug, Clone, Serialize, Deserialize)]
5441#[serde(rename_all = "camelCase")]
5442pub struct ToolBinaryResult {
5443 pub data: String,
5445 pub mime_type: String,
5447 pub r#type: String,
5449 #[serde(default, skip_serializing_if = "Option::is_none")]
5451 pub description: Option<String>,
5452}
5453
5454#[derive(Debug, Clone, Serialize, Deserialize)]
5461#[serde(rename_all = "camelCase")]
5462#[non_exhaustive]
5463pub struct ToolResultExpanded {
5464 pub text_result_for_llm: String,
5466 pub result_type: String,
5468 #[serde(default, skip_serializing_if = "Option::is_none")]
5470 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5471 #[serde(skip_serializing_if = "Option::is_none")]
5473 pub session_log: Option<String>,
5474 #[serde(skip_serializing_if = "Option::is_none")]
5476 pub error: Option<String>,
5477 #[serde(default, skip_serializing_if = "Option::is_none")]
5479 pub tool_telemetry: Option<HashMap<String, Value>>,
5480 #[serde(default, skip_serializing_if = "Option::is_none")]
5482 pub tool_references: Option<Vec<String>>,
5483}
5484
5485impl ToolResultExpanded {
5486 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5490 Self {
5491 text_result_for_llm: text_result_for_llm.into(),
5492 result_type: result_type.into(),
5493 binary_results_for_llm: None,
5494 session_log: None,
5495 error: None,
5496 tool_telemetry: None,
5497 tool_references: None,
5498 }
5499 }
5500
5501 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5503 self.binary_results_for_llm = Some(results);
5504 self
5505 }
5506
5507 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5509 self.session_log = Some(session_log.into());
5510 self
5511 }
5512
5513 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5515 self.error = Some(error.into());
5516 self
5517 }
5518
5519 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5521 self.tool_telemetry = Some(telemetry);
5522 self
5523 }
5524
5525 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5527 where
5528 I: IntoIterator<Item = S>,
5529 S: Into<String>,
5530 {
5531 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5532 self
5533 }
5534}
5535
5536#[derive(Debug, Clone, Serialize, Deserialize)]
5538#[serde(untagged)]
5539#[non_exhaustive]
5540pub enum ToolResult {
5541 Text(String),
5543 Expanded(ToolResultExpanded),
5545}
5546
5547#[derive(Debug, Clone, Serialize, Deserialize)]
5549#[serde(rename_all = "camelCase")]
5550pub struct ToolResultResponse {
5551 pub result: ToolResult,
5553}
5554
5555#[derive(Debug, Clone, Serialize, Deserialize)]
5557#[serde(rename_all = "camelCase")]
5558pub struct SessionMetadata {
5559 pub session_id: SessionId,
5561 pub start_time: String,
5563 pub modified_time: String,
5565 #[serde(skip_serializing_if = "Option::is_none")]
5567 pub summary: Option<String>,
5568 pub is_remote: bool,
5570}
5571
5572#[derive(Debug, Clone, Serialize, Deserialize)]
5574#[serde(rename_all = "camelCase")]
5575pub struct ListSessionsResponse {
5576 pub sessions: Vec<SessionMetadata>,
5578}
5579
5580#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5584#[serde(rename_all = "camelCase")]
5585pub struct SessionListFilter {
5586 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5588 pub working_directory: Option<String>,
5589 #[serde(default, skip_serializing_if = "Option::is_none")]
5591 pub git_root: Option<String>,
5592 #[serde(default, skip_serializing_if = "Option::is_none")]
5594 pub repository: Option<String>,
5595 #[serde(default, skip_serializing_if = "Option::is_none")]
5597 pub branch: Option<String>,
5598}
5599
5600#[derive(Debug, Clone, Serialize, Deserialize)]
5602#[serde(rename_all = "camelCase")]
5603pub struct GetSessionMetadataResponse {
5604 #[serde(skip_serializing_if = "Option::is_none")]
5606 pub session: Option<SessionMetadata>,
5607}
5608
5609#[derive(Debug, Clone, Serialize, Deserialize)]
5611#[serde(rename_all = "camelCase")]
5612pub struct GetLastSessionIdResponse {
5613 #[serde(skip_serializing_if = "Option::is_none")]
5615 pub session_id: Option<SessionId>,
5616}
5617
5618#[derive(Debug, Clone, Serialize, Deserialize)]
5620#[serde(rename_all = "camelCase")]
5621pub struct GetForegroundSessionResponse {
5622 #[serde(skip_serializing_if = "Option::is_none")]
5624 pub session_id: Option<SessionId>,
5625}
5626
5627#[derive(Debug, Clone, Serialize, Deserialize)]
5629#[serde(rename_all = "camelCase")]
5630pub struct GetMessagesResponse {
5631 pub events: Vec<SessionEvent>,
5633}
5634
5635#[derive(Debug, Clone, Serialize, Deserialize)]
5637#[serde(rename_all = "camelCase")]
5638pub struct ElicitationResult {
5639 pub action: String,
5641 #[serde(skip_serializing_if = "Option::is_none")]
5643 pub content: Option<Value>,
5644}
5645
5646#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5652#[serde(rename_all = "camelCase")]
5653#[non_exhaustive]
5654pub enum ElicitationMode {
5655 Form,
5657 Url,
5659 #[serde(other)]
5661 Unknown,
5662}
5663
5664#[derive(Debug, Clone, Serialize, Deserialize)]
5671#[serde(rename_all = "camelCase")]
5672pub struct ElicitationRequest {
5673 pub message: String,
5675 #[serde(skip_serializing_if = "Option::is_none")]
5677 pub requested_schema: Option<Value>,
5678 #[serde(skip_serializing_if = "Option::is_none")]
5680 pub mode: Option<ElicitationMode>,
5681 #[serde(skip_serializing_if = "Option::is_none")]
5683 pub elicitation_source: Option<String>,
5684 #[serde(skip_serializing_if = "Option::is_none")]
5686 pub url: Option<String>,
5687}
5688
5689#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5694#[serde(rename_all = "camelCase")]
5695pub struct SessionCapabilities {
5696 #[serde(skip_serializing_if = "Option::is_none")]
5698 pub ui: Option<UiCapabilities>,
5699}
5700
5701#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5703#[serde(rename_all = "camelCase")]
5704pub struct UiCapabilities {
5705 #[serde(skip_serializing_if = "Option::is_none")]
5707 pub elicitation: Option<bool>,
5708 #[serde(skip_serializing_if = "Option::is_none")]
5719 pub mcp_apps: Option<bool>,
5720 #[serde(skip_serializing_if = "Option::is_none")]
5722 pub canvases: Option<bool>,
5723}
5724
5725#[derive(Debug, Clone, Default)]
5727pub struct UiInputOptions<'a> {
5728 pub title: Option<&'a str>,
5730 pub description: Option<&'a str>,
5732 pub min_length: Option<u64>,
5734 pub max_length: Option<u64>,
5736 pub format: Option<InputFormat>,
5738 pub default: Option<&'a str>,
5740}
5741
5742#[derive(Debug, Clone, Copy)]
5744#[non_exhaustive]
5745pub enum InputFormat {
5746 Email,
5748 Uri,
5750 Date,
5752 DateTime,
5754}
5755
5756impl InputFormat {
5757 pub fn as_str(&self) -> &'static str {
5759 match self {
5760 Self::Email => "email",
5761 Self::Uri => "uri",
5762 Self::Date => "date",
5763 Self::DateTime => "date-time",
5764 }
5765 }
5766}
5767
5768pub use crate::generated::api_types::{
5773 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5774 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5775 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5776 PermissionDecisionApproveOnce, PermissionDecisionContext, PermissionDecisionOutcome,
5777 PermissionDecisionReject, PermissionDecisionSource, PermissionDecisionSurface,
5778 PermissionDecisionUserNotAvailable,
5779};
5780
5781#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5787#[serde(rename_all = "kebab-case")]
5788#[non_exhaustive]
5789pub enum PermissionRequestKind {
5790 Shell,
5792 Write,
5794 Read,
5796 Url,
5798 Mcp,
5800 CustomTool,
5802 Memory,
5804 Hook,
5806 #[serde(other)]
5809 Unknown,
5810}
5811
5812#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5818#[serde(rename_all = "camelCase")]
5819pub struct PermissionRequestData {
5820 #[serde(default, skip_serializing_if = "Option::is_none")]
5824 pub kind: Option<PermissionRequestKind>,
5825 #[serde(default, skip_serializing_if = "Option::is_none")]
5828 pub tool_call_id: Option<String>,
5829 #[serde(default, skip_serializing_if = "Option::is_none")]
5831 pub managed_approval_required: Option<bool>,
5832 #[serde(default, skip_serializing_if = "is_false")]
5834 pub managed_settings_enabled: bool,
5835 #[serde(flatten)]
5839 pub extra: Value,
5840}
5841
5842#[derive(Debug, Clone, Serialize, Deserialize)]
5844#[serde(rename_all = "camelCase")]
5845pub struct ExitPlanModeData {
5846 #[serde(default)]
5848 pub summary: String,
5849 #[serde(default, skip_serializing_if = "Option::is_none")]
5851 pub plan_content: Option<String>,
5852 #[serde(default)]
5854 pub actions: Vec<String>,
5855 #[serde(default = "default_recommended_action")]
5857 pub recommended_action: String,
5858}
5859
5860fn default_recommended_action() -> String {
5861 "autopilot".to_string()
5862}
5863
5864impl Default for ExitPlanModeData {
5865 fn default() -> Self {
5866 Self {
5867 summary: String::new(),
5868 plan_content: None,
5869 actions: Vec::new(),
5870 recommended_action: default_recommended_action(),
5871 }
5872 }
5873}
5874
5875#[cfg(test)]
5876mod tests {
5877 use std::collections::HashMap;
5878 use std::path::PathBuf;
5879
5880 use serde_json::json;
5881
5882 use super::{
5883 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5884 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5885 CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry,
5886 ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
5887 InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
5888 MemoryConfiguration, NamedProviderConfig, ProviderConfig, ProviderModelConfig,
5889 ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, SessionId,
5890 SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
5891 ToolResultResponse, ensure_attachment_display_names,
5892 };
5893 use crate::generated::session_events::TypedSessionEvent;
5894
5895 #[test]
5896 fn tool_builder_composes() {
5897 let tool = Tool::new("greet")
5898 .with_description("Say hello")
5899 .with_namespaced_name("hello/greet")
5900 .with_instructions("Pass the user's name")
5901 .with_parameters(json!({
5902 "type": "object",
5903 "properties": { "name": { "type": "string" } },
5904 "required": ["name"]
5905 }))
5906 .with_overrides_built_in_tool(true)
5907 .with_skip_permission(true);
5908 assert_eq!(tool.name, "greet");
5909 assert_eq!(tool.description, "Say hello");
5910 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5911 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5912 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5913 assert!(tool.overrides_built_in_tool);
5914 assert!(tool.skip_permission);
5915 }
5916
5917 #[test]
5918 fn tool_defer_serialization() {
5919 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5920 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5921 let value = serde_json::to_value(&tool).unwrap();
5922 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5923
5924 let plain = Tool::new("plain");
5925 let value = serde_json::to_value(&plain).unwrap();
5926 assert!(value.get("defer").is_none());
5927 }
5928
5929 #[test]
5930 fn tool_metadata_serialization() {
5931 use indexmap::IndexMap;
5932
5933 let mut metadata = IndexMap::new();
5934 metadata.insert(
5935 "github.com/copilot:safeForTelemetry".to_string(),
5936 json!({ "name": true, "inputsNames": false }),
5937 );
5938 let tool = Tool::new("lookup").with_metadata(metadata);
5939 let value = serde_json::to_value(&tool).unwrap();
5940 assert_eq!(
5941 value
5942 .get("metadata")
5943 .unwrap()
5944 .get("github.com/copilot:safeForTelemetry")
5945 .unwrap(),
5946 &json!({ "name": true, "inputsNames": false })
5947 );
5948
5949 let plain = Tool::new("plain");
5951 let value = serde_json::to_value(&plain).unwrap();
5952 assert!(value.get("metadata").is_none());
5953 }
5954
5955 #[test]
5956 fn custom_agent_config_builder_with_model() {
5957 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5958 .with_model("claude-haiku-4.5")
5959 .with_display_name("My Agent");
5960 assert_eq!(agent.name, "my-agent");
5961 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5962 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5963 }
5964
5965 #[test]
5966 fn custom_agent_config_serializes_model() {
5967 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5968 let wire = serde_json::to_value(&agent).unwrap();
5969 assert_eq!(wire["model"], "claude-haiku-4.5");
5970 assert_eq!(wire["name"], "model-agent");
5971 }
5972
5973 #[test]
5974 fn custom_agent_config_omits_model_when_none() {
5975 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5976 let wire = serde_json::to_value(&agent).unwrap();
5977 assert!(wire.get("model").is_none());
5978 }
5979
5980 #[test]
5981 fn custom_agent_config_builder_with_reasoning_effort() {
5982 let agent =
5983 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5984 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
5985 }
5986
5987 #[test]
5988 fn custom_agent_config_serializes_reasoning_effort() {
5989 let agent =
5990 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5991 let wire = serde_json::to_value(&agent).unwrap();
5992 assert_eq!(wire["reasoningEffort"], "high");
5993 }
5994
5995 #[test]
5996 fn custom_agent_config_omits_reasoning_effort_when_none() {
5997 let agent = CustomAgentConfig::new("default-agent", "prompt");
5998 let wire = serde_json::to_value(&agent).unwrap();
5999 assert!(wire.get("reasoningEffort").is_none());
6000 }
6001
6002 #[test]
6003 #[should_panic(expected = "tool parameter schema must be a JSON object")]
6004 fn tool_with_parameters_panics_on_non_object_value() {
6005 let _ = Tool::new("noop").with_parameters(json!(null));
6006 }
6007
6008 #[test]
6009 fn tool_result_expanded_serializes_binary_results_for_llm() {
6010 let response = ToolResultResponse {
6011 result: ToolResult::Expanded(ToolResultExpanded {
6012 text_result_for_llm: "rendered chart".to_string(),
6013 result_type: "success".to_string(),
6014 binary_results_for_llm: Some(vec![ToolBinaryResult {
6015 data: "aW1n".to_string(),
6016 mime_type: "image/png".to_string(),
6017 r#type: "image".to_string(),
6018 description: Some("chart preview".to_string()),
6019 }]),
6020 session_log: None,
6021 error: None,
6022 tool_telemetry: None,
6023 tool_references: None,
6024 }),
6025 };
6026
6027 let wire = serde_json::to_value(&response).unwrap();
6028
6029 assert_eq!(
6030 wire,
6031 json!({
6032 "result": {
6033 "textResultForLlm": "rendered chart",
6034 "resultType": "success",
6035 "binaryResultsForLlm": [
6036 {
6037 "data": "aW1n",
6038 "mimeType": "image/png",
6039 "type": "image",
6040 "description": "chart preview"
6041 }
6042 ]
6043 }
6044 })
6045 );
6046 }
6047
6048 #[test]
6049 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
6050 let response = ToolResultResponse {
6051 result: ToolResult::Expanded(ToolResultExpanded {
6052 text_result_for_llm: "ok".to_string(),
6053 result_type: "success".to_string(),
6054 binary_results_for_llm: None,
6055 session_log: None,
6056 error: None,
6057 tool_telemetry: None,
6058 tool_references: None,
6059 }),
6060 };
6061
6062 let wire = serde_json::to_value(&response).unwrap();
6063
6064 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6065 assert!(wire["result"].get("binaryResultsForLlm").is_none());
6066 }
6067
6068 #[test]
6069 fn tool_result_expanded_serializes_tool_references() {
6070 let response = ToolResultResponse {
6071 result: ToolResult::Expanded(
6072 ToolResultExpanded::new("found 2 tools", "success")
6073 .with_tool_references(["get_weather", "check_status"]),
6074 ),
6075 };
6076
6077 let wire = serde_json::to_value(&response).unwrap();
6078
6079 assert_eq!(
6080 wire,
6081 json!({
6082 "result": {
6083 "textResultForLlm": "found 2 tools",
6084 "resultType": "success",
6085 "toolReferences": ["get_weather", "check_status"]
6086 }
6087 })
6088 );
6089 }
6090
6091 #[test]
6092 fn tool_result_expanded_omits_tool_references_when_none() {
6093 let response = ToolResultResponse {
6094 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
6095 };
6096
6097 let wire = serde_json::to_value(&response).unwrap();
6098
6099 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6100 assert!(wire["result"].get("toolReferences").is_none());
6101 }
6102
6103 #[test]
6104 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
6105 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
6108 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
6109
6110 assert_eq!(
6111 expanded.tool_references.as_deref(),
6112 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6113 );
6114 }
6115
6116 #[test]
6117 fn tool_result_expanded_deserializes_tool_references() {
6118 let wire = json!({
6119 "textResultForLlm": "found tools",
6120 "resultType": "success",
6121 "toolReferences": ["alpha", "beta"]
6122 });
6123
6124 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
6125
6126 assert_eq!(
6127 expanded.tool_references.as_deref(),
6128 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6129 );
6130 }
6131
6132 #[test]
6133 fn session_config_default_wire_flags_off_without_handlers() {
6134 let cfg = SessionConfig::default();
6135 assert_eq!(cfg.mcp_oauth_token_storage, None);
6136 let (wire, _runtime) = cfg
6140 .into_wire(Some(SessionId::from("default-flags")))
6141 .expect("default config has no duplicate handlers");
6142 assert!(!wire.request_user_input);
6143 assert!(!wire.request_permission);
6144 assert!(!wire.request_elicitation);
6145 assert!(!wire.request_exit_plan_mode);
6146 assert!(!wire.request_auto_mode_switch);
6147 assert!(!wire.hooks);
6148 assert!(!wire.request_mcp_apps);
6149 }
6150
6151 #[test]
6152 fn resume_session_config_new_wire_flags_off_without_handlers() {
6153 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
6154 assert_eq!(cfg.mcp_oauth_token_storage, None);
6155 let (wire, _runtime) = cfg
6156 .into_wire()
6157 .expect("default resume config has no duplicate handlers");
6158 assert!(!wire.request_user_input);
6159 assert!(!wire.request_permission);
6160 assert!(!wire.request_elicitation);
6161 assert!(!wire.request_exit_plan_mode);
6162 assert!(!wire.request_auto_mode_switch);
6163 assert!(!wire.hooks);
6164 assert!(!wire.request_mcp_apps);
6165 }
6166
6167 #[test]
6168 fn custom_agents_local_only_serializes_on_create_and_resume() {
6169 let (create_wire, _) = SessionConfig::default()
6170 .with_custom_agents_local_only(false)
6171 .into_wire(Some(SessionId::from("create-locality")))
6172 .expect("create config has no duplicate handlers");
6173 let create_json = serde_json::to_value(&create_wire).unwrap();
6174 assert_eq!(create_json["customAgentsLocalOnly"], false);
6175
6176 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
6177 .with_custom_agents_local_only(false)
6178 .into_wire()
6179 .expect("resume config has no duplicate handlers");
6180 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6181 assert_eq!(resume_json["customAgentsLocalOnly"], false);
6182
6183 let (unset_create_wire, _) = SessionConfig::default()
6184 .into_wire(Some(SessionId::from("create-unset")))
6185 .expect("create config has no duplicate handlers");
6186 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6187 assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
6188
6189 let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
6190 .into_wire()
6191 .expect("resume config has no duplicate handlers");
6192 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6193 assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
6194 }
6195
6196 #[test]
6197 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6198 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
6199 assert_eq!(cfg.enable_mcp_apps, Some(true));
6200
6201 let (wire, _runtime) = cfg
6202 .into_wire(Some(SessionId::from("enable-mcp-apps")))
6203 .expect("enable_mcp_apps config has no duplicate handlers");
6204 assert!(wire.request_mcp_apps);
6205
6206 let json = serde_json::to_value(&wire).unwrap();
6207 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6208 }
6209
6210 #[test]
6211 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6212 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
6213 .with_enable_mcp_apps(true);
6214 assert_eq!(cfg.enable_mcp_apps, Some(true));
6215
6216 let (wire, _runtime) = cfg
6217 .into_wire()
6218 .expect("resume enable_mcp_apps config has no duplicate handlers");
6219 assert!(wire.request_mcp_apps);
6220
6221 let json = serde_json::to_value(&wire).unwrap();
6222 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6223 }
6224
6225 #[test]
6226 fn github_mcp_tool_config_serializes_for_create_and_resume() {
6227 let github_config = GitHubMcpToolConfig::new()
6228 .with_enable_all_tools(true)
6229 .with_additional_toolsets(["repos"])
6230 .with_additional_tools(["get_issue"])
6231 .with_enable_insiders_mode(true)
6232 .with_disable_form_deferral(true);
6233
6234 let (create_wire, _) = SessionConfig::default()
6235 .with_github_mcp_tool_config(github_config.clone())
6236 .into_wire(Some(SessionId::from("github-mcp")))
6237 .expect("create config has no duplicate handlers");
6238 assert_eq!(
6239 serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6240 serde_json::json!({
6241 "enableAllTools": true,
6242 "additionalToolsets": ["repos"],
6243 "additionalTools": ["get_issue"],
6244 "enableInsidersMode": true,
6245 "disableFormDeferral": true,
6246 })
6247 );
6248
6249 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6250 .with_github_mcp_tool_config(github_config)
6251 .into_wire()
6252 .expect("resume config has no duplicate handlers");
6253 assert!(resume_wire.github_mcp_tool_config.is_some());
6254
6255 let (unset_wire, _) = SessionConfig::default()
6256 .into_wire(Some(SessionId::from("github-mcp-unset")))
6257 .expect("default config has no duplicate handlers");
6258 assert!(
6259 serde_json::to_value(&unset_wire)
6260 .unwrap()
6261 .get("githubMcpToolConfig")
6262 .is_none()
6263 );
6264 }
6265
6266 #[test]
6267 fn memory_configuration_constructors_and_serde() {
6268 assert!(MemoryConfiguration::enabled().enabled);
6269 assert!(!MemoryConfiguration::disabled().enabled);
6270 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6271
6272 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6273 assert_eq!(json, serde_json::json!({ "enabled": true }));
6274 }
6275
6276 #[test]
6277 fn session_config_with_memory_serializes() {
6278 let (wire, _runtime) = SessionConfig::default()
6279 .with_memory(MemoryConfiguration::enabled())
6280 .into_wire(Some(SessionId::from("memory-on")))
6281 .expect("no duplicate handlers");
6282 let json = serde_json::to_value(&wire).unwrap();
6283 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6284
6285 let (wire_off, _) = SessionConfig::default()
6286 .with_memory(MemoryConfiguration::disabled())
6287 .into_wire(Some(SessionId::from("memory-off")))
6288 .expect("no duplicate handlers");
6289 let json_off = serde_json::to_value(&wire_off).unwrap();
6290 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6291
6292 let (empty_wire, _) = SessionConfig::default()
6294 .into_wire(Some(SessionId::from("memory-unset")))
6295 .expect("no duplicate handlers");
6296 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6297 assert!(empty_json.get("memory").is_none());
6298 }
6299
6300 #[test]
6301 fn resume_session_config_with_memory_serializes() {
6302 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6303 .with_memory(MemoryConfiguration::enabled())
6304 .into_wire()
6305 .expect("no duplicate handlers");
6306 let json = serde_json::to_value(&wire).unwrap();
6307 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6308
6309 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6311 .into_wire()
6312 .expect("no duplicate handlers");
6313 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6314 assert!(empty_json.get("memory").is_none());
6315 }
6316
6317 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6318 CopilotExpAssignmentResponse {
6319 features: vec!["copilot_exp_flag".to_string()],
6320 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6321 configs: vec![ExpConfigEntry {
6322 id: "cfg-1".to_string(),
6323 parameters: HashMap::from([
6324 ("threshold".to_string(), ExpFlagValue::Integer(5)),
6325 ("enabled".to_string(), ExpFlagValue::Bool(true)),
6326 ]),
6327 }],
6328 assignment_context: context.to_string(),
6329 ..Default::default()
6330 }
6331 }
6332
6333 #[test]
6334 fn exp_flag_value_round_trips_all_variants() {
6335 let values = serde_json::json!({
6336 "s": "text",
6337 "i": 7,
6338 "f": 1.5,
6339 "b": true,
6340 "n": null,
6341 });
6342 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6343 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6344 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6345 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6346 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6347 assert_eq!(parsed["n"], ExpFlagValue::Null);
6348 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6349 }
6350
6351 #[test]
6352 fn session_config_with_exp_assignments_serializes() {
6353 let assignments = sample_exp_assignments("ctx-123");
6354 let expected = serde_json::to_value(&assignments).unwrap();
6355 let (wire, _runtime) = SessionConfig::default()
6356 .with_exp_assignments(assignments)
6357 .into_wire(Some(SessionId::from("exp-on")))
6358 .expect("no duplicate handlers");
6359 let json = serde_json::to_value(&wire).unwrap();
6360 assert_eq!(json["expAssignments"], expected);
6361 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6362 assert_eq!(
6363 json["expAssignments"]["Flights"]["copilot_exp_flag"],
6364 "treatment"
6365 );
6366
6367 let (empty_wire, _) = SessionConfig::default()
6369 .into_wire(Some(SessionId::from("exp-unset")))
6370 .expect("no duplicate handlers");
6371 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6372 assert!(empty_json.get("expAssignments").is_none());
6373 }
6374
6375 #[test]
6376 fn resume_session_config_with_exp_assignments_serializes() {
6377 let assignments = sample_exp_assignments("ctx-456");
6378 let expected = serde_json::to_value(&assignments).unwrap();
6379 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6380 .with_exp_assignments(assignments)
6381 .into_wire()
6382 .expect("no duplicate handlers");
6383 let json = serde_json::to_value(&wire).unwrap();
6384 assert_eq!(json["expAssignments"], expected);
6385
6386 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6388 .into_wire()
6389 .expect("no duplicate handlers");
6390 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6391 assert!(empty_json.get("expAssignments").is_none());
6392 }
6393
6394 #[test]
6395 fn session_config_clone_preserves_exp_assignments() {
6396 let assignments = sample_exp_assignments("ctx-clone");
6397 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6398 let cloned = config.clone();
6399
6400 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6401
6402 let (wire, _runtime) = cloned
6403 .into_wire(Some(SessionId::from("exp-clone")))
6404 .expect("no duplicate handlers");
6405 let json = serde_json::to_value(&wire).unwrap();
6406 assert_eq!(
6407 json["expAssignments"],
6408 serde_json::to_value(&assignments).unwrap()
6409 );
6410 }
6411
6412 #[test]
6413 fn resume_session_config_clone_preserves_exp_assignments() {
6414 let assignments = sample_exp_assignments("ctx-clone-resume");
6415 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6416 .with_exp_assignments(assignments.clone());
6417 let cloned = config.clone();
6418
6419 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6420
6421 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6422 let json = serde_json::to_value(&wire).unwrap();
6423 assert_eq!(
6424 json["expAssignments"],
6425 serde_json::to_value(&assignments).unwrap()
6426 );
6427 }
6428
6429 #[test]
6430 #[allow(clippy::field_reassign_with_default)]
6431 fn session_config_into_wire_serializes_bucket_b_fields() {
6432 use std::path::PathBuf;
6433
6434 use super::{CloudSessionOptions, CloudSessionRepository};
6435
6436 let mut cfg = SessionConfig::default();
6437 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6438 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6439 cfg.github_token = Some("ghs_secret".to_string());
6440 cfg.include_sub_agent_streaming_events = Some(false);
6441 cfg.enable_session_telemetry = Some(false);
6442 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
6443 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
6444 cfg.enable_on_demand_instruction_discovery = Some(false);
6445 cfg.cloud = Some(CloudSessionOptions::with_repository(
6446 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
6447 ));
6448
6449 let (wire, _runtime) = cfg
6450 .into_wire(Some(SessionId::from("custom-id")))
6451 .expect("no duplicate handlers");
6452 let wire_json = serde_json::to_value(&wire).unwrap();
6453 assert_eq!(wire_json["sessionId"], "custom-id");
6454 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6455 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6456 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6457 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6458 assert_eq!(wire_json["enableSessionTelemetry"], false);
6459 assert_eq!(wire_json["reasoningSummary"], "concise");
6460 assert_eq!(wire_json["remoteSession"], "export");
6461 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6462 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6463 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6464 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6465
6466 let (empty_wire, _) = SessionConfig::default()
6468 .into_wire(Some(SessionId::from("empty")))
6469 .expect("default has no duplicate handlers");
6470 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6471 assert!(empty_json.get("gitHubToken").is_none());
6472 assert!(empty_json.get("enableSessionTelemetry").is_none());
6473 assert!(empty_json.get("reasoningSummary").is_none());
6474 assert!(empty_json.get("remoteSession").is_none());
6475 assert!(
6476 empty_json
6477 .get("enableOnDemandInstructionDiscovery")
6478 .is_none()
6479 );
6480 assert!(empty_json.get("cloud").is_none());
6481 }
6482
6483 #[test]
6484 fn session_config_into_wire_serializes_named_providers_and_models() {
6485 let cfg = SessionConfig::default()
6486 .with_providers(vec![
6487 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6488 .with_provider_type("openai")
6489 .with_wire_api("responses")
6490 .with_api_key("sk-test"),
6491 ])
6492 .with_models(vec![
6493 ProviderModelConfig::new("gpt-x", "my-openai")
6494 .with_wire_model("gpt-x-2025")
6495 .with_max_output_tokens(2048),
6496 ]);
6497
6498 let (wire, _) = cfg
6499 .into_wire(Some(SessionId::from("sess-providers")))
6500 .expect("no duplicate handlers");
6501 let wire_json = serde_json::to_value(&wire).unwrap();
6502 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6503 assert_eq!(
6504 wire_json["providers"][0]["baseUrl"],
6505 "https://api.example.com/v1"
6506 );
6507 assert_eq!(wire_json["providers"][0]["type"], "openai");
6508 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6509 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6510 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
6511 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
6512 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
6513 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
6514
6515 let (empty_wire, _) = SessionConfig::default()
6516 .into_wire(Some(SessionId::from("empty")))
6517 .expect("default has no duplicate handlers");
6518 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6519 assert!(empty_json.get("providers").is_none());
6520 assert!(empty_json.get("models").is_none());
6521 }
6522
6523 #[test]
6524 fn resume_config_into_wire_serializes_named_providers_and_models() {
6525 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
6526 .with_providers(vec![
6527 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
6528 .with_provider_type("azure")
6529 .with_azure(AzureProviderOptions {
6530 api_version: Some("2024-10-21".to_string()),
6531 }),
6532 ])
6533 .with_models(vec![
6534 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
6535 ]);
6536
6537 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6538 let wire_json = serde_json::to_value(&wire).unwrap();
6539 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
6540 assert_eq!(wire_json["providers"][0]["type"], "azure");
6541 assert_eq!(
6542 wire_json["providers"][0]["azure"]["apiVersion"],
6543 "2024-10-21"
6544 );
6545 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
6546 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
6547 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
6548
6549 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
6550 .into_wire()
6551 .expect("default has no duplicate handlers");
6552 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6553 assert!(empty_json.get("providers").is_none());
6554 assert!(empty_json.get("models").is_none());
6555 }
6556
6557 #[test]
6558 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
6559 use std::path::PathBuf;
6560
6561 let cfg = SessionConfig {
6562 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
6563 disabled_mcp_servers: Some(vec![
6564 "local-files".to_string(),
6565 "remote-github".to_string(),
6566 ]),
6567 large_output: Some(
6568 LargeToolOutputConfig::new()
6569 .with_enabled(true)
6570 .with_max_size_bytes(1024)
6571 .with_output_directory(PathBuf::from("/tmp/large-output")),
6572 ),
6573 ..Default::default()
6574 };
6575
6576 let (wire, _) = cfg
6577 .into_wire(Some(SessionId::from("sess-1")))
6578 .expect("no duplicate handlers");
6579 let wire_json = serde_json::to_value(&wire).unwrap();
6580 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
6581 assert_eq!(
6582 wire_json["disabledMcpServers"],
6583 serde_json::json!(["local-files", "remote-github"])
6584 );
6585 assert_eq!(wire_json["largeOutput"]["enabled"], true);
6586 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
6587 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
6588
6589 let (empty_wire, _) = SessionConfig::default()
6590 .into_wire(Some(SessionId::from("empty")))
6591 .expect("default has no duplicate handlers");
6592 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6593 assert!(empty_json.get("pluginDirectories").is_none());
6594 assert!(empty_json.get("disabledMcpServers").is_none());
6595 assert!(empty_json.get("largeOutput").is_none());
6596 }
6597
6598 #[test]
6599 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
6600 use std::path::PathBuf;
6601
6602 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6603 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6604 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6605 cfg.github_token = Some("ghs_secret".to_string());
6606 cfg.include_sub_agent_streaming_events = Some(true);
6607 cfg.enable_session_telemetry = Some(false);
6608 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
6609 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
6610 cfg.enable_on_demand_instruction_discovery = Some(false);
6611
6612 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6613 let wire_json = serde_json::to_value(&wire).unwrap();
6614 assert_eq!(wire_json["sessionId"], "sess-1");
6615 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6616 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6617 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6618 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
6619 assert_eq!(wire_json["enableSessionTelemetry"], false);
6620 assert_eq!(wire_json["reasoningSummary"], "detailed");
6621 assert_eq!(wire_json["remoteSession"], "on");
6622 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6623
6624 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6626 .into_wire()
6627 .expect("default resume has no duplicate handlers");
6628 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6629 assert!(empty_json.get("reasoningSummary").is_none());
6630 assert!(empty_json.get("remoteSession").is_none());
6631 assert!(
6632 empty_json
6633 .get("enableOnDemandInstructionDiscovery")
6634 .is_none()
6635 );
6636 }
6637
6638 #[test]
6639 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
6640 use std::path::PathBuf;
6641
6642 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6643 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6644 cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]);
6645 cfg.large_output = Some(
6646 LargeToolOutputConfig::new()
6647 .with_enabled(false)
6648 .with_max_size_bytes(2048)
6649 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6650 );
6651
6652 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6653 let wire_json = serde_json::to_value(&wire).unwrap();
6654 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6655 assert_eq!(
6656 wire_json["disabledMcpServers"],
6657 serde_json::json!(["local-files-r"])
6658 );
6659 assert_eq!(wire_json["largeOutput"]["enabled"], false);
6660 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6661 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6662
6663 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6664 .into_wire()
6665 .expect("default resume has no duplicate handlers");
6666 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6667 assert!(empty_json.get("pluginDirectories").is_none());
6668 assert!(empty_json.get("disabledMcpServers").is_none());
6669 assert!(empty_json.get("largeOutput").is_none());
6670 }
6671
6672 #[test]
6673 fn session_config_clones_disabled_mcp_servers() {
6674 let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]);
6675 let mut create_clone = create.clone();
6676 create_clone
6677 .disabled_mcp_servers
6678 .as_mut()
6679 .expect("configured disabled MCP servers")
6680 .push("remote-github".to_string());
6681 assert_eq!(
6682 create.disabled_mcp_servers.as_deref(),
6683 Some(&["local-files".to_string()][..])
6684 );
6685
6686 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6687 .with_disabled_mcp_servers(["local-files"]);
6688 let mut resume_clone = resume.clone();
6689 resume_clone
6690 .disabled_mcp_servers
6691 .as_mut()
6692 .expect("configured disabled MCP servers")
6693 .push("remote-github".to_string());
6694 assert_eq!(
6695 resume.disabled_mcp_servers.as_deref(),
6696 Some(&["local-files".to_string()][..])
6697 );
6698 }
6699
6700 #[test]
6701 fn session_config_builder_composes() {
6702 use indexmap::IndexMap;
6703
6704 let cfg = SessionConfig::default()
6705 .with_session_id(SessionId::from("sess-1"))
6706 .with_model("claude-sonnet-4")
6707 .with_client_name("test-app")
6708 .with_reasoning_effort("medium")
6709 .with_reasoning_summary(ReasoningSummary::Concise)
6710 .with_context_tier("long_context")
6711 .with_streaming(true)
6712 .with_tools([Tool::new("greet")])
6713 .with_available_tools(["bash", "view"])
6714 .with_excluded_tools(["dangerous"])
6715 .with_mcp_servers(IndexMap::new())
6716 .with_mcp_oauth_token_storage("persistent")
6717 .with_enable_config_discovery(true)
6718 .with_enable_on_demand_instruction_discovery(true)
6719 .with_skill_directories([PathBuf::from("/tmp/skills")])
6720 .with_disabled_skills(["broken-skill"])
6721 .with_disabled_mcp_servers(["local-files"])
6722 .with_agent("researcher")
6723 .with_config_directory(PathBuf::from("/tmp/config"))
6724 .with_working_directory(PathBuf::from("/tmp/work"))
6725 .with_additional_directories([PathBuf::from("/tmp/shared")])
6726 .with_github_token("ghp_test")
6727 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6728 .with_enable_session_telemetry(false)
6729 .with_include_sub_agent_streaming_events(false)
6730 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6731
6732 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
6733 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
6734 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6735 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
6736 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
6737 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
6738 assert_eq!(cfg.streaming, Some(true));
6739 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6740 assert_eq!(
6741 cfg.available_tools.as_deref(),
6742 Some(&["bash".to_string(), "view".to_string()][..])
6743 );
6744 assert_eq!(
6745 cfg.excluded_tools.as_deref(),
6746 Some(&["dangerous".to_string()][..])
6747 );
6748 assert!(cfg.mcp_servers.is_some());
6749 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6750 assert_eq!(cfg.enable_config_discovery, Some(true));
6751 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
6752 assert_eq!(
6753 cfg.skill_directories.as_deref(),
6754 Some(&[PathBuf::from("/tmp/skills")][..])
6755 );
6756 assert_eq!(
6757 cfg.disabled_skills.as_deref(),
6758 Some(&["broken-skill".to_string()][..])
6759 );
6760 assert_eq!(
6761 cfg.disabled_mcp_servers.as_deref(),
6762 Some(&["local-files".to_string()][..])
6763 );
6764 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6765 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6766 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6767 assert_eq!(
6768 cfg.additional_directories.as_deref(),
6769 Some(&[PathBuf::from("/tmp/shared")][..])
6770 );
6771 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6772 assert_eq!(
6773 cfg.capi,
6774 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6775 );
6776 assert_eq!(cfg.enable_session_telemetry, Some(false));
6777 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
6778 assert_eq!(
6779 cfg.extension_info,
6780 Some(ExtensionInfo::new("github-app", "counter"))
6781 );
6782 }
6783
6784 #[test]
6785 fn resume_session_config_builder_composes() {
6786 use indexmap::IndexMap;
6787
6788 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
6789 .with_client_name("test-app")
6790 .with_reasoning_summary(ReasoningSummary::None)
6791 .with_context_tier("default")
6792 .with_streaming(true)
6793 .with_tools([Tool::new("greet")])
6794 .with_available_tools(["bash", "view"])
6795 .with_excluded_tools(["dangerous"])
6796 .with_mcp_servers(IndexMap::new())
6797 .with_mcp_oauth_token_storage("persistent")
6798 .with_enable_config_discovery(true)
6799 .with_enable_on_demand_instruction_discovery(false)
6800 .with_skill_directories([PathBuf::from("/tmp/skills")])
6801 .with_disabled_skills(["broken-skill"])
6802 .with_disabled_mcp_servers(["local-files"])
6803 .with_agent("researcher")
6804 .with_config_directory(PathBuf::from("/tmp/config"))
6805 .with_working_directory(PathBuf::from("/tmp/work"))
6806 .with_additional_directories([PathBuf::from("/tmp/shared")])
6807 .with_github_token("ghp_test")
6808 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6809 .with_enable_session_telemetry(false)
6810 .with_include_sub_agent_streaming_events(true)
6811 .with_suppress_resume_event(true)
6812 .with_continue_pending_work(true)
6813 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6814
6815 assert_eq!(cfg.session_id.as_str(), "sess-2");
6816 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6817 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
6818 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
6819 assert_eq!(cfg.streaming, Some(true));
6820 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6821 assert_eq!(
6822 cfg.available_tools.as_deref(),
6823 Some(&["bash".to_string(), "view".to_string()][..])
6824 );
6825 assert_eq!(
6826 cfg.excluded_tools.as_deref(),
6827 Some(&["dangerous".to_string()][..])
6828 );
6829 assert!(cfg.mcp_servers.is_some());
6830 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6831 assert_eq!(cfg.enable_config_discovery, Some(true));
6832 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
6833 assert_eq!(
6834 cfg.skill_directories.as_deref(),
6835 Some(&[PathBuf::from("/tmp/skills")][..])
6836 );
6837 assert_eq!(
6838 cfg.disabled_skills.as_deref(),
6839 Some(&["broken-skill".to_string()][..])
6840 );
6841 assert_eq!(
6842 cfg.disabled_mcp_servers.as_deref(),
6843 Some(&["local-files".to_string()][..])
6844 );
6845 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6846 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6847 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6848 assert_eq!(
6849 cfg.additional_directories.as_deref(),
6850 Some(&[PathBuf::from("/tmp/shared")][..])
6851 );
6852 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6853 assert_eq!(
6854 cfg.capi,
6855 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6856 );
6857 assert_eq!(cfg.enable_session_telemetry, Some(false));
6858 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
6859 assert_eq!(cfg.suppress_resume_event, Some(true));
6860 assert_eq!(cfg.continue_pending_work, Some(true));
6861 assert_eq!(
6862 cfg.extension_info,
6863 Some(ExtensionInfo::new("github-app", "counter"))
6864 );
6865 }
6866
6867 #[test]
6871 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
6872 let cfg =
6873 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
6874 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6875 let json = serde_json::to_value(&wire).unwrap();
6876 assert_eq!(json["continuePendingWork"], true);
6877
6878 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6880 .into_wire()
6881 .expect("no duplicate handlers");
6882 let json = serde_json::to_value(&wire).unwrap();
6883 assert!(json.get("continuePendingWork").is_none());
6884 }
6885
6886 #[test]
6887 fn session_configs_serialize_additional_directories() {
6888 let create = SessionConfig::default().with_additional_directories([
6889 PathBuf::from("/tmp/shared"),
6890 PathBuf::from("/tmp/generated"),
6891 ]);
6892 let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
6893 let create_json = serde_json::to_value(&create_wire).unwrap();
6894 assert_eq!(
6895 create_json["additionalDirectories"],
6896 serde_json::json!(["/tmp/shared", "/tmp/generated"])
6897 );
6898
6899 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6900 .with_additional_directories([PathBuf::from("/tmp/resumed")]);
6901 let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
6902 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6903 assert_eq!(
6904 resume_json["additionalDirectories"],
6905 serde_json::json!(["/tmp/resumed"])
6906 );
6907 }
6908
6909 #[test]
6913 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
6914 let cfg =
6915 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
6916 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6917 let json = serde_json::to_value(&wire).unwrap();
6918 assert_eq!(json["disableResume"], true);
6919 assert!(json.get("suppressResumeEvent").is_none());
6920 }
6921
6922 #[test]
6925 fn session_config_serializes_instruction_directories_to_camel_case() {
6926 let cfg =
6927 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
6928 let (wire, _) = cfg
6929 .into_wire(Some(SessionId::from("instr-on")))
6930 .expect("no duplicate handlers");
6931 let json = serde_json::to_value(&wire).unwrap();
6932 assert_eq!(
6933 json["instructionDirectories"],
6934 serde_json::json!(["/tmp/instr"])
6935 );
6936
6937 let (wire, _) = SessionConfig::default()
6939 .into_wire(Some(SessionId::from("instr-off")))
6940 .expect("no duplicate handlers");
6941 let json = serde_json::to_value(&wire).unwrap();
6942 assert!(json.get("instructionDirectories").is_none());
6943 }
6944
6945 #[test]
6948 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
6949 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
6950 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
6951 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6952 let json = serde_json::to_value(&wire).unwrap();
6953 assert_eq!(
6954 json["instructionDirectories"],
6955 serde_json::json!(["/tmp/instr"])
6956 );
6957
6958 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6959 .into_wire()
6960 .expect("no duplicate handlers");
6961 let json = serde_json::to_value(&wire).unwrap();
6962 assert!(json.get("instructionDirectories").is_none());
6963 }
6964
6965 #[test]
6966 fn custom_agent_config_builder_composes() {
6967 use indexmap::IndexMap;
6968
6969 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
6970 .with_display_name("Research Assistant")
6971 .with_description("Investigates technical questions.")
6972 .with_tools(["bash", "view"])
6973 .with_mcp_servers(IndexMap::new())
6974 .with_infer(true)
6975 .with_skills(["rust-coding-skill"]);
6976
6977 assert_eq!(cfg.name, "researcher");
6978 assert_eq!(cfg.prompt, "You are a research assistant.");
6979 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
6980 assert_eq!(
6981 cfg.description.as_deref(),
6982 Some("Investigates technical questions.")
6983 );
6984 assert_eq!(
6985 cfg.tools.as_deref(),
6986 Some(&["bash".to_string(), "view".to_string()][..])
6987 );
6988 assert!(cfg.mcp_servers.is_some());
6989 assert_eq!(cfg.infer, Some(true));
6990 assert_eq!(
6991 cfg.skills.as_deref(),
6992 Some(&["rust-coding-skill".to_string()][..])
6993 );
6994 }
6995
6996 #[test]
6997 fn mcp_servers_serialize_in_insertion_order() {
6998 use indexmap::IndexMap;
6999
7000 let order = [
7006 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
7007 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
7008 ];
7009 let mut servers = IndexMap::new();
7010 for name in order {
7011 servers.insert(
7012 name.to_string(),
7013 McpServerConfig::Stdio(McpStdioServerConfig {
7014 command: "run".to_string(),
7015 ..Default::default()
7016 }),
7017 );
7018 }
7019
7020 let (wire, _runtime) = SessionConfig::default()
7021 .with_mcp_servers(servers)
7022 .into_wire(None)
7023 .expect("into_wire should succeed");
7024 let json = serde_json::to_string(&wire).expect("serialize wire");
7025
7026 let positions: Vec<usize> = order
7027 .iter()
7028 .map(|name| {
7029 json.find(&format!("\"{name}\""))
7030 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
7031 })
7032 .collect();
7033 let mut ascending = positions.clone();
7034 ascending.sort_unstable();
7035 assert_eq!(
7036 positions, ascending,
7037 "mcp server keys must serialize in insertion order: {json}"
7038 );
7039 }
7040
7041 #[test]
7042 fn infinite_session_config_builder_composes() {
7043 let cfg = InfiniteSessionConfig::new()
7044 .with_enabled(true)
7045 .with_background_compaction_threshold(0.75)
7046 .with_buffer_exhaustion_threshold(0.92);
7047
7048 assert_eq!(cfg.enabled, Some(true));
7049 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
7050 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
7051 }
7052
7053 #[test]
7054 fn provider_config_builder_composes() {
7055 use std::collections::HashMap;
7056
7057 let mut headers = HashMap::new();
7058 headers.insert("X-Custom".to_string(), "value".to_string());
7059
7060 let cfg = ProviderConfig::new("https://api.example.com")
7061 .with_provider_type("openai")
7062 .with_wire_api("completions")
7063 .with_transport("websockets")
7064 .with_api_key("sk-test")
7065 .with_bearer_token("bearer-test")
7066 .with_headers(headers)
7067 .with_model_id("gpt-4")
7068 .with_wire_model("azure-gpt-4-deployment")
7069 .with_max_prompt_tokens(8192)
7070 .with_max_output_tokens(2048);
7071
7072 assert_eq!(cfg.base_url, "https://api.example.com");
7073 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
7074 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
7075 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
7076 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
7077 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
7078 assert_eq!(
7079 cfg.headers
7080 .as_ref()
7081 .and_then(|h| h.get("X-Custom"))
7082 .map(String::as_str),
7083 Some("value"),
7084 );
7085 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
7086 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
7087 assert_eq!(cfg.max_prompt_tokens, Some(8192));
7088 assert_eq!(cfg.max_output_tokens, Some(2048));
7089
7090 let wire = serde_json::to_value(&cfg).unwrap();
7092 assert_eq!(wire["modelId"], "gpt-4");
7093 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
7094 assert_eq!(wire["maxPromptTokens"], 8192);
7095 assert_eq!(wire["maxOutputTokens"], 2048);
7096
7097 let unset = ProviderConfig::new("https://api.example.com");
7098 let wire_unset = serde_json::to_value(&unset).unwrap();
7099 assert!(wire_unset.get("modelId").is_none());
7100 assert!(wire_unset.get("wireModel").is_none());
7101 assert!(wire_unset.get("maxPromptTokens").is_none());
7102 assert!(wire_unset.get("maxOutputTokens").is_none());
7103 }
7104
7105 #[test]
7106 fn capi_session_options_builder_composes_and_serializes() {
7107 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
7108
7109 assert_eq!(cfg.enable_web_socket_responses, Some(false));
7110
7111 let wire = serde_json::to_value(&cfg).unwrap();
7112 assert_eq!(
7113 wire,
7114 serde_json::json!({ "enableWebSocketResponses": false })
7115 );
7116
7117 let unset = CapiSessionOptions::new();
7118 let wire_unset = serde_json::to_value(&unset).unwrap();
7119 assert!(wire_unset.get("enableWebSocketResponses").is_none());
7120 }
7121
7122 #[test]
7123 fn session_config_with_capi_serializes() {
7124 let (wire, _) = SessionConfig::default()
7125 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7126 .into_wire(Some(SessionId::from("capi-create")))
7127 .expect("no duplicate handlers");
7128 let json = serde_json::to_value(&wire).unwrap();
7129 assert_eq!(
7130 json["capi"],
7131 serde_json::json!({ "enableWebSocketResponses": false })
7132 );
7133
7134 let (empty_wire, _) = SessionConfig::default()
7135 .into_wire(Some(SessionId::from("capi-create-unset")))
7136 .expect("no duplicate handlers");
7137 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7138 assert!(empty_json.get("capi").is_none());
7139 }
7140
7141 #[test]
7142 fn resume_session_config_with_capi_serializes() {
7143 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7144 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7145 .into_wire()
7146 .expect("no duplicate handlers");
7147 let json = serde_json::to_value(&wire).unwrap();
7148 assert_eq!(
7149 json["capi"],
7150 serde_json::json!({ "enableWebSocketResponses": false })
7151 );
7152
7153 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
7154 .into_wire()
7155 .expect("no duplicate handlers");
7156 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7157 assert!(empty_json.get("capi").is_none());
7158 }
7159
7160 #[test]
7161 fn system_message_config_builder_composes() {
7162 use std::collections::HashMap;
7163
7164 let cfg = SystemMessageConfig::new()
7165 .with_mode("replace")
7166 .with_content("Custom system message.")
7167 .with_sections(HashMap::new());
7168
7169 assert_eq!(cfg.mode.as_deref(), Some("replace"));
7170 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
7171 assert!(cfg.sections.is_some());
7172 }
7173
7174 #[test]
7175 fn delivery_mode_serializes_to_kebab_case_strings() {
7176 assert_eq!(
7177 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
7178 "\"enqueue\""
7179 );
7180 assert_eq!(
7181 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
7182 "\"immediate\""
7183 );
7184 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
7185 assert_eq!(parsed, DeliveryMode::Immediate);
7186 }
7187
7188 #[test]
7189 fn agent_mode_serializes_to_kebab_case_strings() {
7190 assert_eq!(
7191 serde_json::to_string(&AgentMode::Interactive).unwrap(),
7192 "\"interactive\""
7193 );
7194 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
7195 assert_eq!(
7196 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
7197 "\"autopilot\""
7198 );
7199 assert_eq!(
7200 serde_json::to_string(&AgentMode::Shell).unwrap(),
7201 "\"shell\""
7202 );
7203 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
7204 assert_eq!(parsed, AgentMode::Plan);
7205 }
7206
7207 #[test]
7208 fn connection_state_distinguishes_variants() {
7209 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
7212 }
7213
7214 #[test]
7220 fn session_event_round_trips_agent_id_on_envelope() {
7221 let wire = json!({
7222 "id": "evt-1",
7223 "timestamp": "2026-04-30T12:00:00Z",
7224 "parentId": null,
7225 "agentId": "sub-agent-42",
7226 "type": "assistant.message",
7227 "data": { "message": "hi" }
7228 });
7229
7230 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
7231 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7232
7233 let roundtripped = serde_json::to_value(&event).unwrap();
7235 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7236
7237 let main_agent_event: SessionEvent = serde_json::from_value(json!({
7239 "id": "evt-2",
7240 "timestamp": "2026-04-30T12:00:01Z",
7241 "parentId": null,
7242 "type": "session.idle",
7243 "data": {}
7244 }))
7245 .unwrap();
7246 assert!(main_agent_event.agent_id.is_none());
7247 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
7248 assert!(roundtripped.get("agentId").is_none());
7249 }
7250
7251 #[test]
7253 fn typed_session_event_round_trips_agent_id_on_envelope() {
7254 let wire = json!({
7255 "id": "evt-1",
7256 "timestamp": "2026-04-30T12:00:00Z",
7257 "parentId": null,
7258 "agentId": "sub-agent-42",
7259 "type": "session.idle",
7260 "data": {}
7261 });
7262
7263 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
7264 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7265
7266 let roundtripped = serde_json::to_value(&event).unwrap();
7267 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7268 }
7269
7270 #[test]
7271 fn connection_state_variants_compile() {
7272 let _ = ConnectionState::Disconnected;
7276 let _ = ConnectionState::Connecting;
7277 let _ = ConnectionState::Connected;
7278 let _ = ConnectionState::Error;
7279 }
7280
7281 #[test]
7282 fn deserializes_runtime_attachment_variants() {
7283 let attachments: Vec<Attachment> = serde_json::from_value(json!([
7284 {
7285 "type": "file",
7286 "path": "/tmp/file.rs",
7287 "displayName": "file.rs",
7288 "lineRange": { "start": 7, "end": 12 }
7289 },
7290 {
7291 "type": "directory",
7292 "path": "/tmp/project",
7293 "displayName": "project"
7294 },
7295 {
7296 "type": "selection",
7297 "filePath": "/tmp/lib.rs",
7298 "displayName": "lib.rs",
7299 "text": "fn main() {}",
7300 "selection": {
7301 "start": { "line": 1, "character": 2 },
7302 "end": { "line": 3, "character": 4 }
7303 }
7304 },
7305 {
7306 "type": "blob",
7307 "data": "Zm9v",
7308 "mimeType": "image/png",
7309 "displayName": "image.png"
7310 },
7311 {
7312 "type": "github_reference",
7313 "number": 42,
7314 "title": "Fix rendering",
7315 "referenceType": "issue",
7316 "state": "open",
7317 "url": "https://github.com/example/repo/issues/42"
7318 }
7319 ]))
7320 .expect("attachments should deserialize");
7321
7322 assert_eq!(attachments.len(), 5);
7323 assert!(matches!(
7324 &attachments[0],
7325 Attachment::File {
7326 path,
7327 display_name,
7328 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7329 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7330 ));
7331 assert!(matches!(
7332 &attachments[1],
7333 Attachment::Directory { path, display_name }
7334 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7335 ));
7336 assert!(matches!(
7337 &attachments[2],
7338 Attachment::Selection {
7339 file_path,
7340 display_name,
7341 selection:
7342 AttachmentSelectionRange {
7343 start: AttachmentSelectionPosition { line: 1, character: 2 },
7344 end: AttachmentSelectionPosition { line: 3, character: 4 },
7345 },
7346 ..
7347 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
7348 ));
7349 assert!(matches!(
7350 &attachments[3],
7351 Attachment::Blob {
7352 data,
7353 mime_type,
7354 display_name,
7355 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
7356 ));
7357 assert!(matches!(
7358 &attachments[4],
7359 Attachment::GitHubReference {
7360 number: 42,
7361 title,
7362 reference_type: GitHubReferenceType::Issue,
7363 state,
7364 url,
7365 } if title == "Fix rendering"
7366 && state == "open"
7367 && url == "https://github.com/example/repo/issues/42"
7368 ));
7369 }
7370
7371 #[test]
7372 fn ensures_display_names_for_variants_that_support_them() {
7373 let mut attachments = vec![
7374 Attachment::File {
7375 path: PathBuf::from("/tmp/file.rs"),
7376 display_name: None,
7377 line_range: None,
7378 },
7379 Attachment::Selection {
7380 file_path: PathBuf::from("/tmp/src/lib.rs"),
7381 display_name: None,
7382 text: "fn main() {}".to_string(),
7383 selection: AttachmentSelectionRange {
7384 start: AttachmentSelectionPosition {
7385 line: 0,
7386 character: 0,
7387 },
7388 end: AttachmentSelectionPosition {
7389 line: 0,
7390 character: 10,
7391 },
7392 },
7393 },
7394 Attachment::Blob {
7395 data: "Zm9v".to_string(),
7396 mime_type: "image/png".to_string(),
7397 display_name: None,
7398 },
7399 Attachment::GitHubReference {
7400 number: 7,
7401 title: "Track regressions".to_string(),
7402 reference_type: GitHubReferenceType::Issue,
7403 state: "open".to_string(),
7404 url: "https://example.com/issues/7".to_string(),
7405 },
7406 ];
7407
7408 ensure_attachment_display_names(&mut attachments);
7409
7410 assert_eq!(attachments[0].display_name(), Some("file.rs"));
7411 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
7412 assert_eq!(attachments[2].display_name(), Some("attachment"));
7413 assert_eq!(attachments[3].display_name(), None);
7414 assert_eq!(
7415 attachments[3].label(),
7416 Some("Track regressions".to_string())
7417 );
7418 }
7419
7420 #[test]
7421 fn github_anchored_attachment_variants_round_trip() {
7422 let cases = vec![
7423 (
7424 "github_commit",
7425 json!({
7426 "type": "github_commit",
7427 "message": "Fix the thing",
7428 "oid": "abc123",
7429 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
7430 "url": "https://github.com/octocat/repo/commit/abc123"
7431 }),
7432 ),
7433 (
7434 "github_release",
7435 json!({
7436 "type": "github_release",
7437 "name": "v1.2.3",
7438 "repo": { "name": "repo", "owner": "octocat" },
7439 "tagName": "v1.2.3",
7440 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
7441 }),
7442 ),
7443 (
7444 "github_actions_job",
7445 json!({
7446 "type": "github_actions_job",
7447 "conclusion": "failure",
7448 "jobId": 99,
7449 "jobName": "build",
7450 "repo": { "name": "repo", "owner": "octocat" },
7451 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
7452 "workflowName": "CI"
7453 }),
7454 ),
7455 (
7456 "github_repository",
7457 json!({
7458 "type": "github_repository",
7459 "description": "An example repository",
7460 "ref": "main",
7461 "repo": { "name": "repo", "owner": "octocat" },
7462 "url": "https://github.com/octocat/repo"
7463 }),
7464 ),
7465 (
7466 "github_file_diff",
7467 json!({
7468 "type": "github_file_diff",
7469 "base": {
7470 "path": "src/lib.rs",
7471 "ref": "main",
7472 "repo": { "name": "repo", "owner": "octocat" }
7473 },
7474 "head": {
7475 "path": "src/lib.rs",
7476 "ref": "feature",
7477 "repo": { "name": "repo", "owner": "octocat" }
7478 },
7479 "url": "https://github.com/octocat/repo/compare/main...feature"
7480 }),
7481 ),
7482 (
7483 "github_tree_comparison",
7484 json!({
7485 "type": "github_tree_comparison",
7486 "base": {
7487 "repo": { "name": "repo", "owner": "octocat" },
7488 "revision": "main"
7489 },
7490 "head": {
7491 "repo": { "name": "repo", "owner": "octocat" },
7492 "revision": "feature"
7493 },
7494 "url": "https://github.com/octocat/repo/compare/main...feature"
7495 }),
7496 ),
7497 (
7498 "github_url",
7499 json!({
7500 "type": "github_url",
7501 "url": "https://github.com/octocat/repo/wiki"
7502 }),
7503 ),
7504 (
7505 "github_file",
7506 json!({
7507 "type": "github_file",
7508 "path": "src/main.rs",
7509 "ref": "main",
7510 "repo": { "name": "repo", "owner": "octocat" },
7511 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
7512 }),
7513 ),
7514 (
7515 "github_snippet",
7516 json!({
7517 "type": "github_snippet",
7518 "lineRange": { "start": 10, "end": 20 },
7519 "path": "src/main.rs",
7520 "ref": "main",
7521 "repo": { "name": "repo", "owner": "octocat" },
7522 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
7523 }),
7524 ),
7525 ];
7526
7527 for (expected_type, input) in cases {
7528 let attachment: Attachment = serde_json::from_value(input.clone())
7529 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
7530
7531 let serialized_string = serde_json::to_string(&attachment)
7536 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
7537
7538 assert_eq!(
7540 serialized_string.matches("\"type\":").count(),
7541 1,
7542 "{expected_type} must serialize a single `type` key"
7543 );
7544
7545 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
7546 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
7547 assert_eq!(
7548 serialized.get("type").and_then(|value| value.as_str()),
7549 Some(expected_type),
7550 "{expected_type} must serialize the correct discriminator"
7551 );
7552
7553 assert_eq!(
7555 serialized, input,
7556 "{expected_type} should round-trip without data loss"
7557 );
7558 let reparsed: Attachment = serde_json::from_value(serialized)
7559 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
7560 assert_eq!(
7561 reparsed, attachment,
7562 "{expected_type} should re-deserialize to the same value"
7563 );
7564 }
7565 }
7566}
7567
7568#[cfg(test)]
7569mod permission_builder_tests {
7570 use std::sync::Arc;
7571
7572 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
7573 use crate::permission;
7574 use crate::types::{
7575 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
7576 SessionId,
7577 };
7578
7579 fn data() -> PermissionRequestData {
7580 PermissionRequestData {
7581 extra: serde_json::json!({"tool": "shell"}),
7582 ..Default::default()
7583 }
7584 }
7585
7586 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7589 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7590 }
7591
7592 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7593 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7594 }
7595
7596 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
7597 handler
7598 .handle(SessionId::from("s1"), RequestId::new("1"), data())
7599 .await
7600 }
7601
7602 #[tokio::test]
7603 async fn approve_all_with_handler_present_approves() {
7604 let cfg = SessionConfig::default()
7605 .with_permission_handler(Arc::new(ApproveAllHandler))
7606 .approve_all_permissions();
7607 let h = resolve_create(cfg).expect("policy + handler yields handler");
7608 assert!(matches!(
7609 dispatch(&h).await,
7610 PermissionResult::Decision {
7611 decision: PermissionDecision::ApproveOnce(_),
7612 ..
7613 }
7614 ));
7615 }
7616
7617 #[tokio::test]
7618 async fn approve_all_standalone_produces_handler() {
7619 let cfg = SessionConfig::default().approve_all_permissions();
7620 let h = resolve_create(cfg).expect("policy alone yields handler");
7621 assert!(matches!(
7622 dispatch(&h).await,
7623 PermissionResult::Decision {
7624 decision: PermissionDecision::ApproveOnce(_),
7625 ..
7626 }
7627 ));
7628 }
7629
7630 #[tokio::test]
7633 async fn approve_all_is_order_independent() {
7634 let a = SessionConfig::default()
7635 .with_permission_handler(Arc::new(ApproveAllHandler))
7636 .approve_all_permissions();
7637 let b = SessionConfig::default()
7638 .approve_all_permissions()
7639 .with_permission_handler(Arc::new(ApproveAllHandler));
7640 let ha = resolve_create(a).unwrap();
7641 let hb = resolve_create(b).unwrap();
7642 assert!(matches!(
7643 dispatch(&ha).await,
7644 PermissionResult::Decision {
7645 decision: PermissionDecision::ApproveOnce(_),
7646 ..
7647 }
7648 ));
7649 assert!(matches!(
7650 dispatch(&hb).await,
7651 PermissionResult::Decision {
7652 decision: PermissionDecision::ApproveOnce(_),
7653 ..
7654 }
7655 ));
7656 }
7657
7658 #[tokio::test]
7659 async fn deny_all_is_order_independent() {
7660 let a = SessionConfig::default()
7661 .with_permission_handler(Arc::new(ApproveAllHandler))
7662 .deny_all_permissions();
7663 let b = SessionConfig::default()
7664 .deny_all_permissions()
7665 .with_permission_handler(Arc::new(ApproveAllHandler));
7666 let ha = resolve_create(a).unwrap();
7667 let hb = resolve_create(b).unwrap();
7668 assert!(matches!(
7669 dispatch(&ha).await,
7670 PermissionResult::Decision {
7671 decision: PermissionDecision::Reject(_),
7672 ..
7673 }
7674 ));
7675 assert!(matches!(
7676 dispatch(&hb).await,
7677 PermissionResult::Decision {
7678 decision: PermissionDecision::Reject(_),
7679 ..
7680 }
7681 ));
7682 }
7683
7684 #[tokio::test]
7685 async fn approve_permissions_if_consults_predicate() {
7686 let cfg = SessionConfig::default().approve_permissions_if(|d| {
7687 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7688 });
7689 let h = resolve_create(cfg).unwrap();
7690 assert!(matches!(
7691 dispatch(&h).await,
7692 PermissionResult::Decision {
7693 decision: PermissionDecision::Reject(_),
7694 ..
7695 }
7696 ));
7697 }
7698
7699 #[tokio::test]
7700 async fn approve_permissions_if_is_order_independent() {
7701 let predicate = |d: &PermissionRequestData| {
7702 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7703 };
7704 let a = SessionConfig::default()
7705 .with_permission_handler(Arc::new(ApproveAllHandler))
7706 .approve_permissions_if(predicate);
7707 let b = SessionConfig::default()
7708 .approve_permissions_if(predicate)
7709 .with_permission_handler(Arc::new(ApproveAllHandler));
7710 let ha = resolve_create(a).unwrap();
7711 let hb = resolve_create(b).unwrap();
7712 assert!(matches!(
7713 dispatch(&ha).await,
7714 PermissionResult::Decision {
7715 decision: PermissionDecision::Reject(_),
7716 ..
7717 }
7718 ));
7719 assert!(matches!(
7720 dispatch(&hb).await,
7721 PermissionResult::Decision {
7722 decision: PermissionDecision::Reject(_),
7723 ..
7724 }
7725 ));
7726 }
7727
7728 #[tokio::test]
7729 async fn resume_session_config_approve_all_works() {
7730 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
7731 .with_permission_handler(Arc::new(ApproveAllHandler))
7732 .approve_all_permissions();
7733 let h = resolve_resume(cfg).unwrap();
7734 assert!(matches!(
7735 dispatch(&h).await,
7736 PermissionResult::Decision {
7737 decision: PermissionDecision::ApproveOnce(_),
7738 ..
7739 }
7740 ));
7741 }
7742
7743 #[tokio::test]
7744 async fn resume_session_config_approve_all_is_order_independent() {
7745 let a = ResumeSessionConfig::new(SessionId::from("s1"))
7746 .with_permission_handler(Arc::new(ApproveAllHandler))
7747 .approve_all_permissions();
7748 let b = ResumeSessionConfig::new(SessionId::from("s1"))
7749 .approve_all_permissions()
7750 .with_permission_handler(Arc::new(ApproveAllHandler));
7751 let ha = resolve_resume(a).unwrap();
7752 let hb = resolve_resume(b).unwrap();
7753 assert!(matches!(
7754 dispatch(&ha).await,
7755 PermissionResult::Decision {
7756 decision: PermissionDecision::ApproveOnce(_),
7757 ..
7758 }
7759 ));
7760 assert!(matches!(
7761 dispatch(&hb).await,
7762 PermissionResult::Decision {
7763 decision: PermissionDecision::ApproveOnce(_),
7764 ..
7765 }
7766 ));
7767 }
7768
7769 #[test]
7770 fn session_config_enable_experimental_mode_serializes_when_set() {
7771 let cfg = SessionConfig::default().with_enable_experimental_mode(false);
7772 assert_eq!(cfg.enable_experimental_mode, Some(false));
7773
7774 let (wire, _runtime) = cfg
7775 .into_wire(Some(SessionId::from("experimental-mode")))
7776 .expect("enable_experimental_mode config has no duplicate handlers");
7777 assert_eq!(wire.is_experimental_mode, Some(false));
7778
7779 let json = serde_json::to_value(&wire).unwrap();
7780 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7781 }
7782
7783 #[test]
7784 fn session_config_enable_experimental_mode_omitted_when_none() {
7785 let cfg = SessionConfig::default();
7786 assert_eq!(cfg.enable_experimental_mode, None);
7787
7788 let (wire, _runtime) = cfg
7789 .into_wire(Some(SessionId::from("no-experimental-mode")))
7790 .expect("default config has no duplicate handlers");
7791 assert_eq!(wire.is_experimental_mode, None);
7792
7793 let json = serde_json::to_value(&wire).unwrap();
7794 assert!(json.get("isExperimentalMode").is_none());
7795 }
7796
7797 #[test]
7798 fn resume_session_config_enable_experimental_mode_serializes_when_set() {
7799 let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
7800 .with_enable_experimental_mode(false);
7801 assert_eq!(cfg.enable_experimental_mode, Some(false));
7802
7803 let (wire, _runtime) = cfg
7804 .into_wire()
7805 .expect("resume enable_experimental_mode config has no duplicate handlers");
7806 assert_eq!(wire.is_experimental_mode, Some(false));
7807
7808 let json = serde_json::to_value(&wire).unwrap();
7809 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7810 }
7811
7812 #[test]
7813 fn resume_session_config_enable_experimental_mode_omitted_when_none() {
7814 let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
7815 assert_eq!(cfg.enable_experimental_mode, None);
7816
7817 let (wire, _runtime) = cfg
7818 .into_wire()
7819 .expect("default resume config has no duplicate handlers");
7820 assert_eq!(wire.is_experimental_mode, None);
7821
7822 let json = serde_json::to_value(&wire).unwrap();
7823 assert!(json.get("isExperimentalMode").is_none());
7824 }
7825}
7826
7827#[cfg(test)]
7828mod is_terminal_tests {
7829 use super::Tool;
7830
7831 #[test]
7832 fn is_terminal_serializes_as_camel_case_when_set() {
7833 let tool = Tool {
7834 name: "clear_context".to_owned(),
7835 is_terminal: true,
7836 ..Default::default()
7837 };
7838 let value = serde_json::to_value(&tool).expect("tool serializes");
7839 assert_eq!(
7840 value.get("isTerminal"),
7841 Some(&serde_json::Value::Bool(true))
7842 );
7843 }
7844
7845 #[test]
7846 fn is_terminal_is_omitted_when_false() {
7847 let tool = Tool {
7848 name: "plain".to_owned(),
7849 ..Default::default()
7850 };
7851 let value = serde_json::to_value(&tool).expect("tool serializes");
7852 assert!(value.get("isTerminal").is_none());
7853 }
7854
7855 #[test]
7858 fn is_terminal_appears_in_debug_output() {
7859 let terminal = Tool {
7860 name: "clear_context".to_owned(),
7861 is_terminal: true,
7862 ..Default::default()
7863 };
7864 assert!(format!("{terminal:?}").contains("is_terminal: true"));
7865
7866 let plain = Tool {
7867 name: "plain".to_owned(),
7868 ..Default::default()
7869 };
7870 assert!(format!("{plain:?}").contains("is_terminal: false"));
7871 }
7872}