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::github_token::GitHubTokenProvider;
28use crate::handler::{
29 AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler,
30 PermissionHandler, UserInputHandler,
31};
32use crate::hooks::SessionHooks;
33use crate::provider_token::BearerTokenProvider;
34pub use crate::session_fs::{
35 DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig,
36 SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult,
37 SessionFsSqliteQueryType, SessionFsSqliteTransactionError,
38 SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionStatement,
39};
40pub use crate::trace_context::{TraceContext, TraceContextProvider};
41use crate::transforms::SystemMessageTransform;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46#[allow(dead_code)]
47#[non_exhaustive]
48pub(crate) enum ConnectionState {
49 Disconnected,
51 Connecting,
53 Connected,
55 Error,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
64#[non_exhaustive]
65pub enum SessionLifecycleEventType {
66 #[serde(rename = "session.created")]
68 Created,
69 #[serde(rename = "session.deleted")]
71 Deleted,
72 #[serde(rename = "session.updated")]
74 Updated,
75 #[serde(rename = "session.foreground")]
77 Foreground,
78 #[serde(rename = "session.background")]
80 Background,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct SessionLifecycleEventMetadata {
86 #[serde(rename = "startTime")]
88 pub start_time: String,
89 #[serde(rename = "modifiedTime")]
91 pub modified_time: String,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub summary: Option<String>,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct SessionLifecycleEvent {
101 #[serde(rename = "type")]
103 pub event_type: SessionLifecycleEventType,
104 #[serde(rename = "sessionId")]
106 pub session_id: SessionId,
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub metadata: Option<SessionLifecycleEventMetadata>,
110}
111
112#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
118#[serde(transparent)]
119pub struct SessionId(String);
120
121impl SessionId {
122 pub fn new(id: impl Into<String>) -> Self {
124 Self(id.into())
125 }
126
127 pub fn as_str(&self) -> &str {
129 &self.0
130 }
131
132 pub fn into_inner(self) -> String {
134 self.0
135 }
136}
137
138impl std::ops::Deref for SessionId {
139 type Target = str;
140
141 fn deref(&self) -> &str {
142 &self.0
143 }
144}
145
146impl std::fmt::Display for SessionId {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 f.write_str(&self.0)
149 }
150}
151
152impl From<String> for SessionId {
153 fn from(s: String) -> Self {
154 Self(s)
155 }
156}
157
158impl From<&str> for SessionId {
159 fn from(s: &str) -> Self {
160 Self(s.to_owned())
161 }
162}
163
164impl AsRef<str> for SessionId {
165 fn as_ref(&self) -> &str {
166 &self.0
167 }
168}
169
170impl std::borrow::Borrow<str> for SessionId {
171 fn borrow(&self) -> &str {
172 &self.0
173 }
174}
175
176impl From<SessionId> for String {
177 fn from(id: SessionId) -> String {
178 id.0
179 }
180}
181
182impl PartialEq<str> for SessionId {
183 fn eq(&self, other: &str) -> bool {
184 self.0 == other
185 }
186}
187
188impl PartialEq<String> for SessionId {
189 fn eq(&self, other: &String) -> bool {
190 &self.0 == other
191 }
192}
193
194impl PartialEq<SessionId> for String {
195 fn eq(&self, other: &SessionId) -> bool {
196 self == &other.0
197 }
198}
199
200impl PartialEq<&str> for SessionId {
201 fn eq(&self, other: &&str) -> bool {
202 self.0 == *other
203 }
204}
205
206impl PartialEq<&SessionId> for SessionId {
207 fn eq(&self, other: &&SessionId) -> bool {
208 self.0 == other.0
209 }
210}
211
212impl PartialEq<SessionId> for &SessionId {
213 fn eq(&self, other: &SessionId) -> bool {
214 self.0 == other.0
215 }
216}
217
218#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
224#[serde(transparent)]
225pub struct RequestId(String);
226
227impl RequestId {
228 pub fn new(id: impl Into<String>) -> Self {
230 Self(id.into())
231 }
232
233 pub fn into_inner(self) -> String {
235 self.0
236 }
237}
238
239impl std::ops::Deref for RequestId {
240 type Target = str;
241
242 fn deref(&self) -> &str {
243 &self.0
244 }
245}
246
247impl std::fmt::Display for RequestId {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 f.write_str(&self.0)
250 }
251}
252
253impl From<String> for RequestId {
254 fn from(s: String) -> Self {
255 Self(s)
256 }
257}
258
259impl From<&str> for RequestId {
260 fn from(s: &str) -> Self {
261 Self(s.to_owned())
262 }
263}
264
265impl AsRef<str> for RequestId {
266 fn as_ref(&self) -> &str {
267 &self.0
268 }
269}
270
271impl std::borrow::Borrow<str> for RequestId {
272 fn borrow(&self) -> &str {
273 &self.0
274 }
275}
276
277impl From<RequestId> for String {
278 fn from(id: RequestId) -> String {
279 id.0
280 }
281}
282
283impl PartialEq<str> for RequestId {
284 fn eq(&self, other: &str) -> bool {
285 self.0 == other
286 }
287}
288
289impl PartialEq<String> for RequestId {
290 fn eq(&self, other: &String) -> bool {
291 &self.0 == other
292 }
293}
294
295impl PartialEq<RequestId> for String {
296 fn eq(&self, other: &RequestId) -> bool {
297 self == &other.0
298 }
299}
300
301impl PartialEq<&str> for RequestId {
302 fn eq(&self, other: &&str) -> bool {
303 self.0 == *other
304 }
305}
306
307#[derive(Clone, Default, Serialize, Deserialize)]
322#[serde(rename_all = "camelCase")]
323#[non_exhaustive]
324pub struct Tool {
325 pub name: String,
327 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub namespaced_name: Option<String>,
331 #[serde(default)]
333 pub description: String,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
336 pub instructions: Option<String>,
337 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
339 pub parameters: IndexMap<String, Value>,
340 #[serde(default, skip_serializing_if = "is_false")]
344 pub overrides_built_in_tool: bool,
345 #[serde(default, skip_serializing_if = "is_false")]
349 pub skip_permission: bool,
350 #[serde(default, skip_serializing_if = "is_false")]
355 pub is_terminal: bool,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
362 pub defer: Option<DeferMode>,
363 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
368 pub metadata: IndexMap<String, Value>,
369 #[serde(skip)]
381 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
382}
383
384#[inline]
385fn is_false(b: &bool) -> bool {
386 !*b
387}
388
389#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
392#[serde(rename_all = "lowercase")]
393pub enum DeferMode {
394 Auto,
396 Never,
398}
399
400impl Tool {
401 pub fn new(name: impl Into<String>) -> Self {
421 Self {
422 name: name.into(),
423 ..Default::default()
424 }
425 }
426
427 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
430 self.namespaced_name = Some(namespaced_name.into());
431 self
432 }
433
434 pub fn with_description(mut self, description: impl Into<String>) -> Self {
436 self.description = description.into();
437 self
438 }
439
440 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
442 self.instructions = Some(instructions.into());
443 self
444 }
445
446 pub fn with_parameters(mut self, parameters: Value) -> Self {
460 self.parameters = crate::tool::tool_parameters(parameters);
461 self
462 }
463
464 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
468 self.overrides_built_in_tool = overrides;
469 self
470 }
471
472 pub fn with_skip_permission(mut self, skip: bool) -> Self {
476 self.skip_permission = skip;
477 self
478 }
479
480 #[must_use]
487 pub fn with_is_terminal(mut self, is_terminal: bool) -> Self {
488 self.is_terminal = is_terminal;
489 self
490 }
491
492 pub fn with_defer(mut self, defer: DeferMode) -> Self {
496 self.defer = Some(defer);
497 self
498 }
499
500 pub fn with_metadata(mut self, metadata: IndexMap<String, Value>) -> Self {
503 self.metadata = metadata;
504 self
505 }
506
507 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
511 self.handler = Some(handler);
512 self
513 }
514
515 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
520 self.handler.as_ref()
521 }
522}
523
524impl std::fmt::Debug for Tool {
525 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
526 f.debug_struct("Tool")
527 .field("name", &self.name)
528 .field("namespaced_name", &self.namespaced_name)
529 .field("description", &self.description)
530 .field("instructions", &self.instructions)
531 .field("parameters", &self.parameters)
532 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
533 .field("skip_permission", &self.skip_permission)
534 .field("is_terminal", &self.is_terminal)
535 .field("defer", &self.defer)
536 .field("metadata", &self.metadata)
537 .field(
538 "handler",
539 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
540 )
541 .finish()
542 }
543}
544
545#[non_exhaustive]
548#[derive(Debug, Clone)]
549pub struct CommandContext {
550 pub session_id: SessionId,
552 pub command: String,
554 pub command_name: String,
556 pub args: String,
558}
559
560#[async_trait::async_trait]
566pub trait CommandHandler: Send + Sync {
567 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
569}
570
571#[non_exhaustive]
577#[derive(Clone)]
578pub struct CommandDefinition {
579 pub name: String,
581 pub description: Option<String>,
583 pub handler: Arc<dyn CommandHandler>,
585}
586
587impl CommandDefinition {
588 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
591 Self {
592 name: name.into(),
593 description: None,
594 handler,
595 }
596 }
597
598 pub fn with_description(mut self, description: impl Into<String>) -> Self {
600 self.description = Some(description.into());
601 self
602 }
603}
604
605impl std::fmt::Debug for CommandDefinition {
606 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
607 f.debug_struct("CommandDefinition")
608 .field("name", &self.name)
609 .field("description", &self.description)
610 .field("handler", &"<set>")
611 .finish()
612 }
613}
614
615impl Serialize for CommandDefinition {
616 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
617 use serde::ser::SerializeStruct;
618 let mut state = serializer.serialize_struct("CommandDefinition", 2)?;
619 state.serialize_field("name", &self.name)?;
620 state.serialize_field("description", self.description.as_deref().unwrap_or(""))?;
621 state.end()
622 }
623}
624
625#[derive(Debug, Clone, Default, Serialize, Deserialize)]
632#[serde(rename_all = "camelCase")]
633#[non_exhaustive]
634pub struct CustomAgentConfig {
635 pub name: String,
637 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub display_name: Option<String>,
640 #[serde(default, skip_serializing_if = "Option::is_none")]
642 pub description: Option<String>,
643 #[serde(default, skip_serializing_if = "Option::is_none")]
645 pub tools: Option<Vec<String>>,
646 pub prompt: String,
648 #[serde(default, skip_serializing_if = "Option::is_none")]
650 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
651 #[serde(default, skip_serializing_if = "Option::is_none")]
653 pub infer: Option<bool>,
654 #[serde(default, skip_serializing_if = "Option::is_none")]
656 pub skills: Option<Vec<String>>,
657 #[serde(default, skip_serializing_if = "Option::is_none")]
662 pub model: Option<String>,
663 #[serde(default, skip_serializing_if = "Option::is_none")]
668 pub reasoning_effort: Option<String>,
669}
670
671impl CustomAgentConfig {
672 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
679 Self {
680 name: name.into(),
681 prompt: prompt.into(),
682 ..Self::default()
683 }
684 }
685
686 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
688 self.display_name = Some(display_name.into());
689 self
690 }
691
692 pub fn with_description(mut self, description: impl Into<String>) -> Self {
694 self.description = Some(description.into());
695 self
696 }
697
698 pub fn with_tools<I, S>(mut self, tools: I) -> Self
701 where
702 I: IntoIterator<Item = S>,
703 S: Into<String>,
704 {
705 self.tools = Some(tools.into_iter().map(Into::into).collect());
706 self
707 }
708
709 pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
711 self.mcp_servers = Some(mcp_servers);
712 self
713 }
714
715 pub fn with_infer(mut self, infer: bool) -> Self {
717 self.infer = Some(infer);
718 self
719 }
720
721 pub fn with_skills<I, S>(mut self, skills: I) -> Self
723 where
724 I: IntoIterator<Item = S>,
725 S: Into<String>,
726 {
727 self.skills = Some(skills.into_iter().map(Into::into).collect());
728 self
729 }
730
731 pub fn with_model(mut self, model: impl Into<String>) -> Self {
733 self.model = Some(model.into());
734 self
735 }
736
737 pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into<String>) -> Self {
739 self.reasoning_effort = Some(reasoning_effort.into());
740 self
741 }
742}
743
744#[derive(Debug, Clone, Default, Serialize, Deserialize)]
751#[serde(rename_all = "camelCase")]
752pub struct DefaultAgentConfig {
753 #[serde(default, skip_serializing_if = "Option::is_none")]
755 pub excluded_tools: Option<Vec<String>>,
756}
757
758#[derive(Debug, Clone, Default, Serialize, Deserialize)]
764#[serde(rename_all = "camelCase")]
765#[non_exhaustive]
766pub struct LargeToolOutputConfig {
767 #[serde(default, skip_serializing_if = "Option::is_none")]
769 pub enabled: Option<bool>,
770 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub max_size_bytes: Option<u64>,
774 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
777 pub output_directory: Option<PathBuf>,
778}
779
780impl LargeToolOutputConfig {
781 pub fn new() -> Self {
784 Self::default()
785 }
786
787 pub fn with_enabled(mut self, enabled: bool) -> Self {
789 self.enabled = Some(enabled);
790 self
791 }
792
793 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
795 self.max_size_bytes = Some(max_size_bytes);
796 self
797 }
798
799 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
801 self.output_directory = Some(output_directory.into());
802 self
803 }
804}
805
806#[derive(Debug, Clone, Default, Serialize, Deserialize)]
812#[serde(rename_all = "camelCase")]
813#[non_exhaustive]
814pub struct ToolSearchConfig {
815 #[serde(default, skip_serializing_if = "Option::is_none")]
817 pub enabled: Option<bool>,
818 #[serde(default, skip_serializing_if = "Option::is_none")]
821 pub defer_threshold: Option<u32>,
822}
823
824impl ToolSearchConfig {
825 pub fn new() -> Self {
828 Self::default()
829 }
830
831 pub fn with_enabled(mut self, enabled: bool) -> Self {
833 self.enabled = Some(enabled);
834 self
835 }
836
837 pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
840 self.defer_threshold = Some(defer_threshold);
841 self
842 }
843}
844
845#[derive(Debug, Clone, Default, Serialize, Deserialize)]
850#[serde(rename_all = "camelCase")]
851#[non_exhaustive]
852pub struct GitHubMcpToolConfig {
853 #[serde(default, skip_serializing_if = "Option::is_none")]
855 pub enable_all_tools: Option<bool>,
856 #[serde(default, skip_serializing_if = "Option::is_none")]
858 pub additional_toolsets: Option<Vec<String>>,
859 #[serde(default, skip_serializing_if = "Option::is_none")]
861 pub additional_tools: Option<Vec<String>>,
862 #[serde(default, skip_serializing_if = "Option::is_none")]
864 pub enable_insiders_mode: Option<bool>,
865 #[serde(default, skip_serializing_if = "Option::is_none")]
869 pub disable_form_deferral: Option<bool>,
870}
871
872impl GitHubMcpToolConfig {
873 pub fn new() -> Self {
875 Self::default()
876 }
877
878 pub fn with_enable_all_tools(mut self, value: bool) -> Self {
880 self.enable_all_tools = Some(value);
881 self
882 }
883
884 pub fn with_additional_toolsets<I, S>(mut self, values: I) -> Self
886 where
887 I: IntoIterator<Item = S>,
888 S: Into<String>,
889 {
890 self.additional_toolsets = Some(values.into_iter().map(Into::into).collect());
891 self
892 }
893
894 pub fn with_additional_tools<I, S>(mut self, values: I) -> Self
896 where
897 I: IntoIterator<Item = S>,
898 S: Into<String>,
899 {
900 self.additional_tools = Some(values.into_iter().map(Into::into).collect());
901 self
902 }
903
904 pub fn with_enable_insiders_mode(mut self, value: bool) -> Self {
906 self.enable_insiders_mode = Some(value);
907 self
908 }
909
910 pub fn with_disable_form_deferral(mut self, value: bool) -> Self {
914 self.disable_form_deferral = Some(value);
915 self
916 }
917}
918
919#[derive(Debug, Clone, Default, Serialize, Deserialize)]
926#[serde(rename_all = "camelCase")]
927#[non_exhaustive]
928pub struct InfiniteSessionConfig {
929 #[serde(default, skip_serializing_if = "Option::is_none")]
931 pub enabled: Option<bool>,
932 #[serde(default, skip_serializing_if = "Option::is_none")]
935 pub background_compaction_threshold: Option<f64>,
936 #[serde(default, skip_serializing_if = "Option::is_none")]
939 pub buffer_exhaustion_threshold: Option<f64>,
940}
941
942impl InfiniteSessionConfig {
943 pub fn new() -> Self {
946 Self::default()
947 }
948
949 pub fn with_enabled(mut self, enabled: bool) -> Self {
952 self.enabled = Some(enabled);
953 self
954 }
955
956 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
959 self.background_compaction_threshold = Some(threshold);
960 self
961 }
962
963 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
966 self.buffer_exhaustion_threshold = Some(threshold);
967 self
968 }
969}
970
971#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
982#[serde(rename_all = "camelCase")]
983#[non_exhaustive]
984pub struct MemoryConfiguration {
985 pub enabled: bool,
987}
988
989impl MemoryConfiguration {
990 pub fn enabled() -> Self {
992 Self { enabled: true }
993 }
994
995 pub fn disabled() -> Self {
997 Self { enabled: false }
998 }
999
1000 pub fn with_enabled(mut self, enabled: bool) -> Self {
1002 self.enabled = enabled;
1003 self
1004 }
1005}
1006
1007#[derive(Debug, Clone, Serialize, Deserialize)]
1009#[serde(rename_all = "camelCase")]
1010#[non_exhaustive]
1011pub struct CloudSessionRepository {
1012 pub owner: String,
1014 pub name: String,
1016 #[serde(skip_serializing_if = "Option::is_none")]
1018 pub branch: Option<String>,
1019}
1020
1021impl CloudSessionRepository {
1022 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
1024 Self {
1025 owner: owner.into(),
1026 name: name.into(),
1027 branch: None,
1028 }
1029 }
1030
1031 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
1033 self.branch = Some(branch.into());
1034 self
1035 }
1036}
1037
1038#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1040#[serde(rename_all = "camelCase")]
1041#[non_exhaustive]
1042pub struct CloudSessionOptions {
1043 #[serde(skip_serializing_if = "Option::is_none")]
1045 pub repository: Option<CloudSessionRepository>,
1046}
1047
1048impl CloudSessionOptions {
1049 pub fn with_repository(repository: CloudSessionRepository) -> Self {
1051 Self {
1052 repository: Some(repository),
1053 }
1054 }
1055}
1056
1057#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1059#[serde(rename_all = "camelCase")]
1060pub struct ExtensionInfo {
1061 pub source: String,
1063 pub name: String,
1065}
1066
1067impl ExtensionInfo {
1068 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
1070 Self {
1071 source: source.into(),
1072 name: name.into(),
1073 }
1074 }
1075}
1076
1077#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1088#[serde(rename_all = "camelCase")]
1089pub struct CanvasProviderIdentity {
1090 pub id: String,
1092 #[serde(skip_serializing_if = "Option::is_none")]
1094 pub name: Option<String>,
1095}
1096
1097impl CanvasProviderIdentity {
1098 pub fn new(id: impl Into<String>) -> Self {
1100 Self {
1101 id: id.into(),
1102 name: None,
1103 }
1104 }
1105
1106 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1108 self.name = Some(name.into());
1109 self
1110 }
1111}
1112
1113#[derive(Debug, Clone, Serialize, Deserialize)]
1147#[serde(tag = "type", rename_all = "lowercase")]
1148#[non_exhaustive]
1149pub enum McpServerConfig {
1150 #[serde(alias = "local")]
1154 Stdio(McpStdioServerConfig),
1155 Http(McpHttpServerConfig),
1157 Sse(McpHttpServerConfig),
1159}
1160
1161#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1165#[serde(rename_all = "camelCase")]
1166pub struct McpStdioServerConfig {
1167 #[serde(default, skip_serializing_if = "Option::is_none")]
1173 pub tools: Option<Vec<String>>,
1174 #[serde(default, skip_serializing_if = "Option::is_none")]
1176 pub timeout: Option<i64>,
1177 pub command: String,
1179 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1181 pub args: Vec<String>,
1182 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1185 pub env: HashMap<String, String>,
1186 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1188 pub working_directory: Option<String>,
1189}
1190
1191#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1195#[serde(rename_all = "camelCase")]
1196pub struct McpHttpServerConfig {
1197 #[serde(default, skip_serializing_if = "Option::is_none")]
1203 pub tools: Option<Vec<String>>,
1204 #[serde(default, skip_serializing_if = "Option::is_none")]
1206 pub timeout: Option<i64>,
1207 pub url: String,
1209 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1211 pub headers: HashMap<String, String>,
1212}
1213
1214#[derive(Clone, Default, Serialize, Deserialize)]
1220#[serde(rename_all = "camelCase")]
1221#[non_exhaustive]
1222pub struct ProviderConfig {
1223 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1226 pub provider_type: Option<String>,
1227 #[serde(default, skip_serializing_if = "Option::is_none")]
1230 pub wire_api: Option<String>,
1231 #[serde(default, skip_serializing_if = "Option::is_none")]
1236 pub transport: Option<String>,
1237 pub base_url: String,
1239 #[serde(default, skip_serializing_if = "Option::is_none")]
1241 pub api_key: Option<String>,
1242 #[serde(default, skip_serializing_if = "Option::is_none")]
1246 pub bearer_token: Option<String>,
1247 #[serde(skip)]
1250 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1251 #[serde(default, skip_serializing_if = "Option::is_none")]
1252 pub(crate) has_bearer_token_provider: Option<bool>,
1253 #[serde(default, skip_serializing_if = "Option::is_none")]
1255 pub azure: Option<AzureProviderOptions>,
1256 #[serde(default, skip_serializing_if = "Option::is_none")]
1258 pub headers: Option<HashMap<String, String>>,
1259 #[serde(default, skip_serializing_if = "Option::is_none")]
1263 pub model_id: Option<String>,
1264 #[serde(default, skip_serializing_if = "Option::is_none")]
1271 pub wire_model: Option<String>,
1272 #[serde(default, skip_serializing_if = "Option::is_none")]
1277 pub max_prompt_tokens: Option<i64>,
1278 #[serde(default, skip_serializing_if = "Option::is_none")]
1281 pub max_output_tokens: Option<i64>,
1282}
1283
1284impl std::fmt::Debug for ProviderConfig {
1285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1286 f.debug_struct("ProviderConfig")
1287 .field("provider_type", &self.provider_type)
1288 .field("wire_api", &self.wire_api)
1289 .field("transport", &self.transport)
1290 .field("base_url", &self.base_url)
1291 .field("api_key", &self.api_key)
1292 .field("bearer_token", &self.bearer_token)
1293 .field(
1294 "bearer_token_provider",
1295 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1296 )
1297 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1298 .field("azure", &self.azure)
1299 .field("headers", &self.headers)
1300 .field("model_id", &self.model_id)
1301 .field("wire_model", &self.wire_model)
1302 .field("max_prompt_tokens", &self.max_prompt_tokens)
1303 .field("max_output_tokens", &self.max_output_tokens)
1304 .finish()
1305 }
1306}
1307
1308impl ProviderConfig {
1309 pub fn new(base_url: impl Into<String>) -> Self {
1312 Self {
1313 base_url: base_url.into(),
1314 ..Self::default()
1315 }
1316 }
1317
1318 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1320 self.provider_type = Some(provider_type.into());
1321 self
1322 }
1323
1324 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1326 self.wire_api = Some(wire_api.into());
1327 self
1328 }
1329
1330 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1333 self.transport = Some(transport.into());
1334 self
1335 }
1336
1337 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1339 self.api_key = Some(api_key.into());
1340 self
1341 }
1342
1343 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1346 self.bearer_token = Some(bearer_token.into());
1347 self
1348 }
1349
1350 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1356 self.bearer_token_provider = Some(provider);
1357 self
1358 }
1359
1360 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1362 self.azure = Some(azure);
1363 self
1364 }
1365
1366 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1368 self.headers = Some(headers);
1369 self
1370 }
1371
1372 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1375 self.model_id = Some(model_id.into());
1376 self
1377 }
1378
1379 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1384 self.wire_model = Some(wire_model.into());
1385 self
1386 }
1387
1388 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1392 self.max_prompt_tokens = Some(max);
1393 self
1394 }
1395
1396 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1399 self.max_output_tokens = Some(max);
1400 self
1401 }
1402}
1403
1404#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1417#[serde(rename_all = "camelCase")]
1418#[non_exhaustive]
1419pub struct CapiSessionOptions {
1420 #[serde(default, skip_serializing_if = "Option::is_none")]
1426 pub enable_web_socket_responses: Option<bool>,
1427}
1428
1429impl CapiSessionOptions {
1430 pub fn new() -> Self {
1432 Self::default()
1433 }
1434
1435 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1437 self.enable_web_socket_responses = Some(enable);
1438 self
1439 }
1440}
1441
1442#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1444#[serde(rename_all = "camelCase")]
1445pub struct AzureProviderOptions {
1446 #[serde(default, skip_serializing_if = "Option::is_none")]
1448 pub api_version: Option<String>,
1449}
1450
1451#[derive(Clone, Default, Serialize, Deserialize)]
1462#[serde(rename_all = "camelCase")]
1463#[non_exhaustive]
1464pub struct NamedProviderConfig {
1465 pub name: String,
1468 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1471 pub provider_type: Option<String>,
1472 #[serde(default, skip_serializing_if = "Option::is_none")]
1475 pub wire_api: Option<String>,
1476 pub base_url: String,
1478 #[serde(default, skip_serializing_if = "Option::is_none")]
1480 pub api_key: Option<String>,
1481 #[serde(default, skip_serializing_if = "Option::is_none")]
1484 pub bearer_token: Option<String>,
1485 #[serde(skip)]
1488 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1489 #[serde(default, skip_serializing_if = "Option::is_none")]
1490 pub(crate) has_bearer_token_provider: Option<bool>,
1491 #[serde(default, skip_serializing_if = "Option::is_none")]
1493 pub azure: Option<AzureProviderOptions>,
1494 #[serde(default, skip_serializing_if = "Option::is_none")]
1496 pub headers: Option<HashMap<String, String>>,
1497}
1498
1499impl std::fmt::Debug for NamedProviderConfig {
1500 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1501 f.debug_struct("NamedProviderConfig")
1502 .field("name", &self.name)
1503 .field("provider_type", &self.provider_type)
1504 .field("wire_api", &self.wire_api)
1505 .field("base_url", &self.base_url)
1506 .field("api_key", &self.api_key)
1507 .field("bearer_token", &self.bearer_token)
1508 .field(
1509 "bearer_token_provider",
1510 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1511 )
1512 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1513 .field("azure", &self.azure)
1514 .field("headers", &self.headers)
1515 .finish()
1516 }
1517}
1518
1519impl NamedProviderConfig {
1520 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1523 Self {
1524 name: name.into(),
1525 base_url: base_url.into(),
1526 ..Self::default()
1527 }
1528 }
1529
1530 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1532 self.provider_type = Some(provider_type.into());
1533 self
1534 }
1535
1536 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1538 self.wire_api = Some(wire_api.into());
1539 self
1540 }
1541
1542 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1544 self.api_key = Some(api_key.into());
1545 self
1546 }
1547
1548 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1551 self.bearer_token = Some(bearer_token.into());
1552 self
1553 }
1554
1555 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1561 self.bearer_token_provider = Some(provider);
1562 self
1563 }
1564
1565 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1567 self.azure = Some(azure);
1568 self
1569 }
1570
1571 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1573 self.headers = Some(headers);
1574 self
1575 }
1576}
1577
1578fn prepare_bearer_token_providers(
1579 provider: &mut Option<ProviderConfig>,
1580 providers: &mut Option<Vec<NamedProviderConfig>>,
1581) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1582 let mut bearer_token_providers = HashMap::new();
1583
1584 if let Some(provider) = provider.as_mut()
1585 && let Some(token_provider) = provider.bearer_token_provider.take()
1586 {
1587 provider.has_bearer_token_provider = Some(true);
1588 bearer_token_providers.insert("default".to_string(), token_provider);
1589 }
1590
1591 if let Some(providers) = providers.as_mut() {
1592 for provider in providers {
1593 if let Some(token_provider) = provider.bearer_token_provider.take() {
1594 provider.has_bearer_token_provider = Some(true);
1595 bearer_token_providers.insert(provider.name.clone(), token_provider);
1596 }
1597 }
1598 }
1599
1600 bearer_token_providers
1601}
1602
1603#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1611#[serde(rename_all = "camelCase")]
1612#[non_exhaustive]
1613pub struct ProviderModelConfig {
1614 pub id: String,
1617 pub provider: String,
1619 #[serde(default, skip_serializing_if = "Option::is_none")]
1622 pub wire_model: Option<String>,
1623 #[serde(default, skip_serializing_if = "Option::is_none")]
1626 pub model_id: Option<String>,
1627 #[serde(default, skip_serializing_if = "Option::is_none")]
1629 pub name: Option<String>,
1630 #[serde(default, skip_serializing_if = "Option::is_none")]
1632 pub max_prompt_tokens: Option<i64>,
1633 #[serde(default, skip_serializing_if = "Option::is_none")]
1635 pub max_context_window_tokens: Option<i64>,
1636 #[serde(default, skip_serializing_if = "Option::is_none")]
1638 pub max_output_tokens: Option<i64>,
1639 #[serde(default, skip_serializing_if = "Option::is_none")]
1642 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1643}
1644
1645impl ProviderModelConfig {
1646 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1649 Self {
1650 id: id.into(),
1651 provider: provider.into(),
1652 ..Self::default()
1653 }
1654 }
1655
1656 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1658 self.wire_model = Some(wire_model.into());
1659 self
1660 }
1661
1662 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1665 self.model_id = Some(model_id.into());
1666 self
1667 }
1668
1669 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1671 self.name = Some(name.into());
1672 self
1673 }
1674
1675 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1677 self.max_prompt_tokens = Some(max);
1678 self
1679 }
1680
1681 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1683 self.max_context_window_tokens = Some(max);
1684 self
1685 }
1686
1687 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1689 self.max_output_tokens = Some(max);
1690 self
1691 }
1692
1693 pub fn with_capabilities(
1695 mut self,
1696 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1697 ) -> Self {
1698 self.capabilities = Some(capabilities);
1699 self
1700 }
1701}
1702
1703#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1707#[serde(untagged)]
1708pub enum ExpFlagValue {
1709 Bool(bool),
1711 Integer(i64),
1713 Float(f64),
1715 String(String),
1717 Null,
1719}
1720
1721#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1725#[serde(rename_all = "PascalCase")]
1726pub struct ExpConfigEntry {
1727 pub id: String,
1729 pub parameters: HashMap<String, ExpFlagValue>,
1731}
1732
1733#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1739#[serde(rename_all = "PascalCase")]
1740pub struct CopilotExpAssignmentResponse {
1741 #[serde(default)]
1743 pub features: Vec<String>,
1744 #[serde(default)]
1746 pub flights: HashMap<String, String>,
1747 #[serde(default)]
1749 pub configs: Vec<ExpConfigEntry>,
1750 #[serde(default, skip_serializing_if = "Option::is_none")]
1752 pub parameter_groups: Option<Value>,
1753 #[serde(default, skip_serializing_if = "Option::is_none")]
1755 pub flighting_version: Option<i64>,
1756 #[serde(default, skip_serializing_if = "Option::is_none")]
1758 pub impression_id: Option<String>,
1759 #[serde(default)]
1761 pub assignment_context: String,
1762}
1763
1764pub struct DisableBypassPermissionsModes;
1766
1767impl DisableBypassPermissionsModes {
1768 pub const ALLOW_AUTO_ONLY: &'static str = "allow-auto-only";
1770 pub const DISABLE: &'static str = "disable";
1772}
1773
1774#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1783#[serde(rename_all = "camelCase")]
1784#[non_exhaustive]
1785pub struct ManagedSettingsPermissions {
1786 #[serde(default, skip_serializing_if = "Option::is_none")]
1790 pub disable_bypass_permissions_mode: Option<String>,
1791 #[serde(default, skip_serializing_if = "Option::is_none")]
1793 pub deny: Option<Vec<String>>,
1794 #[serde(default, skip_serializing_if = "Option::is_none")]
1796 pub ask: Option<Vec<String>>,
1797 #[serde(default, skip_serializing_if = "Option::is_none")]
1799 pub allow: Option<Vec<String>>,
1800}
1801
1802impl ManagedSettingsPermissions {
1803 pub fn with_disable_bypass_permissions_mode(mut self, value: impl Into<String>) -> Self {
1805 self.disable_bypass_permissions_mode = Some(value.into());
1806 self
1807 }
1808
1809 pub fn with_deny(mut self, rules: Vec<String>) -> Self {
1811 self.deny = Some(rules);
1812 self
1813 }
1814
1815 pub fn with_ask(mut self, rules: Vec<String>) -> Self {
1817 self.ask = Some(rules);
1818 self
1819 }
1820
1821 pub fn with_allow(mut self, rules: Vec<String>) -> Self {
1823 self.allow = Some(rules);
1824 self
1825 }
1826}
1827
1828#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1838#[serde(rename_all = "camelCase")]
1839#[non_exhaustive]
1840pub struct ManagedSettings {
1841 #[serde(default, skip_serializing_if = "Option::is_none")]
1843 pub permissions: Option<ManagedSettingsPermissions>,
1844}
1845
1846impl ManagedSettings {
1847 pub fn with_permissions(mut self, permissions: ManagedSettingsPermissions) -> Self {
1849 self.permissions = Some(permissions);
1850 self
1851 }
1852}
1853
1854#[derive(Clone)]
1906#[non_exhaustive]
1907pub struct SessionConfig {
1908 pub session_id: Option<SessionId>,
1910 pub model: Option<String>,
1912 pub client_name: Option<String>,
1914 pub reasoning_effort: Option<String>,
1916 pub reasoning_summary: Option<ReasoningSummary>,
1920 pub context_tier: Option<String>,
1923 pub streaming: Option<bool>,
1925 pub system_message: Option<SystemMessageConfig>,
1927 pub tools: Option<Vec<Tool>>,
1929 pub canvases: Option<Vec<CanvasDeclaration>>,
1931 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1936 pub request_canvas_renderer: Option<bool>,
1938 pub request_extensions: Option<bool>,
1940 pub extension_sdk_path: Option<String>,
1944 pub extension_info: Option<ExtensionInfo>,
1946 pub canvas_provider: Option<CanvasProviderIdentity>,
1949 pub available_tools: Option<Vec<String>>,
1951 pub excluded_tools: Option<Vec<String>>,
1953 pub excluded_builtin_agents: Option<Vec<String>>,
1959 pub included_builtin_skills: Option<Vec<String>>,
1963 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
1965 pub mcp_oauth_token_storage: Option<String>,
1974 pub enable_config_discovery: Option<bool>,
1977 pub skip_embedding_retrieval: Option<bool>,
1979 pub embedding_cache_storage: Option<String>,
1982 pub organization_custom_instructions: Option<String>,
1984 pub enable_on_demand_instruction_discovery: Option<bool>,
1986 pub enable_file_hooks: Option<bool>,
1988 pub enable_host_git_operations: Option<bool>,
1990 pub enable_session_store: Option<bool>,
1992 pub enable_skills: Option<bool>,
1994 pub enable_mcp_apps: Option<bool>,
2021 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
2026 pub skill_directories: Option<Vec<PathBuf>>,
2028 pub instruction_directories: Option<Vec<PathBuf>>,
2031 pub plugin_directories: Option<Vec<PathBuf>>,
2033 pub large_output: Option<LargeToolOutputConfig>,
2035 pub tool_search: Option<ToolSearchConfig>,
2039 pub disabled_skills: Option<Vec<String>>,
2042 pub disabled_mcp_servers: Option<Vec<String>>,
2046 pub hooks: Option<bool>,
2050 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2052 pub default_agent: Option<DefaultAgentConfig>,
2056 pub agent: Option<String>,
2059 pub infinite_sessions: Option<InfiniteSessionConfig>,
2062 pub provider: Option<ProviderConfig>,
2066 pub capi: Option<CapiSessionOptions>,
2072 pub providers: Option<Vec<NamedProviderConfig>>,
2079 pub models: Option<Vec<ProviderModelConfig>>,
2085 pub enable_session_telemetry: Option<bool>,
2093 pub enable_citations: Option<bool>,
2095 pub enable_file_change_tracking: Option<bool>,
2098 pub session_limits: Option<SessionLimitsConfig>,
2100 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2103 pub memory: Option<MemoryConfiguration>,
2105 pub config_directory: Option<PathBuf>,
2108 pub working_directory: Option<PathBuf>,
2111 pub additional_directories: Option<Vec<PathBuf>>,
2115 pub github_token: Option<String>,
2121 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2127 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2133 pub cloud: Option<CloudSessionOptions>,
2136 pub include_sub_agent_streaming_events: Option<bool>,
2140 pub commands: Option<Vec<CommandDefinition>>,
2144 #[doc(hidden)]
2151 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
2152 pub enable_managed_settings: Option<bool>,
2160 pub managed_settings: Option<ManagedSettings>,
2169 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2174 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2178 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2181 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2184 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2188 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2191 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2194 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2198 pub(crate) permission_policy: Option<crate::permission::Policy>,
2202 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2207 pub skip_custom_instructions: Option<bool>,
2211 pub custom_agents_local_only: Option<bool>,
2215 pub enable_experimental_mode: Option<bool>,
2220 pub coauthor_enabled: Option<bool>,
2224 pub manage_schedule_enabled: Option<bool>,
2228}
2229
2230impl std::fmt::Debug for SessionConfig {
2231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2232 f.debug_struct("SessionConfig")
2233 .field("session_id", &self.session_id)
2234 .field("model", &self.model)
2235 .field("client_name", &self.client_name)
2236 .field("reasoning_effort", &self.reasoning_effort)
2237 .field("reasoning_summary", &self.reasoning_summary)
2238 .field("context_tier", &self.context_tier)
2239 .field("streaming", &self.streaming)
2240 .field("system_message", &self.system_message)
2241 .field("tools", &self.tools)
2242 .field("canvases", &self.canvases)
2243 .field(
2244 "canvas_handler",
2245 &self.canvas_handler.as_ref().map(|_| "<set>"),
2246 )
2247 .field("request_canvas_renderer", &self.request_canvas_renderer)
2248 .field("request_extensions", &self.request_extensions)
2249 .field("extension_sdk_path", &self.extension_sdk_path)
2250 .field("extension_info", &self.extension_info)
2251 .field("canvas_provider", &self.canvas_provider)
2252 .field("available_tools", &self.available_tools)
2253 .field("excluded_tools", &self.excluded_tools)
2254 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2255 .field("included_builtin_skills", &self.included_builtin_skills)
2256 .field("mcp_servers", &self.mcp_servers)
2257 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2258 .field("embedding_cache_storage", &self.embedding_cache_storage)
2259 .field("enable_config_discovery", &self.enable_config_discovery)
2260 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2261 .field(
2262 "organization_custom_instructions",
2263 &self
2264 .organization_custom_instructions
2265 .as_ref()
2266 .map(|_| "<redacted>"),
2267 )
2268 .field(
2269 "enable_on_demand_instruction_discovery",
2270 &self.enable_on_demand_instruction_discovery,
2271 )
2272 .field("enable_file_hooks", &self.enable_file_hooks)
2273 .field(
2274 "enable_host_git_operations",
2275 &self.enable_host_git_operations,
2276 )
2277 .field("enable_session_store", &self.enable_session_store)
2278 .field("enable_skills", &self.enable_skills)
2279 .field("enable_mcp_apps", &self.enable_mcp_apps)
2280 .field("skill_directories", &self.skill_directories)
2281 .field("instruction_directories", &self.instruction_directories)
2282 .field("plugin_directories", &self.plugin_directories)
2283 .field("large_output", &self.large_output)
2284 .field("tool_search", &self.tool_search)
2285 .field("disabled_skills", &self.disabled_skills)
2286 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
2287 .field("hooks", &self.hooks)
2288 .field("custom_agents", &self.custom_agents)
2289 .field("default_agent", &self.default_agent)
2290 .field("agent", &self.agent)
2291 .field("infinite_sessions", &self.infinite_sessions)
2292 .field("provider", &self.provider)
2293 .field("capi", &self.capi)
2294 .field("enable_session_telemetry", &self.enable_session_telemetry)
2295 .field("enable_citations", &self.enable_citations)
2296 .field(
2297 "enable_file_change_tracking",
2298 &self.enable_file_change_tracking,
2299 )
2300 .field("session_limits", &self.session_limits)
2301 .field("model_capabilities", &self.model_capabilities)
2302 .field("memory", &self.memory)
2303 .field("config_directory", &self.config_directory)
2304 .field("working_directory", &self.working_directory)
2305 .field("additional_directories", &self.additional_directories)
2306 .field(
2307 "github_token",
2308 &self.github_token.as_ref().map(|_| "<redacted>"),
2309 )
2310 .field(
2311 "github_token_provider",
2312 &self.github_token_provider.as_ref().map(|_| "<set>"),
2313 )
2314 .field("remote_session", &self.remote_session)
2315 .field("cloud", &self.cloud)
2316 .field(
2317 "include_sub_agent_streaming_events",
2318 &self.include_sub_agent_streaming_events,
2319 )
2320 .field("commands", &self.commands)
2321 .field("exp_assignments", &self.exp_assignments)
2322 .field("enable_managed_settings", &self.enable_managed_settings)
2323 .field("enable_experimental_mode", &self.enable_experimental_mode)
2324 .field("managed_settings", &self.managed_settings)
2325 .field(
2326 "session_fs_provider",
2327 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2328 )
2329 .field(
2330 "permission_handler",
2331 &self.permission_handler.as_ref().map(|_| "<set>"),
2332 )
2333 .field(
2334 "elicitation_handler",
2335 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2336 )
2337 .field(
2338 "mcp_auth_handler",
2339 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2340 )
2341 .field(
2342 "user_input_handler",
2343 &self.user_input_handler.as_ref().map(|_| "<set>"),
2344 )
2345 .field(
2346 "exit_plan_mode_handler",
2347 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2348 )
2349 .field(
2350 "auto_mode_switch_handler",
2351 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2352 )
2353 .field(
2354 "hooks_handler",
2355 &self.hooks_handler.as_ref().map(|_| "<set>"),
2356 )
2357 .field(
2358 "system_message_transform",
2359 &self.system_message_transform.as_ref().map(|_| "<set>"),
2360 )
2361 .finish()
2362 }
2363}
2364
2365impl Default for SessionConfig {
2366 fn default() -> Self {
2372 Self {
2373 session_id: None,
2374 model: None,
2375 client_name: None,
2376 reasoning_effort: None,
2377 reasoning_summary: None,
2378 context_tier: None,
2379 streaming: None,
2380 system_message: None,
2381 tools: None,
2382 canvases: None,
2383 canvas_handler: None,
2384 request_canvas_renderer: None,
2385 request_extensions: None,
2386 extension_sdk_path: None,
2387 extension_info: None,
2388 canvas_provider: None,
2389 available_tools: None,
2390 excluded_tools: None,
2391 excluded_builtin_agents: None,
2392 included_builtin_skills: None,
2393 mcp_servers: None,
2394 mcp_oauth_token_storage: None,
2395 enable_config_discovery: None,
2396 skip_embedding_retrieval: None,
2397 organization_custom_instructions: None,
2398 enable_on_demand_instruction_discovery: None,
2399 enable_file_hooks: None,
2400 enable_host_git_operations: None,
2401 enable_session_store: None,
2402 enable_skills: None,
2403 embedding_cache_storage: None,
2404 enable_mcp_apps: None,
2405 github_mcp_tool_config: None,
2406 skill_directories: None,
2407 instruction_directories: None,
2408 plugin_directories: None,
2409 large_output: None,
2410 tool_search: None,
2411 disabled_skills: None,
2412 disabled_mcp_servers: None,
2413 hooks: None,
2414 custom_agents: None,
2415 default_agent: None,
2416 agent: None,
2417 infinite_sessions: None,
2418 provider: None,
2419 capi: None,
2420 providers: None,
2421 models: None,
2422 enable_session_telemetry: None,
2423 enable_citations: None,
2424 enable_file_change_tracking: None,
2425 session_limits: None,
2426 model_capabilities: None,
2427 memory: None,
2428 config_directory: None,
2429 working_directory: None,
2430 additional_directories: None,
2431 github_token: None,
2432 github_token_provider: None,
2433 remote_session: None,
2434 cloud: None,
2435 include_sub_agent_streaming_events: None,
2436 commands: None,
2437 exp_assignments: None,
2438 enable_managed_settings: None,
2439 managed_settings: None,
2440 session_fs_provider: None,
2441 permission_handler: None,
2442 elicitation_handler: None,
2443 mcp_auth_handler: None,
2444 user_input_handler: None,
2445 exit_plan_mode_handler: None,
2446 auto_mode_switch_handler: None,
2447 hooks_handler: None,
2448 permission_policy: None,
2449 system_message_transform: None,
2450 skip_custom_instructions: None,
2451 custom_agents_local_only: None,
2452 enable_experimental_mode: None,
2453 coauthor_enabled: None,
2454 manage_schedule_enabled: None,
2455 }
2456 }
2457}
2458
2459pub(crate) struct SessionConfigRuntime {
2465 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2466 pub permission_policy: Option<crate::permission::Policy>,
2467 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2468 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2469 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2470 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2471 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2472 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2473 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2474 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2475 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2476 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2477 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2478 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2479 pub commands: Option<Vec<CommandDefinition>>,
2480}
2481
2482impl SessionConfig {
2483 pub(crate) fn into_wire(
2495 mut self,
2496 session_id: Option<SessionId>,
2497 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2498 if self.github_token.is_some() && self.github_token_provider.is_some() {
2499 return Err(crate::Error::with_message(
2500 crate::ErrorKind::InvalidConfig,
2501 "github_token and github_token_provider are mutually exclusive",
2502 ));
2503 }
2504 let permission_active =
2505 self.permission_handler.is_some() || self.permission_policy.is_some();
2506 let request_user_input = self.user_input_handler.is_some();
2507 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2508 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2509 let request_elicitation = self.elicitation_handler.is_some();
2510 let hooks_flag = self.hooks_handler.is_some();
2511
2512 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2513 if let Some(tools) = self.tools.as_mut() {
2514 for tool in tools.iter_mut() {
2515 if let Some(handler) = tool.handler.take()
2516 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2517 {
2518 return Err(crate::Error::with_message(
2519 crate::ErrorKind::InvalidConfig,
2520 format!("duplicate tool handler registered for name {:?}", tool.name),
2521 ));
2522 }
2523 }
2524 }
2525
2526 let wire_commands = self.commands.as_ref().map(|cmds| {
2527 cmds.iter()
2528 .map(|c| crate::wire::CommandWireDefinition {
2529 name: c.name.clone(),
2530 description: c.description.clone().unwrap_or_default(),
2531 })
2532 .collect()
2533 });
2534 let wire_canvases = self.canvases.clone();
2535 let canvas_handler = self.canvas_handler.clone();
2536 let bearer_token_providers =
2537 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2538
2539 let wire = crate::wire::SessionCreateWire {
2540 session_id,
2541 model: self.model,
2542 client_name: self.client_name,
2543 reasoning_effort: self.reasoning_effort,
2544 reasoning_summary: self.reasoning_summary,
2545 context_tier: self.context_tier,
2546 streaming: self.streaming,
2547 system_message: self.system_message,
2548 tools: self.tools,
2549 canvases: wire_canvases,
2550 request_canvas_renderer: self.request_canvas_renderer,
2551 request_extensions: self.request_extensions,
2552 extension_sdk_path: self.extension_sdk_path,
2553 extension_info: self.extension_info,
2554 canvas_provider: self.canvas_provider,
2555 available_tools: self.available_tools,
2556 excluded_tools: self.excluded_tools,
2557 excluded_builtin_agents: self.excluded_builtin_agents,
2558 tool_filter_precedence: "excluded",
2559 mcp_servers: self.mcp_servers,
2560 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2561 embedding_cache_storage: self.embedding_cache_storage,
2562 env_value_mode: "direct",
2563 enable_config_discovery: self.enable_config_discovery,
2564 skip_embedding_retrieval: self.skip_embedding_retrieval,
2565 organization_custom_instructions: self.organization_custom_instructions,
2566 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2567 enable_file_hooks: self.enable_file_hooks,
2568 enable_host_git_operations: self.enable_host_git_operations,
2569 enable_session_store: self.enable_session_store,
2570 enable_skills: self.enable_skills,
2571 request_user_input,
2572 request_permission: permission_active,
2573 request_exit_plan_mode,
2574 request_auto_mode_switch,
2575 request_elicitation,
2576 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2577 github_mcp_tool_config: self.github_mcp_tool_config,
2578 hooks: hooks_flag,
2579 skill_directories: self.skill_directories,
2580 instruction_directories: self.instruction_directories,
2581 plugin_directories: self.plugin_directories,
2582 large_output: self.large_output,
2583 tool_search: self.tool_search,
2584 disabled_skills: self.disabled_skills,
2585 disabled_mcp_servers: self.disabled_mcp_servers,
2586 custom_agents: self.custom_agents,
2587 custom_agents_local_only: self.custom_agents_local_only,
2588 default_agent: self.default_agent,
2589 agent: self.agent,
2590 infinite_sessions: self.infinite_sessions,
2591 provider: self.provider,
2592 capi: self.capi,
2593 providers: self.providers,
2594 models: self.models,
2595 enable_session_telemetry: self.enable_session_telemetry,
2596 enable_citations: self.enable_citations,
2597 enable_file_change_tracking: self.enable_file_change_tracking,
2598 session_limits: self.session_limits,
2599 model_capabilities: self.model_capabilities,
2600 memory: self.memory,
2601 config_dir: self.config_directory,
2602 working_directory: self.working_directory,
2603 additional_directories: self.additional_directories,
2604 github_token: self.github_token,
2605 github_token_provider_registration_id: None,
2606 remote_session: self.remote_session,
2607 cloud: self.cloud,
2608 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2609 enable_github_telemetry_forwarding: None,
2610 commands: wire_commands,
2611 exp_assignments: self.exp_assignments,
2612 enable_managed_settings: self.enable_managed_settings,
2613 is_experimental_mode: self.enable_experimental_mode,
2614 managed_settings: self.managed_settings,
2615 };
2616
2617 let runtime = SessionConfigRuntime {
2618 permission_handler: self.permission_handler,
2619 permission_policy: self.permission_policy,
2620 elicitation_handler: self.elicitation_handler,
2621 mcp_auth_handler: self.mcp_auth_handler,
2622 user_input_handler: self.user_input_handler,
2623 exit_plan_mode_handler: self.exit_plan_mode_handler,
2624 auto_mode_switch_handler: self.auto_mode_switch_handler,
2625 hooks_handler: self.hooks_handler,
2626 system_message_transform: self.system_message_transform,
2627 tool_handlers,
2628 canvas_handler,
2629 session_fs_provider: self.session_fs_provider,
2630 bearer_token_providers,
2631 github_token_provider: self.github_token_provider,
2632 commands: self.commands,
2633 };
2634
2635 Ok((wire, runtime))
2636 }
2637
2638 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2642 self.permission_handler = Some(handler);
2643 self
2644 }
2645
2646 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2649 self.elicitation_handler = Some(handler);
2650 self
2651 }
2652
2653 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2655 self.mcp_auth_handler = Some(handler);
2656 self
2657 }
2658
2659 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2662 self.user_input_handler = Some(handler);
2663 self
2664 }
2665
2666 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2668 self.exit_plan_mode_handler = Some(handler);
2669 self
2670 }
2671
2672 pub fn with_auto_mode_switch_handler(
2674 mut self,
2675 handler: Arc<dyn AutoModeSwitchHandler>,
2676 ) -> Self {
2677 self.auto_mode_switch_handler = Some(handler);
2678 self
2679 }
2680
2681 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2686 self.commands = Some(commands);
2687 self
2688 }
2689
2690 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2694 self.session_fs_provider = Some(provider);
2695 self
2696 }
2697
2698 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2701 self.hooks_handler = Some(hooks);
2702 self
2703 }
2704
2705 pub fn with_system_message_transform(
2709 mut self,
2710 transform: Arc<dyn SystemMessageTransform>,
2711 ) -> Self {
2712 self.system_message_transform = Some(transform);
2713 self
2714 }
2715
2716 pub fn approve_all_permissions(mut self) -> Self {
2722 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2723 self
2724 }
2725
2726 pub fn deny_all_permissions(mut self) -> Self {
2729 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2730 self
2731 }
2732
2733 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2738 where
2739 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2740 {
2741 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2742 self
2743 }
2744
2745 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2747 self.session_id = Some(id.into());
2748 self
2749 }
2750
2751 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2753 self.model = Some(model.into());
2754 self
2755 }
2756
2757 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2759 self.client_name = Some(name.into());
2760 self
2761 }
2762
2763 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2765 self.reasoning_effort = Some(effort.into());
2766 self
2767 }
2768
2769 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2771 self.reasoning_summary = Some(summary);
2772 self
2773 }
2774
2775 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2777 self.context_tier = Some(tier.into());
2778 self
2779 }
2780
2781 pub fn with_streaming(mut self, streaming: bool) -> Self {
2783 self.streaming = Some(streaming);
2784 self
2785 }
2786
2787 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2789 self.system_message = Some(system_message);
2790 self
2791 }
2792
2793 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2795 self.tools = Some(tools.into_iter().collect());
2796 self
2797 }
2798
2799 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2804 self.canvases = Some(canvases.into_iter().collect());
2805 self
2806 }
2807
2808 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2810 self.canvas_handler = Some(handler);
2811 self
2812 }
2813
2814 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2816 self.request_canvas_renderer = Some(request);
2817 self
2818 }
2819
2820 pub fn with_request_extensions(mut self, request: bool) -> Self {
2822 self.request_extensions = Some(request);
2823 self
2824 }
2825
2826 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2830 self.extension_sdk_path = Some(path.into());
2831 self
2832 }
2833
2834 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2836 self.extension_info = Some(extension_info);
2837 self
2838 }
2839
2840 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2843 self.canvas_provider = Some(canvas_provider);
2844 self
2845 }
2846
2847 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2849 where
2850 I: IntoIterator<Item = S>,
2851 S: Into<String>,
2852 {
2853 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2854 self
2855 }
2856
2857 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2859 where
2860 I: IntoIterator<Item = S>,
2861 S: Into<String>,
2862 {
2863 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2864 self
2865 }
2866
2867 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2869 where
2870 I: IntoIterator<Item = S>,
2871 S: Into<String>,
2872 {
2873 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2874 self
2875 }
2876
2877 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2879 self.mcp_servers = Some(servers);
2880 self
2881 }
2882
2883 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2891 self.mcp_oauth_token_storage = Some(mode.into());
2892 self
2893 }
2894
2895 pub fn with_embedding_cache_storage(
2897 mut self,
2898 embedding_cache_storage: impl Into<String>,
2899 ) -> Self {
2900 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2901 self
2902 }
2903
2904 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2907 self.enable_config_discovery = Some(enable);
2908 self
2909 }
2910
2911 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2913 self.skip_embedding_retrieval = Some(value);
2914 self
2915 }
2916
2917 pub fn with_organization_custom_instructions(
2919 mut self,
2920 instructions: impl Into<String>,
2921 ) -> Self {
2922 self.organization_custom_instructions = Some(instructions.into());
2923 self
2924 }
2925
2926 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2928 self.enable_on_demand_instruction_discovery = Some(value);
2929 self
2930 }
2931
2932 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2934 self.enable_file_hooks = Some(value);
2935 self
2936 }
2937
2938 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2940 self.enable_host_git_operations = Some(value);
2941 self
2942 }
2943
2944 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2946 self.enable_session_store = Some(value);
2947 self
2948 }
2949
2950 pub fn with_enable_skills(mut self, value: bool) -> Self {
2952 self.enable_skills = Some(value);
2953 self
2954 }
2955
2956 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2962 self.enable_mcp_apps = Some(enable);
2963 self
2964 }
2965
2966 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
2968 self.github_mcp_tool_config = Some(config);
2969 self
2970 }
2971
2972 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2974 where
2975 I: IntoIterator<Item = P>,
2976 P: Into<PathBuf>,
2977 {
2978 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2979 self
2980 }
2981
2982 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
2984 where
2985 I: IntoIterator<Item = S>,
2986 S: Into<String>,
2987 {
2988 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
2989 self
2990 }
2991
2992 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2996 where
2997 I: IntoIterator<Item = P>,
2998 P: Into<PathBuf>,
2999 {
3000 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3001 self
3002 }
3003
3004 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3006 where
3007 I: IntoIterator<Item = P>,
3008 P: Into<PathBuf>,
3009 {
3010 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3011 self
3012 }
3013
3014 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3016 self.large_output = Some(config);
3017 self
3018 }
3019
3020 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
3023 self.tool_search = Some(config);
3024 self
3025 }
3026
3027 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3029 where
3030 I: IntoIterator<Item = S>,
3031 S: Into<String>,
3032 {
3033 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3034 self
3035 }
3036
3037 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
3039 where
3040 I: IntoIterator<Item = S>,
3041 S: Into<String>,
3042 {
3043 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
3044 self
3045 }
3046
3047 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3049 mut self,
3050 agents: I,
3051 ) -> Self {
3052 self.custom_agents = Some(agents.into_iter().collect());
3053 self
3054 }
3055
3056 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3058 self.default_agent = Some(agent);
3059 self
3060 }
3061
3062 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3065 self.agent = Some(name.into());
3066 self
3067 }
3068
3069 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3072 self.infinite_sessions = Some(config);
3073 self
3074 }
3075
3076 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3078 self.provider = Some(provider);
3079 self
3080 }
3081
3082 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3084 self.capi = Some(capi);
3085 self
3086 }
3087
3088 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3094 self.providers = Some(providers);
3095 self
3096 }
3097
3098 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3104 self.models = Some(models);
3105 self
3106 }
3107
3108 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3112 self.enable_session_telemetry = Some(enable);
3113 self
3114 }
3115
3116 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3118 self.enable_citations = Some(enable);
3119 self
3120 }
3121
3122 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
3125 self.enable_file_change_tracking = Some(enable);
3126 self
3127 }
3128
3129 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3131 self.session_limits = Some(limits);
3132 self
3133 }
3134
3135 pub fn with_model_capabilities(
3137 mut self,
3138 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3139 ) -> Self {
3140 self.model_capabilities = Some(capabilities);
3141 self
3142 }
3143
3144 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3146 self.memory = Some(memory);
3147 self
3148 }
3149
3150 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3152 self.config_directory = Some(dir.into());
3153 self
3154 }
3155
3156 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3159 self.working_directory = Some(dir.into());
3160 self
3161 }
3162
3163 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
3165 where
3166 I: IntoIterator<Item = P>,
3167 P: Into<PathBuf>,
3168 {
3169 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
3170 self
3171 }
3172
3173 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3178 self.github_token = Some(token.into());
3179 self
3180 }
3181
3182 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
3188 self.github_token_provider = Some(provider);
3189 self
3190 }
3191
3192 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3195 self.include_sub_agent_streaming_events = Some(include);
3196 self
3197 }
3198
3199 pub fn with_remote_session(
3201 mut self,
3202 mode: crate::generated::api_types::RemoteSessionMode,
3203 ) -> Self {
3204 self.remote_session = Some(mode);
3205 self
3206 }
3207
3208 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
3210 self.cloud = Some(cloud);
3211 self
3212 }
3213
3214 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3216 self.skip_custom_instructions = Some(value);
3217 self
3218 }
3219
3220 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3222 self.custom_agents_local_only = Some(value);
3223 self
3224 }
3225
3226 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
3228 self.enable_experimental_mode = Some(enable_experimental_mode);
3229 self
3230 }
3231
3232 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3234 self.coauthor_enabled = Some(value);
3235 self
3236 }
3237
3238 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3240 self.manage_schedule_enabled = Some(value);
3241 self
3242 }
3243
3244 #[doc(hidden)]
3252 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
3253 self.exp_assignments = Some(assignments);
3254 self
3255 }
3256
3257 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3264 self.enable_managed_settings = Some(enabled);
3265 self
3266 }
3267
3268 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
3273 self.managed_settings = Some(managed_settings);
3274 self
3275 }
3276}
3277#[derive(Clone)]
3284#[non_exhaustive]
3285pub struct ResumeSessionConfig {
3286 pub session_id: SessionId,
3288 pub model: Option<String>,
3291 pub client_name: Option<String>,
3293 pub reasoning_effort: Option<String>,
3295 pub reasoning_summary: Option<ReasoningSummary>,
3299 pub context_tier: Option<String>,
3302 pub streaming: Option<bool>,
3304 pub system_message: Option<SystemMessageConfig>,
3307 pub tools: Option<Vec<Tool>>,
3309 pub canvases: Option<Vec<CanvasDeclaration>>,
3311 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
3314 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3316 pub request_canvas_renderer: Option<bool>,
3318 pub request_extensions: Option<bool>,
3320 pub extension_sdk_path: Option<String>,
3324 pub extension_info: Option<ExtensionInfo>,
3326 pub canvas_provider: Option<CanvasProviderIdentity>,
3329 pub available_tools: Option<Vec<String>>,
3331 pub excluded_tools: Option<Vec<String>>,
3333 pub excluded_builtin_agents: Option<Vec<String>>,
3339 pub included_builtin_skills: Option<Vec<String>>,
3343 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3345 pub mcp_oauth_token_storage: Option<String>,
3348 pub enable_config_discovery: Option<bool>,
3351 pub skip_embedding_retrieval: Option<bool>,
3353 pub embedding_cache_storage: Option<String>,
3355 pub organization_custom_instructions: Option<String>,
3357 pub enable_on_demand_instruction_discovery: Option<bool>,
3359 pub enable_file_hooks: Option<bool>,
3361 pub enable_host_git_operations: Option<bool>,
3363 pub enable_session_store: Option<bool>,
3365 pub enable_skills: Option<bool>,
3367 pub enable_mcp_apps: Option<bool>,
3373 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
3378 pub skill_directories: Option<Vec<PathBuf>>,
3380 pub instruction_directories: Option<Vec<PathBuf>>,
3383 pub plugin_directories: Option<Vec<PathBuf>>,
3385 pub large_output: Option<LargeToolOutputConfig>,
3387 pub tool_search: Option<ToolSearchConfig>,
3390 pub disabled_skills: Option<Vec<String>>,
3392 pub disabled_mcp_servers: Option<Vec<String>>,
3395 pub hooks: Option<bool>,
3397 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3399 pub default_agent: Option<DefaultAgentConfig>,
3401 pub agent: Option<String>,
3403 pub infinite_sessions: Option<InfiniteSessionConfig>,
3405 pub provider: Option<ProviderConfig>,
3407 pub capi: Option<CapiSessionOptions>,
3413 pub providers: Option<Vec<NamedProviderConfig>>,
3419 pub models: Option<Vec<ProviderModelConfig>>,
3425 pub enable_session_telemetry: Option<bool>,
3433 pub enable_citations: Option<bool>,
3435 pub enable_file_change_tracking: Option<bool>,
3439 pub session_limits: Option<SessionLimitsConfig>,
3441 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3443 pub memory: Option<MemoryConfiguration>,
3445 pub config_directory: Option<PathBuf>,
3447 pub working_directory: Option<PathBuf>,
3449 pub additional_directories: Option<Vec<PathBuf>>,
3452 pub github_token: Option<String>,
3455 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
3458 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3461 pub include_sub_agent_streaming_events: Option<bool>,
3463 pub commands: Option<Vec<CommandDefinition>>,
3467 #[doc(hidden)]
3472 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3473 pub enable_managed_settings: Option<bool>,
3479 pub managed_settings: Option<ManagedSettings>,
3485 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3490 pub suppress_resume_event: Option<bool>,
3493 pub continue_pending_work: Option<bool>,
3501 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3504 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3507 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3509 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3512 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3515 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3518 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3520 pub(crate) permission_policy: Option<crate::permission::Policy>,
3522 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3524 pub skip_custom_instructions: Option<bool>,
3526 pub custom_agents_local_only: Option<bool>,
3528 pub enable_experimental_mode: Option<bool>,
3533 pub coauthor_enabled: Option<bool>,
3535 pub manage_schedule_enabled: Option<bool>,
3537}
3538
3539impl std::fmt::Debug for ResumeSessionConfig {
3540 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3541 f.debug_struct("ResumeSessionConfig")
3542 .field("session_id", &self.session_id)
3543 .field("model", &self.model)
3544 .field("client_name", &self.client_name)
3545 .field("reasoning_effort", &self.reasoning_effort)
3546 .field("reasoning_summary", &self.reasoning_summary)
3547 .field("context_tier", &self.context_tier)
3548 .field("streaming", &self.streaming)
3549 .field("system_message", &self.system_message)
3550 .field("tools", &self.tools)
3551 .field("canvases", &self.canvases)
3552 .field(
3553 "canvas_handler",
3554 &self.canvas_handler.as_ref().map(|_| "<set>"),
3555 )
3556 .field("open_canvases", &self.open_canvases)
3557 .field("request_canvas_renderer", &self.request_canvas_renderer)
3558 .field("request_extensions", &self.request_extensions)
3559 .field("extension_sdk_path", &self.extension_sdk_path)
3560 .field("extension_info", &self.extension_info)
3561 .field("canvas_provider", &self.canvas_provider)
3562 .field("available_tools", &self.available_tools)
3563 .field("excluded_tools", &self.excluded_tools)
3564 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3565 .field("included_builtin_skills", &self.included_builtin_skills)
3566 .field("mcp_servers", &self.mcp_servers)
3567 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3568 .field("embedding_cache_storage", &self.embedding_cache_storage)
3569 .field("enable_config_discovery", &self.enable_config_discovery)
3570 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3571 .field(
3572 "organization_custom_instructions",
3573 &self
3574 .organization_custom_instructions
3575 .as_ref()
3576 .map(|_| "<redacted>"),
3577 )
3578 .field(
3579 "enable_on_demand_instruction_discovery",
3580 &self.enable_on_demand_instruction_discovery,
3581 )
3582 .field("enable_file_hooks", &self.enable_file_hooks)
3583 .field(
3584 "enable_host_git_operations",
3585 &self.enable_host_git_operations,
3586 )
3587 .field("enable_session_store", &self.enable_session_store)
3588 .field("enable_skills", &self.enable_skills)
3589 .field("enable_mcp_apps", &self.enable_mcp_apps)
3590 .field("skill_directories", &self.skill_directories)
3591 .field("instruction_directories", &self.instruction_directories)
3592 .field("plugin_directories", &self.plugin_directories)
3593 .field("large_output", &self.large_output)
3594 .field("tool_search", &self.tool_search)
3595 .field("disabled_skills", &self.disabled_skills)
3596 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
3597 .field("hooks", &self.hooks)
3598 .field("custom_agents", &self.custom_agents)
3599 .field("default_agent", &self.default_agent)
3600 .field("agent", &self.agent)
3601 .field("infinite_sessions", &self.infinite_sessions)
3602 .field("provider", &self.provider)
3603 .field("capi", &self.capi)
3604 .field("enable_session_telemetry", &self.enable_session_telemetry)
3605 .field("enable_citations", &self.enable_citations)
3606 .field(
3607 "enable_file_change_tracking",
3608 &self.enable_file_change_tracking,
3609 )
3610 .field("session_limits", &self.session_limits)
3611 .field("model_capabilities", &self.model_capabilities)
3612 .field("memory", &self.memory)
3613 .field("config_directory", &self.config_directory)
3614 .field("working_directory", &self.working_directory)
3615 .field("additional_directories", &self.additional_directories)
3616 .field(
3617 "github_token",
3618 &self.github_token.as_ref().map(|_| "<redacted>"),
3619 )
3620 .field(
3621 "github_token_provider",
3622 &self.github_token_provider.as_ref().map(|_| "<set>"),
3623 )
3624 .field("remote_session", &self.remote_session)
3625 .field(
3626 "include_sub_agent_streaming_events",
3627 &self.include_sub_agent_streaming_events,
3628 )
3629 .field("commands", &self.commands)
3630 .field("exp_assignments", &self.exp_assignments)
3631 .field("enable_managed_settings", &self.enable_managed_settings)
3632 .field("enable_experimental_mode", &self.enable_experimental_mode)
3633 .field("managed_settings", &self.managed_settings)
3634 .field(
3635 "session_fs_provider",
3636 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3637 )
3638 .field(
3639 "permission_handler",
3640 &self.permission_handler.as_ref().map(|_| "<set>"),
3641 )
3642 .field(
3643 "elicitation_handler",
3644 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3645 )
3646 .field(
3647 "user_input_handler",
3648 &self.user_input_handler.as_ref().map(|_| "<set>"),
3649 )
3650 .field(
3651 "exit_plan_mode_handler",
3652 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3653 )
3654 .field(
3655 "auto_mode_switch_handler",
3656 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3657 )
3658 .field(
3659 "hooks_handler",
3660 &self.hooks_handler.as_ref().map(|_| "<set>"),
3661 )
3662 .field(
3663 "system_message_transform",
3664 &self.system_message_transform.as_ref().map(|_| "<set>"),
3665 )
3666 .field("suppress_resume_event", &self.suppress_resume_event)
3667 .field("continue_pending_work", &self.continue_pending_work)
3668 .finish()
3669 }
3670}
3671
3672impl ResumeSessionConfig {
3673 pub(crate) fn into_wire(
3681 mut self,
3682 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3683 if self.github_token.is_some() && self.github_token_provider.is_some() {
3684 return Err(crate::Error::with_message(
3685 crate::ErrorKind::InvalidConfig,
3686 "github_token and github_token_provider are mutually exclusive",
3687 ));
3688 }
3689 let permission_active =
3690 self.permission_handler.is_some() || self.permission_policy.is_some();
3691 let request_user_input = self.user_input_handler.is_some();
3692 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3693 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3694 let request_elicitation = self.elicitation_handler.is_some();
3695 let hooks_flag = self.hooks_handler.is_some();
3696
3697 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3698 if let Some(tools) = self.tools.as_mut() {
3699 for tool in tools.iter_mut() {
3700 if let Some(handler) = tool.handler.take()
3701 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3702 {
3703 return Err(crate::Error::with_message(
3704 crate::ErrorKind::InvalidConfig,
3705 format!("duplicate tool handler registered for name {:?}", tool.name),
3706 ));
3707 }
3708 }
3709 }
3710
3711 let wire_commands = self.commands.as_ref().map(|cmds| {
3712 cmds.iter()
3713 .map(|c| crate::wire::CommandWireDefinition {
3714 name: c.name.clone(),
3715 description: c.description.clone().unwrap_or_default(),
3716 })
3717 .collect()
3718 });
3719 let wire_canvases = self.canvases.clone();
3720 let canvas_handler = self.canvas_handler.clone();
3721 let bearer_token_providers =
3722 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3723
3724 let wire = crate::wire::SessionResumeWire {
3725 session_id: self.session_id,
3726 model: self.model,
3727 client_name: self.client_name,
3728 reasoning_effort: self.reasoning_effort,
3729 reasoning_summary: self.reasoning_summary,
3730 context_tier: self.context_tier,
3731 streaming: self.streaming,
3732 system_message: self.system_message,
3733 tools: self.tools,
3734 canvases: wire_canvases,
3735 open_canvases: self.open_canvases,
3736 request_canvas_renderer: self.request_canvas_renderer,
3737 request_extensions: self.request_extensions,
3738 extension_sdk_path: self.extension_sdk_path,
3739 extension_info: self.extension_info,
3740 canvas_provider: self.canvas_provider,
3741 available_tools: self.available_tools,
3742 excluded_tools: self.excluded_tools,
3743 excluded_builtin_agents: self.excluded_builtin_agents,
3744 tool_filter_precedence: "excluded",
3745 mcp_servers: self.mcp_servers,
3746 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3747 embedding_cache_storage: self.embedding_cache_storage,
3748 env_value_mode: "direct",
3749 enable_config_discovery: self.enable_config_discovery,
3750 skip_embedding_retrieval: self.skip_embedding_retrieval,
3751 organization_custom_instructions: self.organization_custom_instructions,
3752 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3753 enable_file_hooks: self.enable_file_hooks,
3754 enable_host_git_operations: self.enable_host_git_operations,
3755 enable_session_store: self.enable_session_store,
3756 enable_skills: self.enable_skills,
3757 request_user_input,
3758 request_permission: permission_active,
3759 request_exit_plan_mode,
3760 request_auto_mode_switch,
3761 request_elicitation,
3762 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3763 github_mcp_tool_config: self.github_mcp_tool_config,
3764 hooks: hooks_flag,
3765 skill_directories: self.skill_directories,
3766 instruction_directories: self.instruction_directories,
3767 plugin_directories: self.plugin_directories,
3768 large_output: self.large_output,
3769 tool_search: self.tool_search,
3770 disabled_skills: self.disabled_skills,
3771 disabled_mcp_servers: self.disabled_mcp_servers,
3772 custom_agents: self.custom_agents,
3773 custom_agents_local_only: self.custom_agents_local_only,
3774 default_agent: self.default_agent,
3775 agent: self.agent,
3776 infinite_sessions: self.infinite_sessions,
3777 provider: self.provider,
3778 capi: self.capi,
3779 providers: self.providers,
3780 models: self.models,
3781 enable_session_telemetry: self.enable_session_telemetry,
3782 enable_citations: self.enable_citations,
3783 enable_file_change_tracking: self.enable_file_change_tracking,
3784 session_limits: self.session_limits,
3785 model_capabilities: self.model_capabilities,
3786 memory: self.memory,
3787 config_dir: self.config_directory,
3788 working_directory: self.working_directory,
3789 additional_directories: self.additional_directories,
3790 github_token: self.github_token,
3791 github_token_provider_registration_id: None,
3792 remote_session: self.remote_session,
3793 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3794 enable_github_telemetry_forwarding: None,
3795 commands: wire_commands,
3796 exp_assignments: self.exp_assignments,
3797 enable_managed_settings: self.enable_managed_settings,
3798 is_experimental_mode: self.enable_experimental_mode,
3799 managed_settings: self.managed_settings,
3800 suppress_resume_event: self.suppress_resume_event,
3801 continue_pending_work: self.continue_pending_work,
3802 };
3803
3804 let runtime = SessionConfigRuntime {
3805 permission_handler: self.permission_handler,
3806 permission_policy: self.permission_policy,
3807 elicitation_handler: self.elicitation_handler,
3808 mcp_auth_handler: self.mcp_auth_handler,
3809 user_input_handler: self.user_input_handler,
3810 exit_plan_mode_handler: self.exit_plan_mode_handler,
3811 auto_mode_switch_handler: self.auto_mode_switch_handler,
3812 hooks_handler: self.hooks_handler,
3813 system_message_transform: self.system_message_transform,
3814 tool_handlers,
3815 canvas_handler,
3816 session_fs_provider: self.session_fs_provider,
3817 bearer_token_providers,
3818 github_token_provider: self.github_token_provider,
3819 commands: self.commands,
3820 };
3821
3822 Ok((wire, runtime))
3823 }
3824
3825 pub fn new(session_id: SessionId) -> Self {
3830 Self {
3831 session_id,
3832 model: None,
3833 client_name: None,
3834 reasoning_effort: None,
3835 reasoning_summary: None,
3836 context_tier: None,
3837 streaming: None,
3838 system_message: None,
3839 tools: None,
3840 canvases: None,
3841 canvas_handler: None,
3842 open_canvases: None,
3843 request_canvas_renderer: None,
3844 request_extensions: None,
3845 extension_sdk_path: None,
3846 extension_info: None,
3847 canvas_provider: None,
3848 available_tools: None,
3849 excluded_tools: None,
3850 excluded_builtin_agents: None,
3851 included_builtin_skills: None,
3852 mcp_servers: None,
3853 mcp_oauth_token_storage: None,
3854 enable_config_discovery: None,
3855 skip_embedding_retrieval: None,
3856 organization_custom_instructions: None,
3857 enable_on_demand_instruction_discovery: None,
3858 enable_file_hooks: None,
3859 enable_host_git_operations: None,
3860 enable_session_store: None,
3861 enable_skills: None,
3862 embedding_cache_storage: None,
3863 enable_mcp_apps: None,
3864 github_mcp_tool_config: None,
3865 skill_directories: None,
3866 instruction_directories: None,
3867 plugin_directories: None,
3868 large_output: None,
3869 tool_search: None,
3870 disabled_skills: None,
3871 disabled_mcp_servers: None,
3872 hooks: None,
3873 custom_agents: None,
3874 default_agent: None,
3875 agent: None,
3876 infinite_sessions: None,
3877 provider: None,
3878 capi: None,
3879 providers: None,
3880 models: None,
3881 enable_session_telemetry: None,
3882 enable_citations: None,
3883 enable_file_change_tracking: None,
3884 session_limits: None,
3885 model_capabilities: None,
3886 memory: None,
3887 config_directory: None,
3888 working_directory: None,
3889 additional_directories: None,
3890 github_token: None,
3891 github_token_provider: None,
3892 remote_session: None,
3893 include_sub_agent_streaming_events: None,
3894 commands: None,
3895 exp_assignments: None,
3896 enable_managed_settings: None,
3897 managed_settings: None,
3898 session_fs_provider: None,
3899 suppress_resume_event: None,
3900 continue_pending_work: None,
3901 permission_handler: None,
3902 elicitation_handler: None,
3903 mcp_auth_handler: None,
3904 user_input_handler: None,
3905 exit_plan_mode_handler: None,
3906 auto_mode_switch_handler: None,
3907 hooks_handler: None,
3908 permission_policy: None,
3909 system_message_transform: None,
3910 skip_custom_instructions: None,
3911 custom_agents_local_only: None,
3912 enable_experimental_mode: None,
3913 coauthor_enabled: None,
3914 manage_schedule_enabled: None,
3915 }
3916 }
3917
3918 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3920 self.permission_handler = Some(handler);
3921 self
3922 }
3923
3924 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3926 self.elicitation_handler = Some(handler);
3927 self
3928 }
3929
3930 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3932 self.mcp_auth_handler = Some(handler);
3933 self
3934 }
3935
3936 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3938 self.user_input_handler = Some(handler);
3939 self
3940 }
3941
3942 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3944 self.exit_plan_mode_handler = Some(handler);
3945 self
3946 }
3947
3948 pub fn with_auto_mode_switch_handler(
3950 mut self,
3951 handler: Arc<dyn AutoModeSwitchHandler>,
3952 ) -> Self {
3953 self.auto_mode_switch_handler = Some(handler);
3954 self
3955 }
3956
3957 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3960 self.hooks_handler = Some(hooks);
3961 self
3962 }
3963
3964 pub fn with_system_message_transform(
3966 mut self,
3967 transform: Arc<dyn SystemMessageTransform>,
3968 ) -> Self {
3969 self.system_message_transform = Some(transform);
3970 self
3971 }
3972
3973 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3977 self.commands = Some(commands);
3978 self
3979 }
3980
3981 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3984 self.session_fs_provider = Some(provider);
3985 self
3986 }
3987
3988 pub fn approve_all_permissions(mut self) -> Self {
3991 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3992 self
3993 }
3994
3995 pub fn deny_all_permissions(mut self) -> Self {
3998 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3999 self
4000 }
4001
4002 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
4005 where
4006 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
4007 {
4008 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
4009 self
4010 }
4011
4012 pub fn with_model(mut self, model: impl Into<String>) -> Self {
4014 self.model = Some(model.into());
4015 self
4016 }
4017
4018 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
4020 self.client_name = Some(name.into());
4021 self
4022 }
4023
4024 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4026 self.reasoning_effort = Some(effort.into());
4027 self
4028 }
4029
4030 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4032 self.reasoning_summary = Some(summary);
4033 self
4034 }
4035
4036 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
4039 self.context_tier = Some(tier.into());
4040 self
4041 }
4042
4043 pub fn with_streaming(mut self, streaming: bool) -> Self {
4045 self.streaming = Some(streaming);
4046 self
4047 }
4048
4049 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
4052 self.system_message = Some(system_message);
4053 self
4054 }
4055
4056 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
4058 self.tools = Some(tools.into_iter().collect());
4059 self
4060 }
4061
4062 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
4064 self.canvases = Some(canvases.into_iter().collect());
4065 self
4066 }
4067
4068 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
4070 self.canvas_handler = Some(handler);
4071 self
4072 }
4073
4074 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
4076 mut self,
4077 open_canvases: I,
4078 ) -> Self {
4079 self.open_canvases = Some(open_canvases.into_iter().collect());
4080 self
4081 }
4082
4083 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
4085 self.request_canvas_renderer = Some(request);
4086 self
4087 }
4088
4089 pub fn with_request_extensions(mut self, request: bool) -> Self {
4091 self.request_extensions = Some(request);
4092 self
4093 }
4094
4095 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
4099 self.extension_sdk_path = Some(path.into());
4100 self
4101 }
4102
4103 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
4105 self.extension_info = Some(extension_info);
4106 self
4107 }
4108
4109 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
4112 self.canvas_provider = Some(canvas_provider);
4113 self
4114 }
4115
4116 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
4118 where
4119 I: IntoIterator<Item = S>,
4120 S: Into<String>,
4121 {
4122 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
4123 self
4124 }
4125
4126 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
4128 where
4129 I: IntoIterator<Item = S>,
4130 S: Into<String>,
4131 {
4132 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
4133 self
4134 }
4135
4136 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
4138 where
4139 I: IntoIterator<Item = S>,
4140 S: Into<String>,
4141 {
4142 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
4143 self
4144 }
4145
4146 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
4148 self.mcp_servers = Some(servers);
4149 self
4150 }
4151
4152 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
4155 self.mcp_oauth_token_storage = Some(mode.into());
4156 self
4157 }
4158
4159 pub fn with_embedding_cache_storage(
4161 mut self,
4162 embedding_cache_storage: impl Into<String>,
4163 ) -> Self {
4164 self.embedding_cache_storage = Some(embedding_cache_storage.into());
4165 self
4166 }
4167
4168 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
4171 self.enable_config_discovery = Some(enable);
4172 self
4173 }
4174
4175 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
4177 self.skip_embedding_retrieval = Some(value);
4178 self
4179 }
4180
4181 pub fn with_organization_custom_instructions(
4183 mut self,
4184 instructions: impl Into<String>,
4185 ) -> Self {
4186 self.organization_custom_instructions = Some(instructions.into());
4187 self
4188 }
4189
4190 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
4192 self.enable_on_demand_instruction_discovery = Some(value);
4193 self
4194 }
4195
4196 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
4198 self.enable_file_hooks = Some(value);
4199 self
4200 }
4201
4202 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
4204 self.enable_host_git_operations = Some(value);
4205 self
4206 }
4207
4208 pub fn with_enable_session_store(mut self, value: bool) -> Self {
4210 self.enable_session_store = Some(value);
4211 self
4212 }
4213
4214 pub fn with_enable_skills(mut self, value: bool) -> Self {
4216 self.enable_skills = Some(value);
4217 self
4218 }
4219
4220 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
4226 self.enable_mcp_apps = Some(enable);
4227 self
4228 }
4229
4230 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
4232 self.github_mcp_tool_config = Some(config);
4233 self
4234 }
4235
4236 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
4238 where
4239 I: IntoIterator<Item = P>,
4240 P: Into<PathBuf>,
4241 {
4242 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
4243 self
4244 }
4245
4246 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
4248 where
4249 I: IntoIterator<Item = S>,
4250 S: Into<String>,
4251 {
4252 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
4253 self
4254 }
4255
4256 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
4260 where
4261 I: IntoIterator<Item = P>,
4262 P: Into<PathBuf>,
4263 {
4264 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
4265 self
4266 }
4267
4268 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
4270 where
4271 I: IntoIterator<Item = P>,
4272 P: Into<PathBuf>,
4273 {
4274 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
4275 self
4276 }
4277
4278 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
4280 self.large_output = Some(config);
4281 self
4282 }
4283
4284 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
4287 self.tool_search = Some(config);
4288 self
4289 }
4290
4291 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
4293 where
4294 I: IntoIterator<Item = S>,
4295 S: Into<String>,
4296 {
4297 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
4298 self
4299 }
4300
4301 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
4303 where
4304 I: IntoIterator<Item = S>,
4305 S: Into<String>,
4306 {
4307 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
4308 self
4309 }
4310
4311 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
4313 mut self,
4314 agents: I,
4315 ) -> Self {
4316 self.custom_agents = Some(agents.into_iter().collect());
4317 self
4318 }
4319
4320 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
4322 self.default_agent = Some(agent);
4323 self
4324 }
4325
4326 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
4328 self.agent = Some(name.into());
4329 self
4330 }
4331
4332 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
4334 self.infinite_sessions = Some(config);
4335 self
4336 }
4337
4338 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
4340 self.provider = Some(provider);
4341 self
4342 }
4343
4344 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
4346 self.capi = Some(capi);
4347 self
4348 }
4349
4350 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
4356 self.providers = Some(providers);
4357 self
4358 }
4359
4360 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
4366 self.models = Some(models);
4367 self
4368 }
4369
4370 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
4374 self.enable_session_telemetry = Some(enable);
4375 self
4376 }
4377
4378 pub fn with_enable_citations(mut self, enable: bool) -> Self {
4380 self.enable_citations = Some(enable);
4381 self
4382 }
4383
4384 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
4387 self.enable_file_change_tracking = Some(enable);
4388 self
4389 }
4390
4391 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
4393 self.session_limits = Some(limits);
4394 self
4395 }
4396
4397 pub fn with_model_capabilities(
4399 mut self,
4400 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
4401 ) -> Self {
4402 self.model_capabilities = Some(capabilities);
4403 self
4404 }
4405
4406 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
4408 self.memory = Some(memory);
4409 self
4410 }
4411
4412 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4414 self.config_directory = Some(dir.into());
4415 self
4416 }
4417
4418 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4420 self.working_directory = Some(dir.into());
4421 self
4422 }
4423
4424 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
4426 where
4427 I: IntoIterator<Item = P>,
4428 P: Into<PathBuf>,
4429 {
4430 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
4431 self
4432 }
4433
4434 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
4438 self.github_token = Some(token.into());
4439 self
4440 }
4441
4442 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
4448 self.github_token_provider = Some(provider);
4449 self
4450 }
4451
4452 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4454 self.include_sub_agent_streaming_events = Some(include);
4455 self
4456 }
4457
4458 pub fn with_remote_session(
4460 mut self,
4461 mode: crate::generated::api_types::RemoteSessionMode,
4462 ) -> Self {
4463 self.remote_session = Some(mode);
4464 self
4465 }
4466
4467 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4470 self.suppress_resume_event = Some(suppress);
4471 self
4472 }
4473
4474 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4480 self.continue_pending_work = Some(continue_pending);
4481 self
4482 }
4483
4484 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4486 self.skip_custom_instructions = Some(value);
4487 self
4488 }
4489
4490 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4492 self.custom_agents_local_only = Some(value);
4493 self
4494 }
4495
4496 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
4498 self.enable_experimental_mode = Some(enable_experimental_mode);
4499 self
4500 }
4501
4502 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4504 self.coauthor_enabled = Some(value);
4505 self
4506 }
4507
4508 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4510 self.manage_schedule_enabled = Some(value);
4511 self
4512 }
4513
4514 #[doc(hidden)]
4518 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4519 self.exp_assignments = Some(assignments);
4520 self
4521 }
4522
4523 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4526 self.enable_managed_settings = Some(enabled);
4527 self
4528 }
4529
4530 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
4534 self.managed_settings = Some(managed_settings);
4535 self
4536 }
4537}
4538
4539#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4545#[serde(rename_all = "camelCase")]
4546#[non_exhaustive]
4547pub struct SystemMessageConfig {
4548 #[serde(skip_serializing_if = "Option::is_none")]
4550 pub mode: Option<String>,
4551 #[serde(skip_serializing_if = "Option::is_none")]
4553 pub content: Option<String>,
4554 #[serde(skip_serializing_if = "Option::is_none")]
4556 pub sections: Option<HashMap<String, SectionOverride>>,
4557}
4558
4559impl SystemMessageConfig {
4560 pub fn new() -> Self {
4563 Self::default()
4564 }
4565
4566 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4569 self.mode = Some(mode.into());
4570 self
4571 }
4572
4573 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4576 self.content = Some(content.into());
4577 self
4578 }
4579
4580 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4582 self.sections = Some(sections);
4583 self
4584 }
4585}
4586
4587#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4593#[serde(rename_all = "camelCase")]
4594pub struct SectionOverride {
4595 #[serde(skip_serializing_if = "Option::is_none")]
4598 pub action: Option<String>,
4599 #[serde(skip_serializing_if = "Option::is_none")]
4601 pub content: Option<String>,
4602}
4603
4604#[derive(Debug, Clone, Serialize, Deserialize)]
4606#[serde(rename_all = "camelCase")]
4607pub struct CreateSessionResult {
4608 pub session_id: SessionId,
4610 #[serde(skip_serializing_if = "Option::is_none")]
4612 pub workspace_path: Option<PathBuf>,
4613 #[serde(default, alias = "remote_url")]
4615 pub remote_url: Option<String>,
4616 #[serde(skip_serializing_if = "Option::is_none")]
4618 pub capabilities: Option<SessionCapabilities>,
4619}
4620
4621#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4623#[serde(rename_all = "camelCase")]
4624pub(crate) struct ResumeSessionResult {
4625 #[serde(default)]
4627 pub session_id: Option<SessionId>,
4628 #[serde(default, skip_serializing_if = "Option::is_none")]
4630 pub workspace_path: Option<PathBuf>,
4631 #[serde(default, alias = "remote_url")]
4633 pub remote_url: Option<String>,
4634 #[serde(default, skip_serializing_if = "Option::is_none")]
4636 pub capabilities: Option<SessionCapabilities>,
4637 #[serde(
4639 default,
4640 alias = "openCanvasInstances",
4641 skip_serializing_if = "Option::is_none"
4642 )]
4643 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4644}
4645
4646#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4648#[serde(rename_all = "lowercase")]
4649pub enum LogLevel {
4650 #[default]
4652 Info,
4653 Warning,
4655 Error,
4657}
4658
4659#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4664#[serde(rename_all = "camelCase")]
4665pub struct LogOptions {
4666 #[serde(skip_serializing_if = "Option::is_none")]
4668 pub level: Option<LogLevel>,
4669 #[serde(skip_serializing_if = "Option::is_none")]
4672 pub ephemeral: Option<bool>,
4673}
4674
4675impl LogOptions {
4676 pub fn with_level(mut self, level: LogLevel) -> Self {
4678 self.level = Some(level);
4679 self
4680 }
4681
4682 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4684 self.ephemeral = Some(ephemeral);
4685 self
4686 }
4687}
4688
4689#[derive(Debug, Clone, Default)]
4693pub struct SetModelOptions {
4694 pub reasoning_effort: Option<String>,
4697 pub reasoning_summary: Option<ReasoningSummary>,
4701 pub context_tier: Option<ContextTier>,
4704 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4708}
4709
4710impl SetModelOptions {
4711 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4713 self.reasoning_effort = Some(effort.into());
4714 self
4715 }
4716
4717 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4719 self.reasoning_summary = Some(summary);
4720 self
4721 }
4722
4723 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4725 self.context_tier = Some(tier);
4726 self
4727 }
4728
4729 pub fn with_model_capabilities(
4731 mut self,
4732 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4733 ) -> Self {
4734 self.model_capabilities = Some(caps);
4735 self
4736 }
4737}
4738
4739#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4746#[serde(rename_all = "camelCase")]
4747pub struct PingResponse {
4748 #[serde(default)]
4750 pub message: String,
4751 #[serde(default)]
4753 pub timestamp: String,
4754 #[serde(skip_serializing_if = "Option::is_none")]
4756 pub protocol_version: Option<u32>,
4757}
4758
4759#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4761#[serde(rename_all = "camelCase")]
4762pub struct AttachmentLineRange {
4763 pub start: u32,
4765 pub end: u32,
4767}
4768
4769#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4771#[serde(rename_all = "camelCase")]
4772pub struct AttachmentSelectionPosition {
4773 pub line: u32,
4775 pub character: u32,
4777}
4778
4779#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4781#[serde(rename_all = "camelCase")]
4782pub struct AttachmentSelectionRange {
4783 pub start: AttachmentSelectionPosition,
4785 pub end: AttachmentSelectionPosition,
4787}
4788
4789#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4791#[serde(rename_all = "snake_case")]
4792#[non_exhaustive]
4793pub enum GitHubReferenceType {
4794 Issue,
4796 Pr,
4798 Discussion,
4800}
4801
4802#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4808#[serde(rename_all = "camelCase")]
4809pub struct GitHubRepoPointer {
4810 #[serde(skip_serializing_if = "Option::is_none")]
4812 pub id: Option<i64>,
4813 pub name: String,
4815 pub owner: String,
4817}
4818
4819#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4821#[serde(rename_all = "camelCase")]
4822pub struct GitHubFileDiffSide {
4823 pub path: String,
4825 pub r#ref: String,
4827 pub repo: GitHubRepoPointer,
4829}
4830
4831#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4833#[serde(rename_all = "camelCase")]
4834pub struct GitHubTreeComparisonSide {
4835 pub repo: GitHubRepoPointer,
4837 pub revision: String,
4839}
4840
4841#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4843#[serde(rename_all = "camelCase")]
4844pub struct GitHubSnippetLineRange {
4845 pub start: i64,
4847 pub end: i64,
4849}
4850
4851#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4853#[serde(
4854 tag = "type",
4855 rename_all = "camelCase",
4856 rename_all_fields = "camelCase"
4857)]
4858#[non_exhaustive]
4859pub enum Attachment {
4860 File {
4862 path: PathBuf,
4864 #[serde(skip_serializing_if = "Option::is_none")]
4866 display_name: Option<String>,
4867 #[serde(skip_serializing_if = "Option::is_none")]
4869 line_range: Option<AttachmentLineRange>,
4870 },
4871 Directory {
4873 path: PathBuf,
4875 #[serde(skip_serializing_if = "Option::is_none")]
4877 display_name: Option<String>,
4878 },
4879 Selection {
4881 file_path: PathBuf,
4883 text: String,
4885 #[serde(skip_serializing_if = "Option::is_none")]
4887 display_name: Option<String>,
4888 selection: AttachmentSelectionRange,
4890 },
4891 Blob {
4893 data: String,
4895 mime_type: String,
4897 #[serde(skip_serializing_if = "Option::is_none")]
4899 display_name: Option<String>,
4900 },
4901 #[serde(rename = "github_reference")]
4903 GitHubReference {
4904 number: u64,
4906 title: String,
4908 reference_type: GitHubReferenceType,
4910 state: String,
4912 url: String,
4914 },
4915 #[serde(rename = "github_commit")]
4917 GitHubCommit {
4918 message: String,
4920 oid: String,
4922 repo: GitHubRepoPointer,
4924 url: String,
4926 },
4927 #[serde(rename = "github_release")]
4929 GitHubRelease {
4930 name: String,
4932 repo: GitHubRepoPointer,
4934 tag_name: String,
4936 url: String,
4938 },
4939 #[serde(rename = "github_actions_job")]
4941 GitHubActionsJob {
4942 #[serde(skip_serializing_if = "Option::is_none")]
4945 conclusion: Option<String>,
4946 job_id: i64,
4948 job_name: String,
4950 repo: GitHubRepoPointer,
4952 url: String,
4954 workflow_name: String,
4956 },
4957 #[serde(rename = "github_repository")]
4959 GitHubRepository {
4960 #[serde(skip_serializing_if = "Option::is_none")]
4962 description: Option<String>,
4963 #[serde(skip_serializing_if = "Option::is_none")]
4966 r#ref: Option<String>,
4967 repo: GitHubRepoPointer,
4969 url: String,
4971 },
4972 #[serde(rename = "github_file_diff")]
4974 GitHubFileDiff {
4975 #[serde(skip_serializing_if = "Option::is_none")]
4977 base: Option<GitHubFileDiffSide>,
4978 #[serde(skip_serializing_if = "Option::is_none")]
4980 head: Option<GitHubFileDiffSide>,
4981 url: String,
4983 },
4984 #[serde(rename = "github_tree_comparison")]
4986 GitHubTreeComparison {
4987 base: GitHubTreeComparisonSide,
4989 head: GitHubTreeComparisonSide,
4991 url: String,
4993 },
4994 #[serde(rename = "github_url")]
4996 GitHubUrl {
4997 url: String,
4999 },
5000 #[serde(rename = "github_file")]
5002 GitHubFile {
5003 path: String,
5005 r#ref: String,
5007 repo: GitHubRepoPointer,
5009 url: String,
5011 },
5012 #[serde(rename = "github_snippet")]
5014 GitHubSnippet {
5015 line_range: GitHubSnippetLineRange,
5017 path: String,
5019 r#ref: String,
5021 repo: GitHubRepoPointer,
5023 url: String,
5025 },
5026}
5027
5028impl Attachment {
5029 pub fn display_name(&self) -> Option<&str> {
5031 match self {
5032 Self::File { display_name, .. }
5033 | Self::Directory { display_name, .. }
5034 | Self::Selection { display_name, .. }
5035 | Self::Blob { display_name, .. } => display_name.as_deref(),
5036 Self::GitHubReference { .. }
5037 | Self::GitHubCommit { .. }
5038 | Self::GitHubRelease { .. }
5039 | Self::GitHubActionsJob { .. }
5040 | Self::GitHubRepository { .. }
5041 | Self::GitHubFileDiff { .. }
5042 | Self::GitHubTreeComparison { .. }
5043 | Self::GitHubUrl { .. }
5044 | Self::GitHubFile { .. }
5045 | Self::GitHubSnippet { .. } => None,
5046 }
5047 }
5048
5049 pub fn label(&self) -> Option<String> {
5051 if let Some(display_name) = self
5052 .display_name()
5053 .map(str::trim)
5054 .filter(|name| !name.is_empty())
5055 {
5056 return Some(display_name.to_string());
5057 }
5058
5059 match self {
5060 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
5061 format!("#{}", number)
5062 } else {
5063 title.trim().to_string()
5064 }),
5065 _ => self.derived_display_name(),
5066 }
5067 }
5068
5069 pub fn ensure_display_name(&mut self) {
5071 if self
5072 .display_name()
5073 .map(str::trim)
5074 .is_some_and(|name| !name.is_empty())
5075 {
5076 return;
5077 }
5078
5079 let Some(derived_display_name) = self.derived_display_name() else {
5080 return;
5081 };
5082
5083 match self {
5084 Self::File { display_name, .. }
5085 | Self::Directory { display_name, .. }
5086 | Self::Selection { display_name, .. }
5087 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
5088 Self::GitHubReference { .. }
5089 | Self::GitHubCommit { .. }
5090 | Self::GitHubRelease { .. }
5091 | Self::GitHubActionsJob { .. }
5092 | Self::GitHubRepository { .. }
5093 | Self::GitHubFileDiff { .. }
5094 | Self::GitHubTreeComparison { .. }
5095 | Self::GitHubUrl { .. }
5096 | Self::GitHubFile { .. }
5097 | Self::GitHubSnippet { .. } => {}
5098 }
5099 }
5100
5101 fn derived_display_name(&self) -> Option<String> {
5102 match self {
5103 Self::File { path, .. } | Self::Directory { path, .. } => {
5104 Some(attachment_name_from_path(path))
5105 }
5106 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
5107 Self::Blob { .. } => Some("attachment".to_string()),
5108 Self::GitHubReference { .. }
5109 | Self::GitHubCommit { .. }
5110 | Self::GitHubRelease { .. }
5111 | Self::GitHubActionsJob { .. }
5112 | Self::GitHubRepository { .. }
5113 | Self::GitHubFileDiff { .. }
5114 | Self::GitHubTreeComparison { .. }
5115 | Self::GitHubUrl { .. }
5116 | Self::GitHubFile { .. }
5117 | Self::GitHubSnippet { .. } => None,
5118 }
5119 }
5120}
5121
5122fn attachment_name_from_path(path: &Path) -> String {
5123 path.file_name()
5124 .map(|name| name.to_string_lossy().into_owned())
5125 .filter(|name| !name.is_empty())
5126 .unwrap_or_else(|| {
5127 let full = path.to_string_lossy();
5128 if full.is_empty() {
5129 "attachment".to_string()
5130 } else {
5131 full.into_owned()
5132 }
5133 })
5134}
5135
5136pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
5138 for attachment in attachments {
5139 attachment.ensure_display_name();
5140 }
5141}
5142
5143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5148#[serde(rename_all = "lowercase")]
5149#[non_exhaustive]
5150pub enum DeliveryMode {
5151 Enqueue,
5153 Immediate,
5155}
5156
5157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5162#[serde(rename_all = "lowercase")]
5163#[non_exhaustive]
5164pub enum AgentMode {
5165 Interactive,
5167 Plan,
5169 Autopilot,
5171 Shell,
5173}
5174
5175#[derive(Debug, Clone)]
5204#[non_exhaustive]
5205pub struct MessageOptions {
5206 pub prompt: String,
5208 pub mode: Option<DeliveryMode>,
5214 pub agent_mode: Option<AgentMode>,
5218 pub attachments: Option<Vec<Attachment>>,
5220 pub wait_timeout: Option<Duration>,
5223 pub request_headers: Option<HashMap<String, String>>,
5227 pub traceparent: Option<String>,
5234 pub tracestate: Option<String>,
5238 pub display_prompt: Option<String>,
5240}
5241
5242impl MessageOptions {
5243 pub fn new(prompt: impl Into<String>) -> Self {
5245 Self {
5246 prompt: prompt.into(),
5247 mode: None,
5248 agent_mode: None,
5249 attachments: None,
5250 wait_timeout: None,
5251 request_headers: None,
5252 traceparent: None,
5253 tracestate: None,
5254 display_prompt: None,
5255 }
5256 }
5257
5258 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
5264 self.mode = Some(mode);
5265 self
5266 }
5267
5268 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
5272 self.agent_mode = Some(agent_mode);
5273 self
5274 }
5275
5276 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
5278 self.attachments = Some(attachments);
5279 self
5280 }
5281
5282 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
5284 self.wait_timeout = Some(timeout);
5285 self
5286 }
5287
5288 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
5290 self.request_headers = Some(headers);
5291 self
5292 }
5293
5294 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
5299 self.traceparent = ctx.traceparent;
5300 self.tracestate = ctx.tracestate;
5301 self
5302 }
5303
5304 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5306 self.traceparent = Some(traceparent.into());
5307 self
5308 }
5309
5310 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5312 self.tracestate = Some(tracestate.into());
5313 self
5314 }
5315
5316 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5318 self.display_prompt = Some(display_prompt.into());
5319 self
5320 }
5321}
5322
5323impl From<&str> for MessageOptions {
5324 fn from(prompt: &str) -> Self {
5325 Self::new(prompt)
5326 }
5327}
5328
5329impl From<String> for MessageOptions {
5330 fn from(prompt: String) -> Self {
5331 Self::new(prompt)
5332 }
5333}
5334
5335impl From<&String> for MessageOptions {
5336 fn from(prompt: &String) -> Self {
5337 Self::new(prompt.clone())
5338 }
5339}
5340
5341#[derive(Debug, Clone, Serialize, Deserialize)]
5343#[serde(rename_all = "camelCase")]
5344#[non_exhaustive]
5345pub struct GetStatusResponse {
5346 pub version: String,
5348 pub protocol_version: u32,
5350}
5351
5352#[derive(Debug, Clone, Serialize, Deserialize)]
5354#[serde(rename_all = "camelCase")]
5355#[non_exhaustive]
5356pub struct GetAuthStatusResponse {
5357 pub is_authenticated: bool,
5359 #[serde(skip_serializing_if = "Option::is_none")]
5362 pub auth_type: Option<String>,
5363 #[serde(skip_serializing_if = "Option::is_none")]
5365 pub host: Option<String>,
5366 #[serde(skip_serializing_if = "Option::is_none")]
5368 pub login: Option<String>,
5369 #[serde(skip_serializing_if = "Option::is_none")]
5371 pub status_message: Option<String>,
5372}
5373
5374#[derive(Debug, Clone, Serialize, Deserialize)]
5378#[serde(rename_all = "camelCase")]
5379pub struct SessionEventNotification {
5380 pub session_id: SessionId,
5382 pub event: SessionEvent,
5384}
5385
5386#[derive(Debug, Clone, Serialize, Deserialize)]
5393#[serde(rename_all = "camelCase")]
5394pub struct SessionEvent {
5395 pub id: String,
5397 pub timestamp: String,
5399 pub parent_id: Option<String>,
5401 #[serde(skip_serializing_if = "Option::is_none")]
5403 pub ephemeral: Option<bool>,
5404 #[serde(skip_serializing_if = "Option::is_none")]
5407 pub agent_id: Option<String>,
5408 #[serde(skip_serializing_if = "Option::is_none")]
5410 pub debug_cli_received_at_ms: Option<i64>,
5411 #[serde(skip_serializing_if = "Option::is_none")]
5413 pub debug_ws_forwarded_at_ms: Option<i64>,
5414 #[serde(rename = "type")]
5416 pub event_type: String,
5417 pub data: Value,
5419}
5420
5421impl SessionEvent {
5422 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5427 use serde::de::IntoDeserializer;
5428 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5429 self.event_type.as_str().into_deserializer();
5430 crate::generated::SessionEventType::deserialize(deserializer)
5431 .unwrap_or(crate::generated::SessionEventType::Unknown)
5432 }
5433
5434 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5440 serde_json::from_value(self.data.clone()).ok()
5441 }
5442
5443 pub fn is_transient_error(&self) -> bool {
5447 self.event_type == "session.error"
5448 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5449 }
5450}
5451
5452#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5457#[serde(rename_all = "camelCase")]
5458#[non_exhaustive]
5459pub struct ToolInvocation {
5460 pub session_id: SessionId,
5462 pub tool_call_id: String,
5464 pub tool_name: String,
5466 pub arguments: Value,
5468 #[serde(skip)]
5476 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5477 #[serde(default, skip_serializing_if = "Option::is_none")]
5482 pub traceparent: Option<String>,
5483 #[serde(default, skip_serializing_if = "Option::is_none")]
5486 pub tracestate: Option<String>,
5487}
5488
5489impl ToolInvocation {
5490 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5511 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5512 }
5513
5514 pub fn trace_context(&self) -> TraceContext {
5517 TraceContext {
5518 traceparent: self.traceparent.clone(),
5519 tracestate: self.tracestate.clone(),
5520 }
5521 }
5522}
5523
5524#[derive(Debug, Clone, Serialize, Deserialize)]
5526#[serde(rename_all = "camelCase")]
5527pub struct ToolBinaryResult {
5528 pub data: String,
5530 pub mime_type: String,
5532 pub r#type: String,
5534 #[serde(default, skip_serializing_if = "Option::is_none")]
5536 pub description: Option<String>,
5537}
5538
5539#[derive(Debug, Clone, Serialize, Deserialize)]
5546#[serde(rename_all = "camelCase")]
5547#[non_exhaustive]
5548pub struct ToolResultExpanded {
5549 pub text_result_for_llm: String,
5551 pub result_type: String,
5553 #[serde(default, skip_serializing_if = "Option::is_none")]
5555 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5556 #[serde(skip_serializing_if = "Option::is_none")]
5558 pub session_log: Option<String>,
5559 #[serde(skip_serializing_if = "Option::is_none")]
5561 pub error: Option<String>,
5562 #[serde(default, skip_serializing_if = "Option::is_none")]
5564 pub tool_telemetry: Option<HashMap<String, Value>>,
5565 #[serde(default, skip_serializing_if = "Option::is_none")]
5567 pub tool_references: Option<Vec<String>>,
5568}
5569
5570impl ToolResultExpanded {
5571 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5575 Self {
5576 text_result_for_llm: text_result_for_llm.into(),
5577 result_type: result_type.into(),
5578 binary_results_for_llm: None,
5579 session_log: None,
5580 error: None,
5581 tool_telemetry: None,
5582 tool_references: None,
5583 }
5584 }
5585
5586 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5588 self.binary_results_for_llm = Some(results);
5589 self
5590 }
5591
5592 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5594 self.session_log = Some(session_log.into());
5595 self
5596 }
5597
5598 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5600 self.error = Some(error.into());
5601 self
5602 }
5603
5604 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5606 self.tool_telemetry = Some(telemetry);
5607 self
5608 }
5609
5610 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5612 where
5613 I: IntoIterator<Item = S>,
5614 S: Into<String>,
5615 {
5616 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5617 self
5618 }
5619}
5620
5621#[derive(Debug, Clone, Serialize, Deserialize)]
5623#[serde(untagged)]
5624#[non_exhaustive]
5625pub enum ToolResult {
5626 Text(String),
5628 Expanded(ToolResultExpanded),
5630}
5631
5632#[derive(Debug, Clone, Serialize, Deserialize)]
5634#[serde(rename_all = "camelCase")]
5635pub struct ToolResultResponse {
5636 pub result: ToolResult,
5638}
5639
5640#[derive(Debug, Clone, Serialize, Deserialize)]
5642#[serde(rename_all = "camelCase")]
5643pub struct SessionMetadata {
5644 pub session_id: SessionId,
5646 pub start_time: String,
5648 pub modified_time: String,
5650 #[serde(skip_serializing_if = "Option::is_none")]
5652 pub summary: Option<String>,
5653 pub is_remote: bool,
5655}
5656
5657#[derive(Debug, Clone, Serialize, Deserialize)]
5659#[serde(rename_all = "camelCase")]
5660pub struct ListSessionsResponse {
5661 pub sessions: Vec<SessionMetadata>,
5663}
5664
5665#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5669#[serde(rename_all = "camelCase")]
5670pub struct SessionListFilter {
5671 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5673 pub working_directory: Option<String>,
5674 #[serde(default, skip_serializing_if = "Option::is_none")]
5676 pub git_root: Option<String>,
5677 #[serde(default, skip_serializing_if = "Option::is_none")]
5679 pub repository: Option<String>,
5680 #[serde(default, skip_serializing_if = "Option::is_none")]
5682 pub branch: Option<String>,
5683}
5684
5685#[derive(Debug, Clone, Serialize, Deserialize)]
5687#[serde(rename_all = "camelCase")]
5688pub struct GetSessionMetadataResponse {
5689 #[serde(skip_serializing_if = "Option::is_none")]
5691 pub session: Option<SessionMetadata>,
5692}
5693
5694#[derive(Debug, Clone, Serialize, Deserialize)]
5696#[serde(rename_all = "camelCase")]
5697pub struct GetLastSessionIdResponse {
5698 #[serde(skip_serializing_if = "Option::is_none")]
5700 pub session_id: Option<SessionId>,
5701}
5702
5703#[derive(Debug, Clone, Serialize, Deserialize)]
5705#[serde(rename_all = "camelCase")]
5706pub struct GetForegroundSessionResponse {
5707 #[serde(skip_serializing_if = "Option::is_none")]
5709 pub session_id: Option<SessionId>,
5710}
5711
5712#[derive(Debug, Clone, Serialize, Deserialize)]
5714#[serde(rename_all = "camelCase")]
5715pub struct GetMessagesResponse {
5716 pub events: Vec<SessionEvent>,
5718}
5719
5720#[derive(Debug, Clone, Serialize, Deserialize)]
5722#[serde(rename_all = "camelCase")]
5723pub struct ElicitationResult {
5724 pub action: String,
5726 #[serde(skip_serializing_if = "Option::is_none")]
5728 pub content: Option<Value>,
5729}
5730
5731#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5737#[serde(rename_all = "camelCase")]
5738#[non_exhaustive]
5739pub enum ElicitationMode {
5740 Form,
5742 Url,
5744 #[serde(other)]
5746 Unknown,
5747}
5748
5749#[derive(Debug, Clone, Serialize, Deserialize)]
5756#[serde(rename_all = "camelCase")]
5757pub struct ElicitationRequest {
5758 pub message: String,
5760 #[serde(skip_serializing_if = "Option::is_none")]
5762 pub requested_schema: Option<Value>,
5763 #[serde(skip_serializing_if = "Option::is_none")]
5765 pub mode: Option<ElicitationMode>,
5766 #[serde(skip_serializing_if = "Option::is_none")]
5768 pub elicitation_source: Option<String>,
5769 #[serde(skip_serializing_if = "Option::is_none")]
5771 pub url: Option<String>,
5772}
5773
5774#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5779#[serde(rename_all = "camelCase")]
5780pub struct SessionCapabilities {
5781 #[serde(skip_serializing_if = "Option::is_none")]
5783 pub ui: Option<UiCapabilities>,
5784}
5785
5786#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5788#[serde(rename_all = "camelCase")]
5789pub struct UiCapabilities {
5790 #[serde(skip_serializing_if = "Option::is_none")]
5792 pub elicitation: Option<bool>,
5793 #[serde(skip_serializing_if = "Option::is_none")]
5804 pub mcp_apps: Option<bool>,
5805 #[serde(skip_serializing_if = "Option::is_none")]
5807 pub canvases: Option<bool>,
5808}
5809
5810#[derive(Debug, Clone, Default)]
5812pub struct UiInputOptions<'a> {
5813 pub title: Option<&'a str>,
5815 pub description: Option<&'a str>,
5817 pub min_length: Option<u64>,
5819 pub max_length: Option<u64>,
5821 pub format: Option<InputFormat>,
5823 pub default: Option<&'a str>,
5825}
5826
5827#[derive(Debug, Clone, Copy)]
5829#[non_exhaustive]
5830pub enum InputFormat {
5831 Email,
5833 Uri,
5835 Date,
5837 DateTime,
5839}
5840
5841impl InputFormat {
5842 pub fn as_str(&self) -> &'static str {
5844 match self {
5845 Self::Email => "email",
5846 Self::Uri => "uri",
5847 Self::Date => "date",
5848 Self::DateTime => "date-time",
5849 }
5850 }
5851}
5852
5853pub use crate::generated::api_types::{
5858 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5859 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5860 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5861 PermissionDecisionApproveOnce, PermissionDecisionContext, PermissionDecisionOutcome,
5862 PermissionDecisionReject, PermissionDecisionSource, PermissionDecisionSurface,
5863 PermissionDecisionUserNotAvailable, PermissionResponseCapability,
5864};
5865
5866#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5872#[serde(rename_all = "kebab-case")]
5873#[non_exhaustive]
5874pub enum PermissionRequestKind {
5875 Shell,
5877 Write,
5879 Read,
5881 Url,
5883 Mcp,
5885 CustomTool,
5887 Memory,
5889 Hook,
5891 #[serde(other)]
5894 Unknown,
5895}
5896
5897#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5903#[serde(rename_all = "camelCase")]
5904pub struct PermissionRequestData {
5905 #[serde(default, skip_serializing_if = "Option::is_none")]
5909 pub kind: Option<PermissionRequestKind>,
5910 #[serde(default, skip_serializing_if = "Option::is_none")]
5913 pub tool_call_id: Option<String>,
5914 #[serde(default, skip_serializing_if = "Option::is_none")]
5916 pub managed_approval_required: Option<bool>,
5917 #[serde(default, skip_serializing_if = "is_false")]
5919 pub managed_settings_enabled: bool,
5920 #[serde(flatten)]
5924 pub extra: Value,
5925}
5926
5927#[derive(Debug, Clone, Serialize, Deserialize)]
5929#[serde(rename_all = "camelCase")]
5930pub struct ExitPlanModeData {
5931 #[serde(default)]
5933 pub summary: String,
5934 #[serde(default, skip_serializing_if = "Option::is_none")]
5936 pub plan_content: Option<String>,
5937 #[serde(default)]
5939 pub actions: Vec<String>,
5940 #[serde(default = "default_recommended_action")]
5942 pub recommended_action: String,
5943}
5944
5945fn default_recommended_action() -> String {
5946 "autopilot".to_string()
5947}
5948
5949impl Default for ExitPlanModeData {
5950 fn default() -> Self {
5951 Self {
5952 summary: String::new(),
5953 plan_content: None,
5954 actions: Vec::new(),
5955 recommended_action: default_recommended_action(),
5956 }
5957 }
5958}
5959
5960#[cfg(test)]
5961mod tests {
5962 use std::collections::HashMap;
5963 use std::path::PathBuf;
5964
5965 use serde_json::json;
5966
5967 use super::{
5968 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5969 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5970 CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry,
5971 ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
5972 InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
5973 MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig,
5974 ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
5975 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
5976 ToolResultResponse, ensure_attachment_display_names,
5977 };
5978 use crate::generated::session_events::TypedSessionEvent;
5979
5980 #[test]
5981 fn permission_response_capability_is_publicly_exported() {
5982 assert_eq!(
5983 serde_json::to_value(PermissionResponseCapability::Interactive).unwrap(),
5984 json!("interactive")
5985 );
5986 }
5987
5988 #[test]
5989 fn tool_builder_composes() {
5990 let tool = Tool::new("greet")
5991 .with_description("Say hello")
5992 .with_namespaced_name("hello/greet")
5993 .with_instructions("Pass the user's name")
5994 .with_parameters(json!({
5995 "type": "object",
5996 "properties": { "name": { "type": "string" } },
5997 "required": ["name"]
5998 }))
5999 .with_overrides_built_in_tool(true)
6000 .with_skip_permission(true);
6001 assert_eq!(tool.name, "greet");
6002 assert_eq!(tool.description, "Say hello");
6003 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
6004 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
6005 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
6006 assert!(tool.overrides_built_in_tool);
6007 assert!(tool.skip_permission);
6008 }
6009
6010 #[test]
6011 fn tool_defer_serialization() {
6012 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
6013 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
6014 let value = serde_json::to_value(&tool).unwrap();
6015 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
6016
6017 let plain = Tool::new("plain");
6018 let value = serde_json::to_value(&plain).unwrap();
6019 assert!(value.get("defer").is_none());
6020 }
6021
6022 #[test]
6023 fn tool_metadata_serialization() {
6024 use indexmap::IndexMap;
6025
6026 let mut metadata = IndexMap::new();
6027 metadata.insert(
6028 "github.com/copilot:safeForTelemetry".to_string(),
6029 json!({ "name": true, "inputsNames": false }),
6030 );
6031 let tool = Tool::new("lookup").with_metadata(metadata);
6032 let value = serde_json::to_value(&tool).unwrap();
6033 assert_eq!(
6034 value
6035 .get("metadata")
6036 .unwrap()
6037 .get("github.com/copilot:safeForTelemetry")
6038 .unwrap(),
6039 &json!({ "name": true, "inputsNames": false })
6040 );
6041
6042 let plain = Tool::new("plain");
6044 let value = serde_json::to_value(&plain).unwrap();
6045 assert!(value.get("metadata").is_none());
6046 }
6047
6048 #[test]
6049 fn custom_agent_config_builder_with_model() {
6050 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
6051 .with_model("claude-haiku-4.5")
6052 .with_display_name("My Agent");
6053 assert_eq!(agent.name, "my-agent");
6054 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
6055 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
6056 }
6057
6058 #[test]
6059 fn custom_agent_config_serializes_model() {
6060 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
6061 let wire = serde_json::to_value(&agent).unwrap();
6062 assert_eq!(wire["model"], "claude-haiku-4.5");
6063 assert_eq!(wire["name"], "model-agent");
6064 }
6065
6066 #[test]
6067 fn custom_agent_config_omits_model_when_none() {
6068 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
6069 let wire = serde_json::to_value(&agent).unwrap();
6070 assert!(wire.get("model").is_none());
6071 }
6072
6073 #[test]
6074 fn custom_agent_config_builder_with_reasoning_effort() {
6075 let agent =
6076 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6077 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
6078 }
6079
6080 #[test]
6081 fn custom_agent_config_serializes_reasoning_effort() {
6082 let agent =
6083 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6084 let wire = serde_json::to_value(&agent).unwrap();
6085 assert_eq!(wire["reasoningEffort"], "high");
6086 }
6087
6088 #[test]
6089 fn custom_agent_config_omits_reasoning_effort_when_none() {
6090 let agent = CustomAgentConfig::new("default-agent", "prompt");
6091 let wire = serde_json::to_value(&agent).unwrap();
6092 assert!(wire.get("reasoningEffort").is_none());
6093 }
6094
6095 #[test]
6096 #[should_panic(expected = "tool parameter schema must be a JSON object")]
6097 fn tool_with_parameters_panics_on_non_object_value() {
6098 let _ = Tool::new("noop").with_parameters(json!(null));
6099 }
6100
6101 #[test]
6102 fn tool_result_expanded_serializes_binary_results_for_llm() {
6103 let response = ToolResultResponse {
6104 result: ToolResult::Expanded(ToolResultExpanded {
6105 text_result_for_llm: "rendered chart".to_string(),
6106 result_type: "success".to_string(),
6107 binary_results_for_llm: Some(vec![ToolBinaryResult {
6108 data: "aW1n".to_string(),
6109 mime_type: "image/png".to_string(),
6110 r#type: "image".to_string(),
6111 description: Some("chart preview".to_string()),
6112 }]),
6113 session_log: None,
6114 error: None,
6115 tool_telemetry: None,
6116 tool_references: None,
6117 }),
6118 };
6119
6120 let wire = serde_json::to_value(&response).unwrap();
6121
6122 assert_eq!(
6123 wire,
6124 json!({
6125 "result": {
6126 "textResultForLlm": "rendered chart",
6127 "resultType": "success",
6128 "binaryResultsForLlm": [
6129 {
6130 "data": "aW1n",
6131 "mimeType": "image/png",
6132 "type": "image",
6133 "description": "chart preview"
6134 }
6135 ]
6136 }
6137 })
6138 );
6139 }
6140
6141 #[test]
6142 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
6143 let response = ToolResultResponse {
6144 result: ToolResult::Expanded(ToolResultExpanded {
6145 text_result_for_llm: "ok".to_string(),
6146 result_type: "success".to_string(),
6147 binary_results_for_llm: None,
6148 session_log: None,
6149 error: None,
6150 tool_telemetry: None,
6151 tool_references: None,
6152 }),
6153 };
6154
6155 let wire = serde_json::to_value(&response).unwrap();
6156
6157 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6158 assert!(wire["result"].get("binaryResultsForLlm").is_none());
6159 }
6160
6161 #[test]
6162 fn tool_result_expanded_serializes_tool_references() {
6163 let response = ToolResultResponse {
6164 result: ToolResult::Expanded(
6165 ToolResultExpanded::new("found 2 tools", "success")
6166 .with_tool_references(["get_weather", "check_status"]),
6167 ),
6168 };
6169
6170 let wire = serde_json::to_value(&response).unwrap();
6171
6172 assert_eq!(
6173 wire,
6174 json!({
6175 "result": {
6176 "textResultForLlm": "found 2 tools",
6177 "resultType": "success",
6178 "toolReferences": ["get_weather", "check_status"]
6179 }
6180 })
6181 );
6182 }
6183
6184 #[test]
6185 fn tool_result_expanded_omits_tool_references_when_none() {
6186 let response = ToolResultResponse {
6187 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
6188 };
6189
6190 let wire = serde_json::to_value(&response).unwrap();
6191
6192 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6193 assert!(wire["result"].get("toolReferences").is_none());
6194 }
6195
6196 #[test]
6197 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
6198 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
6201 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
6202
6203 assert_eq!(
6204 expanded.tool_references.as_deref(),
6205 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6206 );
6207 }
6208
6209 #[test]
6210 fn tool_result_expanded_deserializes_tool_references() {
6211 let wire = json!({
6212 "textResultForLlm": "found tools",
6213 "resultType": "success",
6214 "toolReferences": ["alpha", "beta"]
6215 });
6216
6217 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
6218
6219 assert_eq!(
6220 expanded.tool_references.as_deref(),
6221 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6222 );
6223 }
6224
6225 #[test]
6226 fn session_config_default_wire_flags_off_without_handlers() {
6227 let cfg = SessionConfig::default();
6228 assert_eq!(cfg.mcp_oauth_token_storage, None);
6229 let (wire, _runtime) = cfg
6233 .into_wire(Some(SessionId::from("default-flags")))
6234 .expect("default config has no duplicate handlers");
6235 assert!(!wire.request_user_input);
6236 assert!(!wire.request_permission);
6237 assert!(!wire.request_elicitation);
6238 assert!(!wire.request_exit_plan_mode);
6239 assert!(!wire.request_auto_mode_switch);
6240 assert!(!wire.hooks);
6241 assert!(!wire.request_mcp_apps);
6242 }
6243
6244 #[test]
6245 fn resume_session_config_new_wire_flags_off_without_handlers() {
6246 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
6247 assert_eq!(cfg.mcp_oauth_token_storage, None);
6248 let (wire, _runtime) = cfg
6249 .into_wire()
6250 .expect("default resume config has no duplicate handlers");
6251 assert!(!wire.request_user_input);
6252 assert!(!wire.request_permission);
6253 assert!(!wire.request_elicitation);
6254 assert!(!wire.request_exit_plan_mode);
6255 assert!(!wire.request_auto_mode_switch);
6256 assert!(!wire.hooks);
6257 assert!(!wire.request_mcp_apps);
6258 }
6259
6260 #[test]
6261 fn custom_agents_local_only_serializes_on_create_and_resume() {
6262 let (create_wire, _) = SessionConfig::default()
6263 .with_custom_agents_local_only(false)
6264 .into_wire(Some(SessionId::from("create-locality")))
6265 .expect("create config has no duplicate handlers");
6266 let create_json = serde_json::to_value(&create_wire).unwrap();
6267 assert_eq!(create_json["customAgentsLocalOnly"], false);
6268
6269 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
6270 .with_custom_agents_local_only(false)
6271 .into_wire()
6272 .expect("resume config has no duplicate handlers");
6273 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6274 assert_eq!(resume_json["customAgentsLocalOnly"], false);
6275
6276 let (unset_create_wire, _) = SessionConfig::default()
6277 .into_wire(Some(SessionId::from("create-unset")))
6278 .expect("create config has no duplicate handlers");
6279 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6280 assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
6281
6282 let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
6283 .into_wire()
6284 .expect("resume config has no duplicate handlers");
6285 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6286 assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
6287 }
6288
6289 #[test]
6290 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6291 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
6292 assert_eq!(cfg.enable_mcp_apps, Some(true));
6293
6294 let (wire, _runtime) = cfg
6295 .into_wire(Some(SessionId::from("enable-mcp-apps")))
6296 .expect("enable_mcp_apps config has no duplicate handlers");
6297 assert!(wire.request_mcp_apps);
6298
6299 let json = serde_json::to_value(&wire).unwrap();
6300 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6301 }
6302
6303 #[test]
6304 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6305 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
6306 .with_enable_mcp_apps(true);
6307 assert_eq!(cfg.enable_mcp_apps, Some(true));
6308
6309 let (wire, _runtime) = cfg
6310 .into_wire()
6311 .expect("resume enable_mcp_apps config has no duplicate handlers");
6312 assert!(wire.request_mcp_apps);
6313
6314 let json = serde_json::to_value(&wire).unwrap();
6315 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6316 }
6317
6318 #[test]
6319 fn github_mcp_tool_config_serializes_for_create_and_resume() {
6320 let github_config = GitHubMcpToolConfig::new()
6321 .with_enable_all_tools(true)
6322 .with_additional_toolsets(["repos"])
6323 .with_additional_tools(["get_issue"])
6324 .with_enable_insiders_mode(true)
6325 .with_disable_form_deferral(true);
6326
6327 let (create_wire, _) = SessionConfig::default()
6328 .with_github_mcp_tool_config(github_config.clone())
6329 .into_wire(Some(SessionId::from("github-mcp")))
6330 .expect("create config has no duplicate handlers");
6331 assert_eq!(
6332 serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6333 serde_json::json!({
6334 "enableAllTools": true,
6335 "additionalToolsets": ["repos"],
6336 "additionalTools": ["get_issue"],
6337 "enableInsidersMode": true,
6338 "disableFormDeferral": true,
6339 })
6340 );
6341
6342 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6343 .with_github_mcp_tool_config(github_config)
6344 .into_wire()
6345 .expect("resume config has no duplicate handlers");
6346 assert!(resume_wire.github_mcp_tool_config.is_some());
6347
6348 let (unset_wire, _) = SessionConfig::default()
6349 .into_wire(Some(SessionId::from("github-mcp-unset")))
6350 .expect("default config has no duplicate handlers");
6351 assert!(
6352 serde_json::to_value(&unset_wire)
6353 .unwrap()
6354 .get("githubMcpToolConfig")
6355 .is_none()
6356 );
6357 }
6358
6359 #[test]
6360 fn memory_configuration_constructors_and_serde() {
6361 assert!(MemoryConfiguration::enabled().enabled);
6362 assert!(!MemoryConfiguration::disabled().enabled);
6363 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6364
6365 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6366 assert_eq!(json, serde_json::json!({ "enabled": true }));
6367 }
6368
6369 #[test]
6370 fn session_config_with_memory_serializes() {
6371 let (wire, _runtime) = SessionConfig::default()
6372 .with_memory(MemoryConfiguration::enabled())
6373 .into_wire(Some(SessionId::from("memory-on")))
6374 .expect("no duplicate handlers");
6375 let json = serde_json::to_value(&wire).unwrap();
6376 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6377
6378 let (wire_off, _) = SessionConfig::default()
6379 .with_memory(MemoryConfiguration::disabled())
6380 .into_wire(Some(SessionId::from("memory-off")))
6381 .expect("no duplicate handlers");
6382 let json_off = serde_json::to_value(&wire_off).unwrap();
6383 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6384
6385 let (empty_wire, _) = SessionConfig::default()
6387 .into_wire(Some(SessionId::from("memory-unset")))
6388 .expect("no duplicate handlers");
6389 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6390 assert!(empty_json.get("memory").is_none());
6391 }
6392
6393 #[test]
6394 fn resume_session_config_with_memory_serializes() {
6395 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6396 .with_memory(MemoryConfiguration::enabled())
6397 .into_wire()
6398 .expect("no duplicate handlers");
6399 let json = serde_json::to_value(&wire).unwrap();
6400 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6401
6402 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6404 .into_wire()
6405 .expect("no duplicate handlers");
6406 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6407 assert!(empty_json.get("memory").is_none());
6408 }
6409
6410 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6411 CopilotExpAssignmentResponse {
6412 features: vec!["copilot_exp_flag".to_string()],
6413 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6414 configs: vec![ExpConfigEntry {
6415 id: "cfg-1".to_string(),
6416 parameters: HashMap::from([
6417 ("threshold".to_string(), ExpFlagValue::Integer(5)),
6418 ("enabled".to_string(), ExpFlagValue::Bool(true)),
6419 ]),
6420 }],
6421 assignment_context: context.to_string(),
6422 ..Default::default()
6423 }
6424 }
6425
6426 #[test]
6427 fn exp_flag_value_round_trips_all_variants() {
6428 let values = serde_json::json!({
6429 "s": "text",
6430 "i": 7,
6431 "f": 1.5,
6432 "b": true,
6433 "n": null,
6434 });
6435 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6436 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6437 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6438 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6439 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6440 assert_eq!(parsed["n"], ExpFlagValue::Null);
6441 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6442 }
6443
6444 #[test]
6445 fn session_config_with_exp_assignments_serializes() {
6446 let assignments = sample_exp_assignments("ctx-123");
6447 let expected = serde_json::to_value(&assignments).unwrap();
6448 let (wire, _runtime) = SessionConfig::default()
6449 .with_exp_assignments(assignments)
6450 .into_wire(Some(SessionId::from("exp-on")))
6451 .expect("no duplicate handlers");
6452 let json = serde_json::to_value(&wire).unwrap();
6453 assert_eq!(json["expAssignments"], expected);
6454 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6455 assert_eq!(
6456 json["expAssignments"]["Flights"]["copilot_exp_flag"],
6457 "treatment"
6458 );
6459
6460 let (empty_wire, _) = SessionConfig::default()
6462 .into_wire(Some(SessionId::from("exp-unset")))
6463 .expect("no duplicate handlers");
6464 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6465 assert!(empty_json.get("expAssignments").is_none());
6466 }
6467
6468 #[test]
6469 fn resume_session_config_with_exp_assignments_serializes() {
6470 let assignments = sample_exp_assignments("ctx-456");
6471 let expected = serde_json::to_value(&assignments).unwrap();
6472 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6473 .with_exp_assignments(assignments)
6474 .into_wire()
6475 .expect("no duplicate handlers");
6476 let json = serde_json::to_value(&wire).unwrap();
6477 assert_eq!(json["expAssignments"], expected);
6478
6479 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6481 .into_wire()
6482 .expect("no duplicate handlers");
6483 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6484 assert!(empty_json.get("expAssignments").is_none());
6485 }
6486
6487 #[test]
6488 fn session_config_clone_preserves_exp_assignments() {
6489 let assignments = sample_exp_assignments("ctx-clone");
6490 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6491 let cloned = config.clone();
6492
6493 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6494
6495 let (wire, _runtime) = cloned
6496 .into_wire(Some(SessionId::from("exp-clone")))
6497 .expect("no duplicate handlers");
6498 let json = serde_json::to_value(&wire).unwrap();
6499 assert_eq!(
6500 json["expAssignments"],
6501 serde_json::to_value(&assignments).unwrap()
6502 );
6503 }
6504
6505 #[test]
6506 fn resume_session_config_clone_preserves_exp_assignments() {
6507 let assignments = sample_exp_assignments("ctx-clone-resume");
6508 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6509 .with_exp_assignments(assignments.clone());
6510 let cloned = config.clone();
6511
6512 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6513
6514 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6515 let json = serde_json::to_value(&wire).unwrap();
6516 assert_eq!(
6517 json["expAssignments"],
6518 serde_json::to_value(&assignments).unwrap()
6519 );
6520 }
6521
6522 #[test]
6523 #[allow(clippy::field_reassign_with_default)]
6524 fn session_config_into_wire_serializes_bucket_b_fields() {
6525 use std::path::PathBuf;
6526
6527 use super::{CloudSessionOptions, CloudSessionRepository};
6528
6529 let mut cfg = SessionConfig::default();
6530 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6531 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6532 cfg.github_token = Some("ghs_secret".to_string());
6533 cfg.include_sub_agent_streaming_events = Some(false);
6534 cfg.enable_session_telemetry = Some(false);
6535 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
6536 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
6537 cfg.enable_on_demand_instruction_discovery = Some(false);
6538 cfg.cloud = Some(CloudSessionOptions::with_repository(
6539 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
6540 ));
6541
6542 let (wire, _runtime) = cfg
6543 .into_wire(Some(SessionId::from("custom-id")))
6544 .expect("no duplicate handlers");
6545 let wire_json = serde_json::to_value(&wire).unwrap();
6546 assert_eq!(wire_json["sessionId"], "custom-id");
6547 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6548 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6549 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6550 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6551 assert_eq!(wire_json["enableSessionTelemetry"], false);
6552 assert_eq!(wire_json["reasoningSummary"], "concise");
6553 assert_eq!(wire_json["remoteSession"], "export");
6554 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6555 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6556 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6557 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6558
6559 let (empty_wire, _) = SessionConfig::default()
6561 .into_wire(Some(SessionId::from("empty")))
6562 .expect("default has no duplicate handlers");
6563 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6564 assert!(empty_json.get("gitHubToken").is_none());
6565 assert!(empty_json.get("enableSessionTelemetry").is_none());
6566 assert!(empty_json.get("reasoningSummary").is_none());
6567 assert!(empty_json.get("remoteSession").is_none());
6568 assert!(
6569 empty_json
6570 .get("enableOnDemandInstructionDiscovery")
6571 .is_none()
6572 );
6573 assert!(empty_json.get("cloud").is_none());
6574 }
6575
6576 #[test]
6577 fn session_config_into_wire_serializes_named_providers_and_models() {
6578 let cfg = SessionConfig::default()
6579 .with_providers(vec![
6580 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6581 .with_provider_type("openai")
6582 .with_wire_api("responses")
6583 .with_api_key("sk-test"),
6584 ])
6585 .with_models(vec![
6586 ProviderModelConfig::new("gpt-x", "my-openai")
6587 .with_wire_model("gpt-x-2025")
6588 .with_max_output_tokens(2048),
6589 ]);
6590
6591 let (wire, _) = cfg
6592 .into_wire(Some(SessionId::from("sess-providers")))
6593 .expect("no duplicate handlers");
6594 let wire_json = serde_json::to_value(&wire).unwrap();
6595 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6596 assert_eq!(
6597 wire_json["providers"][0]["baseUrl"],
6598 "https://api.example.com/v1"
6599 );
6600 assert_eq!(wire_json["providers"][0]["type"], "openai");
6601 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6602 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6603 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
6604 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
6605 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
6606 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
6607
6608 let (empty_wire, _) = SessionConfig::default()
6609 .into_wire(Some(SessionId::from("empty")))
6610 .expect("default has no duplicate handlers");
6611 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6612 assert!(empty_json.get("providers").is_none());
6613 assert!(empty_json.get("models").is_none());
6614 }
6615
6616 #[test]
6617 fn resume_config_into_wire_serializes_named_providers_and_models() {
6618 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
6619 .with_providers(vec![
6620 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
6621 .with_provider_type("azure")
6622 .with_azure(AzureProviderOptions {
6623 api_version: Some("2024-10-21".to_string()),
6624 }),
6625 ])
6626 .with_models(vec![
6627 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
6628 ]);
6629
6630 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6631 let wire_json = serde_json::to_value(&wire).unwrap();
6632 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
6633 assert_eq!(wire_json["providers"][0]["type"], "azure");
6634 assert_eq!(
6635 wire_json["providers"][0]["azure"]["apiVersion"],
6636 "2024-10-21"
6637 );
6638 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
6639 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
6640 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
6641
6642 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
6643 .into_wire()
6644 .expect("default has no duplicate handlers");
6645 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6646 assert!(empty_json.get("providers").is_none());
6647 assert!(empty_json.get("models").is_none());
6648 }
6649
6650 #[test]
6651 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
6652 use std::path::PathBuf;
6653
6654 let cfg = SessionConfig {
6655 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
6656 disabled_mcp_servers: Some(vec![
6657 "local-files".to_string(),
6658 "remote-github".to_string(),
6659 ]),
6660 large_output: Some(
6661 LargeToolOutputConfig::new()
6662 .with_enabled(true)
6663 .with_max_size_bytes(1024)
6664 .with_output_directory(PathBuf::from("/tmp/large-output")),
6665 ),
6666 ..Default::default()
6667 };
6668
6669 let (wire, _) = cfg
6670 .into_wire(Some(SessionId::from("sess-1")))
6671 .expect("no duplicate handlers");
6672 let wire_json = serde_json::to_value(&wire).unwrap();
6673 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
6674 assert_eq!(
6675 wire_json["disabledMcpServers"],
6676 serde_json::json!(["local-files", "remote-github"])
6677 );
6678 assert_eq!(wire_json["largeOutput"]["enabled"], true);
6679 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
6680 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
6681
6682 let (empty_wire, _) = SessionConfig::default()
6683 .into_wire(Some(SessionId::from("empty")))
6684 .expect("default has no duplicate handlers");
6685 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6686 assert!(empty_json.get("pluginDirectories").is_none());
6687 assert!(empty_json.get("disabledMcpServers").is_none());
6688 assert!(empty_json.get("largeOutput").is_none());
6689 }
6690
6691 #[test]
6692 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
6693 use std::path::PathBuf;
6694
6695 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6696 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6697 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6698 cfg.github_token = Some("ghs_secret".to_string());
6699 cfg.include_sub_agent_streaming_events = Some(true);
6700 cfg.enable_session_telemetry = Some(false);
6701 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
6702 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
6703 cfg.enable_on_demand_instruction_discovery = Some(false);
6704
6705 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6706 let wire_json = serde_json::to_value(&wire).unwrap();
6707 assert_eq!(wire_json["sessionId"], "sess-1");
6708 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6709 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6710 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6711 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
6712 assert_eq!(wire_json["enableSessionTelemetry"], false);
6713 assert_eq!(wire_json["reasoningSummary"], "detailed");
6714 assert_eq!(wire_json["remoteSession"], "on");
6715 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6716
6717 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6719 .into_wire()
6720 .expect("default resume has no duplicate handlers");
6721 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6722 assert!(empty_json.get("reasoningSummary").is_none());
6723 assert!(empty_json.get("remoteSession").is_none());
6724 assert!(
6725 empty_json
6726 .get("enableOnDemandInstructionDiscovery")
6727 .is_none()
6728 );
6729 }
6730
6731 #[test]
6732 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
6733 use std::path::PathBuf;
6734
6735 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6736 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6737 cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]);
6738 cfg.large_output = Some(
6739 LargeToolOutputConfig::new()
6740 .with_enabled(false)
6741 .with_max_size_bytes(2048)
6742 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6743 );
6744
6745 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6746 let wire_json = serde_json::to_value(&wire).unwrap();
6747 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6748 assert_eq!(
6749 wire_json["disabledMcpServers"],
6750 serde_json::json!(["local-files-r"])
6751 );
6752 assert_eq!(wire_json["largeOutput"]["enabled"], false);
6753 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6754 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6755
6756 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6757 .into_wire()
6758 .expect("default resume has no duplicate handlers");
6759 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6760 assert!(empty_json.get("pluginDirectories").is_none());
6761 assert!(empty_json.get("disabledMcpServers").is_none());
6762 assert!(empty_json.get("largeOutput").is_none());
6763 }
6764
6765 #[test]
6766 fn session_config_clones_disabled_mcp_servers() {
6767 let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]);
6768 let mut create_clone = create.clone();
6769 create_clone
6770 .disabled_mcp_servers
6771 .as_mut()
6772 .expect("configured disabled MCP servers")
6773 .push("remote-github".to_string());
6774 assert_eq!(
6775 create.disabled_mcp_servers.as_deref(),
6776 Some(&["local-files".to_string()][..])
6777 );
6778
6779 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6780 .with_disabled_mcp_servers(["local-files"]);
6781 let mut resume_clone = resume.clone();
6782 resume_clone
6783 .disabled_mcp_servers
6784 .as_mut()
6785 .expect("configured disabled MCP servers")
6786 .push("remote-github".to_string());
6787 assert_eq!(
6788 resume.disabled_mcp_servers.as_deref(),
6789 Some(&["local-files".to_string()][..])
6790 );
6791 }
6792
6793 #[test]
6794 fn session_config_builder_composes() {
6795 use indexmap::IndexMap;
6796
6797 let cfg = SessionConfig::default()
6798 .with_session_id(SessionId::from("sess-1"))
6799 .with_model("claude-sonnet-4")
6800 .with_client_name("test-app")
6801 .with_reasoning_effort("medium")
6802 .with_reasoning_summary(ReasoningSummary::Concise)
6803 .with_context_tier("long_context")
6804 .with_streaming(true)
6805 .with_tools([Tool::new("greet")])
6806 .with_available_tools(["bash", "view"])
6807 .with_excluded_tools(["dangerous"])
6808 .with_mcp_servers(IndexMap::new())
6809 .with_mcp_oauth_token_storage("persistent")
6810 .with_enable_config_discovery(true)
6811 .with_enable_on_demand_instruction_discovery(true)
6812 .with_skill_directories([PathBuf::from("/tmp/skills")])
6813 .with_disabled_skills(["broken-skill"])
6814 .with_disabled_mcp_servers(["local-files"])
6815 .with_agent("researcher")
6816 .with_config_directory(PathBuf::from("/tmp/config"))
6817 .with_working_directory(PathBuf::from("/tmp/work"))
6818 .with_additional_directories([PathBuf::from("/tmp/shared")])
6819 .with_github_token("ghp_test")
6820 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6821 .with_enable_session_telemetry(false)
6822 .with_include_sub_agent_streaming_events(false)
6823 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6824
6825 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
6826 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
6827 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6828 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
6829 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
6830 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
6831 assert_eq!(cfg.streaming, Some(true));
6832 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6833 assert_eq!(
6834 cfg.available_tools.as_deref(),
6835 Some(&["bash".to_string(), "view".to_string()][..])
6836 );
6837 assert_eq!(
6838 cfg.excluded_tools.as_deref(),
6839 Some(&["dangerous".to_string()][..])
6840 );
6841 assert!(cfg.mcp_servers.is_some());
6842 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6843 assert_eq!(cfg.enable_config_discovery, Some(true));
6844 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
6845 assert_eq!(
6846 cfg.skill_directories.as_deref(),
6847 Some(&[PathBuf::from("/tmp/skills")][..])
6848 );
6849 assert_eq!(
6850 cfg.disabled_skills.as_deref(),
6851 Some(&["broken-skill".to_string()][..])
6852 );
6853 assert_eq!(
6854 cfg.disabled_mcp_servers.as_deref(),
6855 Some(&["local-files".to_string()][..])
6856 );
6857 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6858 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6859 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6860 assert_eq!(
6861 cfg.additional_directories.as_deref(),
6862 Some(&[PathBuf::from("/tmp/shared")][..])
6863 );
6864 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6865 assert_eq!(
6866 cfg.capi,
6867 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6868 );
6869 assert_eq!(cfg.enable_session_telemetry, Some(false));
6870 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
6871 assert_eq!(
6872 cfg.extension_info,
6873 Some(ExtensionInfo::new("github-app", "counter"))
6874 );
6875 }
6876
6877 #[test]
6878 fn resume_session_config_builder_composes() {
6879 use indexmap::IndexMap;
6880
6881 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
6882 .with_client_name("test-app")
6883 .with_reasoning_summary(ReasoningSummary::None)
6884 .with_context_tier("default")
6885 .with_streaming(true)
6886 .with_tools([Tool::new("greet")])
6887 .with_available_tools(["bash", "view"])
6888 .with_excluded_tools(["dangerous"])
6889 .with_mcp_servers(IndexMap::new())
6890 .with_mcp_oauth_token_storage("persistent")
6891 .with_enable_config_discovery(true)
6892 .with_enable_on_demand_instruction_discovery(false)
6893 .with_skill_directories([PathBuf::from("/tmp/skills")])
6894 .with_disabled_skills(["broken-skill"])
6895 .with_disabled_mcp_servers(["local-files"])
6896 .with_agent("researcher")
6897 .with_config_directory(PathBuf::from("/tmp/config"))
6898 .with_working_directory(PathBuf::from("/tmp/work"))
6899 .with_additional_directories([PathBuf::from("/tmp/shared")])
6900 .with_github_token("ghp_test")
6901 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6902 .with_enable_session_telemetry(false)
6903 .with_include_sub_agent_streaming_events(true)
6904 .with_suppress_resume_event(true)
6905 .with_continue_pending_work(true)
6906 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6907
6908 assert_eq!(cfg.session_id.as_str(), "sess-2");
6909 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6910 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
6911 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
6912 assert_eq!(cfg.streaming, Some(true));
6913 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6914 assert_eq!(
6915 cfg.available_tools.as_deref(),
6916 Some(&["bash".to_string(), "view".to_string()][..])
6917 );
6918 assert_eq!(
6919 cfg.excluded_tools.as_deref(),
6920 Some(&["dangerous".to_string()][..])
6921 );
6922 assert!(cfg.mcp_servers.is_some());
6923 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6924 assert_eq!(cfg.enable_config_discovery, Some(true));
6925 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
6926 assert_eq!(
6927 cfg.skill_directories.as_deref(),
6928 Some(&[PathBuf::from("/tmp/skills")][..])
6929 );
6930 assert_eq!(
6931 cfg.disabled_skills.as_deref(),
6932 Some(&["broken-skill".to_string()][..])
6933 );
6934 assert_eq!(
6935 cfg.disabled_mcp_servers.as_deref(),
6936 Some(&["local-files".to_string()][..])
6937 );
6938 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6939 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6940 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6941 assert_eq!(
6942 cfg.additional_directories.as_deref(),
6943 Some(&[PathBuf::from("/tmp/shared")][..])
6944 );
6945 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6946 assert_eq!(
6947 cfg.capi,
6948 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6949 );
6950 assert_eq!(cfg.enable_session_telemetry, Some(false));
6951 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
6952 assert_eq!(cfg.suppress_resume_event, Some(true));
6953 assert_eq!(cfg.continue_pending_work, Some(true));
6954 assert_eq!(
6955 cfg.extension_info,
6956 Some(ExtensionInfo::new("github-app", "counter"))
6957 );
6958 }
6959
6960 #[test]
6964 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
6965 let cfg =
6966 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
6967 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6968 let json = serde_json::to_value(&wire).unwrap();
6969 assert_eq!(json["continuePendingWork"], true);
6970
6971 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6973 .into_wire()
6974 .expect("no duplicate handlers");
6975 let json = serde_json::to_value(&wire).unwrap();
6976 assert!(json.get("continuePendingWork").is_none());
6977 }
6978
6979 #[test]
6980 fn session_configs_serialize_additional_directories() {
6981 let create = SessionConfig::default().with_additional_directories([
6982 PathBuf::from("/tmp/shared"),
6983 PathBuf::from("/tmp/generated"),
6984 ]);
6985 let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
6986 let create_json = serde_json::to_value(&create_wire).unwrap();
6987 assert_eq!(
6988 create_json["additionalDirectories"],
6989 serde_json::json!(["/tmp/shared", "/tmp/generated"])
6990 );
6991
6992 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6993 .with_additional_directories([PathBuf::from("/tmp/resumed")]);
6994 let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
6995 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6996 assert_eq!(
6997 resume_json["additionalDirectories"],
6998 serde_json::json!(["/tmp/resumed"])
6999 );
7000 }
7001
7002 #[test]
7006 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
7007 let cfg =
7008 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
7009 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7010 let json = serde_json::to_value(&wire).unwrap();
7011 assert_eq!(json["disableResume"], true);
7012 assert!(json.get("suppressResumeEvent").is_none());
7013 }
7014
7015 #[test]
7018 fn session_config_serializes_instruction_directories_to_camel_case() {
7019 let cfg =
7020 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
7021 let (wire, _) = cfg
7022 .into_wire(Some(SessionId::from("instr-on")))
7023 .expect("no duplicate handlers");
7024 let json = serde_json::to_value(&wire).unwrap();
7025 assert_eq!(
7026 json["instructionDirectories"],
7027 serde_json::json!(["/tmp/instr"])
7028 );
7029
7030 let (wire, _) = SessionConfig::default()
7032 .into_wire(Some(SessionId::from("instr-off")))
7033 .expect("no duplicate handlers");
7034 let json = serde_json::to_value(&wire).unwrap();
7035 assert!(json.get("instructionDirectories").is_none());
7036 }
7037
7038 #[test]
7041 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
7042 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
7043 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
7044 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7045 let json = serde_json::to_value(&wire).unwrap();
7046 assert_eq!(
7047 json["instructionDirectories"],
7048 serde_json::json!(["/tmp/instr"])
7049 );
7050
7051 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7052 .into_wire()
7053 .expect("no duplicate handlers");
7054 let json = serde_json::to_value(&wire).unwrap();
7055 assert!(json.get("instructionDirectories").is_none());
7056 }
7057
7058 #[test]
7059 fn custom_agent_config_builder_composes() {
7060 use indexmap::IndexMap;
7061
7062 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
7063 .with_display_name("Research Assistant")
7064 .with_description("Investigates technical questions.")
7065 .with_tools(["bash", "view"])
7066 .with_mcp_servers(IndexMap::new())
7067 .with_infer(true)
7068 .with_skills(["rust-coding-skill"]);
7069
7070 assert_eq!(cfg.name, "researcher");
7071 assert_eq!(cfg.prompt, "You are a research assistant.");
7072 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
7073 assert_eq!(
7074 cfg.description.as_deref(),
7075 Some("Investigates technical questions.")
7076 );
7077 assert_eq!(
7078 cfg.tools.as_deref(),
7079 Some(&["bash".to_string(), "view".to_string()][..])
7080 );
7081 assert!(cfg.mcp_servers.is_some());
7082 assert_eq!(cfg.infer, Some(true));
7083 assert_eq!(
7084 cfg.skills.as_deref(),
7085 Some(&["rust-coding-skill".to_string()][..])
7086 );
7087 }
7088
7089 #[test]
7090 fn mcp_servers_serialize_in_insertion_order() {
7091 use indexmap::IndexMap;
7092
7093 let order = [
7099 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
7100 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
7101 ];
7102 let mut servers = IndexMap::new();
7103 for name in order {
7104 servers.insert(
7105 name.to_string(),
7106 McpServerConfig::Stdio(McpStdioServerConfig {
7107 command: "run".to_string(),
7108 ..Default::default()
7109 }),
7110 );
7111 }
7112
7113 let (wire, _runtime) = SessionConfig::default()
7114 .with_mcp_servers(servers)
7115 .into_wire(None)
7116 .expect("into_wire should succeed");
7117 let json = serde_json::to_string(&wire).expect("serialize wire");
7118
7119 let positions: Vec<usize> = order
7120 .iter()
7121 .map(|name| {
7122 json.find(&format!("\"{name}\""))
7123 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
7124 })
7125 .collect();
7126 let mut ascending = positions.clone();
7127 ascending.sort_unstable();
7128 assert_eq!(
7129 positions, ascending,
7130 "mcp server keys must serialize in insertion order: {json}"
7131 );
7132 }
7133
7134 #[test]
7135 fn infinite_session_config_builder_composes() {
7136 let cfg = InfiniteSessionConfig::new()
7137 .with_enabled(true)
7138 .with_background_compaction_threshold(0.75)
7139 .with_buffer_exhaustion_threshold(0.92);
7140
7141 assert_eq!(cfg.enabled, Some(true));
7142 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
7143 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
7144 }
7145
7146 #[test]
7147 fn provider_config_builder_composes() {
7148 use std::collections::HashMap;
7149
7150 let mut headers = HashMap::new();
7151 headers.insert("X-Custom".to_string(), "value".to_string());
7152
7153 let cfg = ProviderConfig::new("https://api.example.com")
7154 .with_provider_type("openai")
7155 .with_wire_api("completions")
7156 .with_transport("websockets")
7157 .with_api_key("sk-test")
7158 .with_bearer_token("bearer-test")
7159 .with_headers(headers)
7160 .with_model_id("gpt-4")
7161 .with_wire_model("azure-gpt-4-deployment")
7162 .with_max_prompt_tokens(8192)
7163 .with_max_output_tokens(2048);
7164
7165 assert_eq!(cfg.base_url, "https://api.example.com");
7166 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
7167 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
7168 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
7169 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
7170 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
7171 assert_eq!(
7172 cfg.headers
7173 .as_ref()
7174 .and_then(|h| h.get("X-Custom"))
7175 .map(String::as_str),
7176 Some("value"),
7177 );
7178 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
7179 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
7180 assert_eq!(cfg.max_prompt_tokens, Some(8192));
7181 assert_eq!(cfg.max_output_tokens, Some(2048));
7182
7183 let wire = serde_json::to_value(&cfg).unwrap();
7185 assert_eq!(wire["modelId"], "gpt-4");
7186 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
7187 assert_eq!(wire["maxPromptTokens"], 8192);
7188 assert_eq!(wire["maxOutputTokens"], 2048);
7189
7190 let unset = ProviderConfig::new("https://api.example.com");
7191 let wire_unset = serde_json::to_value(&unset).unwrap();
7192 assert!(wire_unset.get("modelId").is_none());
7193 assert!(wire_unset.get("wireModel").is_none());
7194 assert!(wire_unset.get("maxPromptTokens").is_none());
7195 assert!(wire_unset.get("maxOutputTokens").is_none());
7196 }
7197
7198 #[test]
7199 fn capi_session_options_builder_composes_and_serializes() {
7200 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
7201
7202 assert_eq!(cfg.enable_web_socket_responses, Some(false));
7203
7204 let wire = serde_json::to_value(&cfg).unwrap();
7205 assert_eq!(
7206 wire,
7207 serde_json::json!({ "enableWebSocketResponses": false })
7208 );
7209
7210 let unset = CapiSessionOptions::new();
7211 let wire_unset = serde_json::to_value(&unset).unwrap();
7212 assert!(wire_unset.get("enableWebSocketResponses").is_none());
7213 }
7214
7215 #[test]
7216 fn session_config_with_capi_serializes() {
7217 let (wire, _) = SessionConfig::default()
7218 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7219 .into_wire(Some(SessionId::from("capi-create")))
7220 .expect("no duplicate handlers");
7221 let json = serde_json::to_value(&wire).unwrap();
7222 assert_eq!(
7223 json["capi"],
7224 serde_json::json!({ "enableWebSocketResponses": false })
7225 );
7226
7227 let (empty_wire, _) = SessionConfig::default()
7228 .into_wire(Some(SessionId::from("capi-create-unset")))
7229 .expect("no duplicate handlers");
7230 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7231 assert!(empty_json.get("capi").is_none());
7232 }
7233
7234 #[test]
7235 fn resume_session_config_with_capi_serializes() {
7236 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7237 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7238 .into_wire()
7239 .expect("no duplicate handlers");
7240 let json = serde_json::to_value(&wire).unwrap();
7241 assert_eq!(
7242 json["capi"],
7243 serde_json::json!({ "enableWebSocketResponses": false })
7244 );
7245
7246 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
7247 .into_wire()
7248 .expect("no duplicate handlers");
7249 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7250 assert!(empty_json.get("capi").is_none());
7251 }
7252
7253 #[test]
7254 fn system_message_config_builder_composes() {
7255 use std::collections::HashMap;
7256
7257 let cfg = SystemMessageConfig::new()
7258 .with_mode("replace")
7259 .with_content("Custom system message.")
7260 .with_sections(HashMap::new());
7261
7262 assert_eq!(cfg.mode.as_deref(), Some("replace"));
7263 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
7264 assert!(cfg.sections.is_some());
7265 }
7266
7267 #[test]
7268 fn delivery_mode_serializes_to_kebab_case_strings() {
7269 assert_eq!(
7270 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
7271 "\"enqueue\""
7272 );
7273 assert_eq!(
7274 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
7275 "\"immediate\""
7276 );
7277 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
7278 assert_eq!(parsed, DeliveryMode::Immediate);
7279 }
7280
7281 #[test]
7282 fn agent_mode_serializes_to_kebab_case_strings() {
7283 assert_eq!(
7284 serde_json::to_string(&AgentMode::Interactive).unwrap(),
7285 "\"interactive\""
7286 );
7287 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
7288 assert_eq!(
7289 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
7290 "\"autopilot\""
7291 );
7292 assert_eq!(
7293 serde_json::to_string(&AgentMode::Shell).unwrap(),
7294 "\"shell\""
7295 );
7296 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
7297 assert_eq!(parsed, AgentMode::Plan);
7298 }
7299
7300 #[test]
7301 fn connection_state_distinguishes_variants() {
7302 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
7305 }
7306
7307 #[test]
7313 fn session_event_round_trips_agent_id_on_envelope() {
7314 let wire = json!({
7315 "id": "evt-1",
7316 "timestamp": "2026-04-30T12:00:00Z",
7317 "parentId": null,
7318 "agentId": "sub-agent-42",
7319 "type": "assistant.message",
7320 "data": { "message": "hi" }
7321 });
7322
7323 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
7324 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7325
7326 let roundtripped = serde_json::to_value(&event).unwrap();
7328 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7329
7330 let main_agent_event: SessionEvent = serde_json::from_value(json!({
7332 "id": "evt-2",
7333 "timestamp": "2026-04-30T12:00:01Z",
7334 "parentId": null,
7335 "type": "session.idle",
7336 "data": {}
7337 }))
7338 .unwrap();
7339 assert!(main_agent_event.agent_id.is_none());
7340 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
7341 assert!(roundtripped.get("agentId").is_none());
7342 }
7343
7344 #[test]
7346 fn typed_session_event_round_trips_agent_id_on_envelope() {
7347 let wire = json!({
7348 "id": "evt-1",
7349 "timestamp": "2026-04-30T12:00:00Z",
7350 "parentId": null,
7351 "agentId": "sub-agent-42",
7352 "type": "session.idle",
7353 "data": {}
7354 });
7355
7356 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
7357 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7358
7359 let roundtripped = serde_json::to_value(&event).unwrap();
7360 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7361 }
7362
7363 #[test]
7364 fn connection_state_variants_compile() {
7365 let _ = ConnectionState::Disconnected;
7369 let _ = ConnectionState::Connecting;
7370 let _ = ConnectionState::Connected;
7371 let _ = ConnectionState::Error;
7372 }
7373
7374 #[test]
7375 fn deserializes_runtime_attachment_variants() {
7376 let attachments: Vec<Attachment> = serde_json::from_value(json!([
7377 {
7378 "type": "file",
7379 "path": "/tmp/file.rs",
7380 "displayName": "file.rs",
7381 "lineRange": { "start": 7, "end": 12 }
7382 },
7383 {
7384 "type": "directory",
7385 "path": "/tmp/project",
7386 "displayName": "project"
7387 },
7388 {
7389 "type": "selection",
7390 "filePath": "/tmp/lib.rs",
7391 "displayName": "lib.rs",
7392 "text": "fn main() {}",
7393 "selection": {
7394 "start": { "line": 1, "character": 2 },
7395 "end": { "line": 3, "character": 4 }
7396 }
7397 },
7398 {
7399 "type": "blob",
7400 "data": "Zm9v",
7401 "mimeType": "image/png",
7402 "displayName": "image.png"
7403 },
7404 {
7405 "type": "github_reference",
7406 "number": 42,
7407 "title": "Fix rendering",
7408 "referenceType": "issue",
7409 "state": "open",
7410 "url": "https://github.com/example/repo/issues/42"
7411 }
7412 ]))
7413 .expect("attachments should deserialize");
7414
7415 assert_eq!(attachments.len(), 5);
7416 assert!(matches!(
7417 &attachments[0],
7418 Attachment::File {
7419 path,
7420 display_name,
7421 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7422 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7423 ));
7424 assert!(matches!(
7425 &attachments[1],
7426 Attachment::Directory { path, display_name }
7427 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7428 ));
7429 assert!(matches!(
7430 &attachments[2],
7431 Attachment::Selection {
7432 file_path,
7433 display_name,
7434 selection:
7435 AttachmentSelectionRange {
7436 start: AttachmentSelectionPosition { line: 1, character: 2 },
7437 end: AttachmentSelectionPosition { line: 3, character: 4 },
7438 },
7439 ..
7440 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
7441 ));
7442 assert!(matches!(
7443 &attachments[3],
7444 Attachment::Blob {
7445 data,
7446 mime_type,
7447 display_name,
7448 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
7449 ));
7450 assert!(matches!(
7451 &attachments[4],
7452 Attachment::GitHubReference {
7453 number: 42,
7454 title,
7455 reference_type: GitHubReferenceType::Issue,
7456 state,
7457 url,
7458 } if title == "Fix rendering"
7459 && state == "open"
7460 && url == "https://github.com/example/repo/issues/42"
7461 ));
7462 }
7463
7464 #[test]
7465 fn ensures_display_names_for_variants_that_support_them() {
7466 let mut attachments = vec![
7467 Attachment::File {
7468 path: PathBuf::from("/tmp/file.rs"),
7469 display_name: None,
7470 line_range: None,
7471 },
7472 Attachment::Selection {
7473 file_path: PathBuf::from("/tmp/src/lib.rs"),
7474 display_name: None,
7475 text: "fn main() {}".to_string(),
7476 selection: AttachmentSelectionRange {
7477 start: AttachmentSelectionPosition {
7478 line: 0,
7479 character: 0,
7480 },
7481 end: AttachmentSelectionPosition {
7482 line: 0,
7483 character: 10,
7484 },
7485 },
7486 },
7487 Attachment::Blob {
7488 data: "Zm9v".to_string(),
7489 mime_type: "image/png".to_string(),
7490 display_name: None,
7491 },
7492 Attachment::GitHubReference {
7493 number: 7,
7494 title: "Track regressions".to_string(),
7495 reference_type: GitHubReferenceType::Issue,
7496 state: "open".to_string(),
7497 url: "https://example.com/issues/7".to_string(),
7498 },
7499 ];
7500
7501 ensure_attachment_display_names(&mut attachments);
7502
7503 assert_eq!(attachments[0].display_name(), Some("file.rs"));
7504 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
7505 assert_eq!(attachments[2].display_name(), Some("attachment"));
7506 assert_eq!(attachments[3].display_name(), None);
7507 assert_eq!(
7508 attachments[3].label(),
7509 Some("Track regressions".to_string())
7510 );
7511 }
7512
7513 #[test]
7514 fn github_anchored_attachment_variants_round_trip() {
7515 let cases = vec![
7516 (
7517 "github_commit",
7518 json!({
7519 "type": "github_commit",
7520 "message": "Fix the thing",
7521 "oid": "abc123",
7522 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
7523 "url": "https://github.com/octocat/repo/commit/abc123"
7524 }),
7525 ),
7526 (
7527 "github_release",
7528 json!({
7529 "type": "github_release",
7530 "name": "v1.2.3",
7531 "repo": { "name": "repo", "owner": "octocat" },
7532 "tagName": "v1.2.3",
7533 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
7534 }),
7535 ),
7536 (
7537 "github_actions_job",
7538 json!({
7539 "type": "github_actions_job",
7540 "conclusion": "failure",
7541 "jobId": 99,
7542 "jobName": "build",
7543 "repo": { "name": "repo", "owner": "octocat" },
7544 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
7545 "workflowName": "CI"
7546 }),
7547 ),
7548 (
7549 "github_repository",
7550 json!({
7551 "type": "github_repository",
7552 "description": "An example repository",
7553 "ref": "main",
7554 "repo": { "name": "repo", "owner": "octocat" },
7555 "url": "https://github.com/octocat/repo"
7556 }),
7557 ),
7558 (
7559 "github_file_diff",
7560 json!({
7561 "type": "github_file_diff",
7562 "base": {
7563 "path": "src/lib.rs",
7564 "ref": "main",
7565 "repo": { "name": "repo", "owner": "octocat" }
7566 },
7567 "head": {
7568 "path": "src/lib.rs",
7569 "ref": "feature",
7570 "repo": { "name": "repo", "owner": "octocat" }
7571 },
7572 "url": "https://github.com/octocat/repo/compare/main...feature"
7573 }),
7574 ),
7575 (
7576 "github_tree_comparison",
7577 json!({
7578 "type": "github_tree_comparison",
7579 "base": {
7580 "repo": { "name": "repo", "owner": "octocat" },
7581 "revision": "main"
7582 },
7583 "head": {
7584 "repo": { "name": "repo", "owner": "octocat" },
7585 "revision": "feature"
7586 },
7587 "url": "https://github.com/octocat/repo/compare/main...feature"
7588 }),
7589 ),
7590 (
7591 "github_url",
7592 json!({
7593 "type": "github_url",
7594 "url": "https://github.com/octocat/repo/wiki"
7595 }),
7596 ),
7597 (
7598 "github_file",
7599 json!({
7600 "type": "github_file",
7601 "path": "src/main.rs",
7602 "ref": "main",
7603 "repo": { "name": "repo", "owner": "octocat" },
7604 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
7605 }),
7606 ),
7607 (
7608 "github_snippet",
7609 json!({
7610 "type": "github_snippet",
7611 "lineRange": { "start": 10, "end": 20 },
7612 "path": "src/main.rs",
7613 "ref": "main",
7614 "repo": { "name": "repo", "owner": "octocat" },
7615 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
7616 }),
7617 ),
7618 ];
7619
7620 for (expected_type, input) in cases {
7621 let attachment: Attachment = serde_json::from_value(input.clone())
7622 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
7623
7624 let serialized_string = serde_json::to_string(&attachment)
7629 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
7630
7631 assert_eq!(
7633 serialized_string.matches("\"type\":").count(),
7634 1,
7635 "{expected_type} must serialize a single `type` key"
7636 );
7637
7638 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
7639 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
7640 assert_eq!(
7641 serialized.get("type").and_then(|value| value.as_str()),
7642 Some(expected_type),
7643 "{expected_type} must serialize the correct discriminator"
7644 );
7645
7646 assert_eq!(
7648 serialized, input,
7649 "{expected_type} should round-trip without data loss"
7650 );
7651 let reparsed: Attachment = serde_json::from_value(serialized)
7652 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
7653 assert_eq!(
7654 reparsed, attachment,
7655 "{expected_type} should re-deserialize to the same value"
7656 );
7657 }
7658 }
7659}
7660
7661#[cfg(test)]
7662mod permission_builder_tests {
7663 use std::sync::Arc;
7664
7665 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
7666 use crate::permission;
7667 use crate::types::{
7668 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
7669 SessionId,
7670 };
7671
7672 fn data() -> PermissionRequestData {
7673 PermissionRequestData {
7674 extra: serde_json::json!({"tool": "shell"}),
7675 ..Default::default()
7676 }
7677 }
7678
7679 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7682 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7683 }
7684
7685 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7686 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7687 }
7688
7689 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
7690 handler
7691 .handle(SessionId::from("s1"), RequestId::new("1"), data())
7692 .await
7693 }
7694
7695 #[tokio::test]
7696 async fn approve_all_with_handler_present_approves() {
7697 let cfg = SessionConfig::default()
7698 .with_permission_handler(Arc::new(ApproveAllHandler))
7699 .approve_all_permissions();
7700 let h = resolve_create(cfg).expect("policy + handler yields handler");
7701 assert!(matches!(
7702 dispatch(&h).await,
7703 PermissionResult::Decision {
7704 decision: PermissionDecision::ApproveOnce(_),
7705 ..
7706 }
7707 ));
7708 }
7709
7710 #[tokio::test]
7711 async fn approve_all_standalone_produces_handler() {
7712 let cfg = SessionConfig::default().approve_all_permissions();
7713 let h = resolve_create(cfg).expect("policy alone yields handler");
7714 assert!(matches!(
7715 dispatch(&h).await,
7716 PermissionResult::Decision {
7717 decision: PermissionDecision::ApproveOnce(_),
7718 ..
7719 }
7720 ));
7721 }
7722
7723 #[tokio::test]
7726 async fn approve_all_is_order_independent() {
7727 let a = SessionConfig::default()
7728 .with_permission_handler(Arc::new(ApproveAllHandler))
7729 .approve_all_permissions();
7730 let b = SessionConfig::default()
7731 .approve_all_permissions()
7732 .with_permission_handler(Arc::new(ApproveAllHandler));
7733 let ha = resolve_create(a).unwrap();
7734 let hb = resolve_create(b).unwrap();
7735 assert!(matches!(
7736 dispatch(&ha).await,
7737 PermissionResult::Decision {
7738 decision: PermissionDecision::ApproveOnce(_),
7739 ..
7740 }
7741 ));
7742 assert!(matches!(
7743 dispatch(&hb).await,
7744 PermissionResult::Decision {
7745 decision: PermissionDecision::ApproveOnce(_),
7746 ..
7747 }
7748 ));
7749 }
7750
7751 #[tokio::test]
7752 async fn deny_all_is_order_independent() {
7753 let a = SessionConfig::default()
7754 .with_permission_handler(Arc::new(ApproveAllHandler))
7755 .deny_all_permissions();
7756 let b = SessionConfig::default()
7757 .deny_all_permissions()
7758 .with_permission_handler(Arc::new(ApproveAllHandler));
7759 let ha = resolve_create(a).unwrap();
7760 let hb = resolve_create(b).unwrap();
7761 assert!(matches!(
7762 dispatch(&ha).await,
7763 PermissionResult::Decision {
7764 decision: PermissionDecision::Reject(_),
7765 ..
7766 }
7767 ));
7768 assert!(matches!(
7769 dispatch(&hb).await,
7770 PermissionResult::Decision {
7771 decision: PermissionDecision::Reject(_),
7772 ..
7773 }
7774 ));
7775 }
7776
7777 #[tokio::test]
7778 async fn approve_permissions_if_consults_predicate() {
7779 let cfg = SessionConfig::default().approve_permissions_if(|d| {
7780 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7781 });
7782 let h = resolve_create(cfg).unwrap();
7783 assert!(matches!(
7784 dispatch(&h).await,
7785 PermissionResult::Decision {
7786 decision: PermissionDecision::Reject(_),
7787 ..
7788 }
7789 ));
7790 }
7791
7792 #[tokio::test]
7793 async fn approve_permissions_if_is_order_independent() {
7794 let predicate = |d: &PermissionRequestData| {
7795 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7796 };
7797 let a = SessionConfig::default()
7798 .with_permission_handler(Arc::new(ApproveAllHandler))
7799 .approve_permissions_if(predicate);
7800 let b = SessionConfig::default()
7801 .approve_permissions_if(predicate)
7802 .with_permission_handler(Arc::new(ApproveAllHandler));
7803 let ha = resolve_create(a).unwrap();
7804 let hb = resolve_create(b).unwrap();
7805 assert!(matches!(
7806 dispatch(&ha).await,
7807 PermissionResult::Decision {
7808 decision: PermissionDecision::Reject(_),
7809 ..
7810 }
7811 ));
7812 assert!(matches!(
7813 dispatch(&hb).await,
7814 PermissionResult::Decision {
7815 decision: PermissionDecision::Reject(_),
7816 ..
7817 }
7818 ));
7819 }
7820
7821 #[tokio::test]
7822 async fn resume_session_config_approve_all_works() {
7823 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
7824 .with_permission_handler(Arc::new(ApproveAllHandler))
7825 .approve_all_permissions();
7826 let h = resolve_resume(cfg).unwrap();
7827 assert!(matches!(
7828 dispatch(&h).await,
7829 PermissionResult::Decision {
7830 decision: PermissionDecision::ApproveOnce(_),
7831 ..
7832 }
7833 ));
7834 }
7835
7836 #[tokio::test]
7837 async fn resume_session_config_approve_all_is_order_independent() {
7838 let a = ResumeSessionConfig::new(SessionId::from("s1"))
7839 .with_permission_handler(Arc::new(ApproveAllHandler))
7840 .approve_all_permissions();
7841 let b = ResumeSessionConfig::new(SessionId::from("s1"))
7842 .approve_all_permissions()
7843 .with_permission_handler(Arc::new(ApproveAllHandler));
7844 let ha = resolve_resume(a).unwrap();
7845 let hb = resolve_resume(b).unwrap();
7846 assert!(matches!(
7847 dispatch(&ha).await,
7848 PermissionResult::Decision {
7849 decision: PermissionDecision::ApproveOnce(_),
7850 ..
7851 }
7852 ));
7853 assert!(matches!(
7854 dispatch(&hb).await,
7855 PermissionResult::Decision {
7856 decision: PermissionDecision::ApproveOnce(_),
7857 ..
7858 }
7859 ));
7860 }
7861
7862 #[test]
7863 fn session_config_enable_experimental_mode_serializes_when_set() {
7864 let cfg = SessionConfig::default().with_enable_experimental_mode(false);
7865 assert_eq!(cfg.enable_experimental_mode, Some(false));
7866
7867 let (wire, _runtime) = cfg
7868 .into_wire(Some(SessionId::from("experimental-mode")))
7869 .expect("enable_experimental_mode config has no duplicate handlers");
7870 assert_eq!(wire.is_experimental_mode, Some(false));
7871
7872 let json = serde_json::to_value(&wire).unwrap();
7873 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7874 }
7875
7876 #[test]
7877 fn session_config_enable_experimental_mode_omitted_when_none() {
7878 let cfg = SessionConfig::default();
7879 assert_eq!(cfg.enable_experimental_mode, None);
7880
7881 let (wire, _runtime) = cfg
7882 .into_wire(Some(SessionId::from("no-experimental-mode")))
7883 .expect("default config has no duplicate handlers");
7884 assert_eq!(wire.is_experimental_mode, None);
7885
7886 let json = serde_json::to_value(&wire).unwrap();
7887 assert!(json.get("isExperimentalMode").is_none());
7888 }
7889
7890 #[test]
7891 fn resume_session_config_enable_experimental_mode_serializes_when_set() {
7892 let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
7893 .with_enable_experimental_mode(false);
7894 assert_eq!(cfg.enable_experimental_mode, Some(false));
7895
7896 let (wire, _runtime) = cfg
7897 .into_wire()
7898 .expect("resume enable_experimental_mode config has no duplicate handlers");
7899 assert_eq!(wire.is_experimental_mode, Some(false));
7900
7901 let json = serde_json::to_value(&wire).unwrap();
7902 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7903 }
7904
7905 #[test]
7906 fn resume_session_config_enable_experimental_mode_omitted_when_none() {
7907 let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
7908 assert_eq!(cfg.enable_experimental_mode, None);
7909
7910 let (wire, _runtime) = cfg
7911 .into_wire()
7912 .expect("default resume config has no duplicate handlers");
7913 assert_eq!(wire.is_experimental_mode, None);
7914
7915 let json = serde_json::to_value(&wire).unwrap();
7916 assert!(json.get("isExperimentalMode").is_none());
7917 }
7918}
7919
7920#[cfg(test)]
7921mod is_terminal_tests {
7922 use super::Tool;
7923
7924 #[test]
7925 fn is_terminal_serializes_as_camel_case_when_set() {
7926 let tool = Tool {
7927 name: "clear_context".to_owned(),
7928 is_terminal: true,
7929 ..Default::default()
7930 };
7931 let value = serde_json::to_value(&tool).expect("tool serializes");
7932 assert_eq!(
7933 value.get("isTerminal"),
7934 Some(&serde_json::Value::Bool(true))
7935 );
7936 }
7937
7938 #[test]
7939 fn is_terminal_is_omitted_when_false() {
7940 let tool = Tool {
7941 name: "plain".to_owned(),
7942 ..Default::default()
7943 };
7944 let value = serde_json::to_value(&tool).expect("tool serializes");
7945 assert!(value.get("isTerminal").is_none());
7946 }
7947
7948 #[test]
7951 fn is_terminal_appears_in_debug_output() {
7952 let terminal = Tool {
7953 name: "clear_context".to_owned(),
7954 is_terminal: true,
7955 ..Default::default()
7956 };
7957 assert!(format!("{terminal:?}").contains("is_terminal: true"));
7958
7959 let plain = Tool {
7960 name: "plain".to_owned(),
7961 ..Default::default()
7962 };
7963 assert!(format!("{plain:?}").contains("is_terminal: false"));
7964 }
7965}