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(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1856#[serde(rename_all = "lowercase")]
1857#[non_exhaustive]
1858pub enum AskUserVariant {
1859 #[default]
1861 Legacy,
1862 Elicitation,
1864}
1865
1866#[derive(Clone)]
1918#[non_exhaustive]
1919pub struct SessionConfig {
1920 pub session_id: Option<SessionId>,
1922 pub model: Option<String>,
1924 pub client_name: Option<String>,
1926 pub reasoning_effort: Option<String>,
1928 pub reasoning_summary: Option<ReasoningSummary>,
1932 pub context_tier: Option<String>,
1935 pub streaming: Option<bool>,
1937 pub system_message: Option<SystemMessageConfig>,
1939 pub ask_user_variant: Option<AskUserVariant>,
1944 pub tools: Option<Vec<Tool>>,
1946 pub canvases: Option<Vec<CanvasDeclaration>>,
1948 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1953 pub request_canvas_renderer: Option<bool>,
1955 pub request_extensions: Option<bool>,
1957 pub extension_sdk_path: Option<String>,
1961 pub extension_info: Option<ExtensionInfo>,
1963 pub canvas_provider: Option<CanvasProviderIdentity>,
1966 pub available_tools: Option<Vec<String>>,
1968 pub excluded_tools: Option<Vec<String>>,
1970 pub excluded_builtin_agents: Option<Vec<String>>,
1976 pub included_builtin_skills: Option<Vec<String>>,
1980 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
1982 pub mcp_oauth_token_storage: Option<String>,
1991 pub enable_config_discovery: Option<bool>,
1994 pub skip_embedding_retrieval: Option<bool>,
1996 pub embedding_cache_storage: Option<String>,
1999 pub organization_custom_instructions: Option<String>,
2001 pub enable_on_demand_instruction_discovery: Option<bool>,
2003 pub enable_file_hooks: Option<bool>,
2005 pub enable_host_git_operations: Option<bool>,
2007 pub enable_session_store: Option<bool>,
2009 pub enable_skills: Option<bool>,
2011 pub enable_mcp_apps: Option<bool>,
2038 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
2043 pub skill_directories: Option<Vec<PathBuf>>,
2045 pub instruction_directories: Option<Vec<PathBuf>>,
2048 pub plugin_directories: Option<Vec<PathBuf>>,
2050 pub large_output: Option<LargeToolOutputConfig>,
2052 pub tool_search: Option<ToolSearchConfig>,
2056 pub disabled_skills: Option<Vec<String>>,
2059 pub disabled_mcp_servers: Option<Vec<String>>,
2063 pub hooks: Option<bool>,
2067 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2069 pub default_agent: Option<DefaultAgentConfig>,
2073 pub agent: Option<String>,
2076 pub infinite_sessions: Option<InfiniteSessionConfig>,
2079 pub provider: Option<ProviderConfig>,
2083 pub capi: Option<CapiSessionOptions>,
2089 pub providers: Option<Vec<NamedProviderConfig>>,
2096 pub models: Option<Vec<ProviderModelConfig>>,
2102 pub enable_session_telemetry: Option<bool>,
2110 pub enable_citations: Option<bool>,
2112 pub enable_file_change_tracking: Option<bool>,
2115 pub session_limits: Option<SessionLimitsConfig>,
2117 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2120 pub memory: Option<MemoryConfiguration>,
2122 pub config_directory: Option<PathBuf>,
2125 pub working_directory: Option<PathBuf>,
2128 pub additional_directories: Option<Vec<PathBuf>>,
2132 pub github_token: Option<String>,
2138 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2144 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2150 pub cloud: Option<CloudSessionOptions>,
2153 pub include_sub_agent_streaming_events: Option<bool>,
2157 pub commands: Option<Vec<CommandDefinition>>,
2161 #[doc(hidden)]
2168 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
2169 pub enable_managed_settings: Option<bool>,
2177 pub managed_settings: Option<ManagedSettings>,
2186 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2191 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2195 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2198 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2201 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2205 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2208 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2211 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2215 pub(crate) permission_policy: Option<crate::permission::Policy>,
2219 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2224 pub skip_custom_instructions: Option<bool>,
2228 pub custom_agents_local_only: Option<bool>,
2232 pub enable_experimental_mode: Option<bool>,
2237 pub coauthor_enabled: Option<bool>,
2241 pub manage_schedule_enabled: Option<bool>,
2245}
2246
2247impl std::fmt::Debug for SessionConfig {
2248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2249 f.debug_struct("SessionConfig")
2250 .field("session_id", &self.session_id)
2251 .field("model", &self.model)
2252 .field("client_name", &self.client_name)
2253 .field("reasoning_effort", &self.reasoning_effort)
2254 .field("reasoning_summary", &self.reasoning_summary)
2255 .field("context_tier", &self.context_tier)
2256 .field("streaming", &self.streaming)
2257 .field("system_message", &self.system_message)
2258 .field("ask_user_variant", &self.ask_user_variant)
2259 .field("tools", &self.tools)
2260 .field("canvases", &self.canvases)
2261 .field(
2262 "canvas_handler",
2263 &self.canvas_handler.as_ref().map(|_| "<set>"),
2264 )
2265 .field("request_canvas_renderer", &self.request_canvas_renderer)
2266 .field("request_extensions", &self.request_extensions)
2267 .field("extension_sdk_path", &self.extension_sdk_path)
2268 .field("extension_info", &self.extension_info)
2269 .field("canvas_provider", &self.canvas_provider)
2270 .field("available_tools", &self.available_tools)
2271 .field("excluded_tools", &self.excluded_tools)
2272 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2273 .field("included_builtin_skills", &self.included_builtin_skills)
2274 .field("mcp_servers", &self.mcp_servers)
2275 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2276 .field("embedding_cache_storage", &self.embedding_cache_storage)
2277 .field("enable_config_discovery", &self.enable_config_discovery)
2278 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2279 .field(
2280 "organization_custom_instructions",
2281 &self
2282 .organization_custom_instructions
2283 .as_ref()
2284 .map(|_| "<redacted>"),
2285 )
2286 .field(
2287 "enable_on_demand_instruction_discovery",
2288 &self.enable_on_demand_instruction_discovery,
2289 )
2290 .field("enable_file_hooks", &self.enable_file_hooks)
2291 .field(
2292 "enable_host_git_operations",
2293 &self.enable_host_git_operations,
2294 )
2295 .field("enable_session_store", &self.enable_session_store)
2296 .field("enable_skills", &self.enable_skills)
2297 .field("enable_mcp_apps", &self.enable_mcp_apps)
2298 .field("skill_directories", &self.skill_directories)
2299 .field("instruction_directories", &self.instruction_directories)
2300 .field("plugin_directories", &self.plugin_directories)
2301 .field("large_output", &self.large_output)
2302 .field("tool_search", &self.tool_search)
2303 .field("disabled_skills", &self.disabled_skills)
2304 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
2305 .field("hooks", &self.hooks)
2306 .field("custom_agents", &self.custom_agents)
2307 .field("default_agent", &self.default_agent)
2308 .field("agent", &self.agent)
2309 .field("infinite_sessions", &self.infinite_sessions)
2310 .field("provider", &self.provider)
2311 .field("capi", &self.capi)
2312 .field("enable_session_telemetry", &self.enable_session_telemetry)
2313 .field("enable_citations", &self.enable_citations)
2314 .field(
2315 "enable_file_change_tracking",
2316 &self.enable_file_change_tracking,
2317 )
2318 .field("session_limits", &self.session_limits)
2319 .field("model_capabilities", &self.model_capabilities)
2320 .field("memory", &self.memory)
2321 .field("config_directory", &self.config_directory)
2322 .field("working_directory", &self.working_directory)
2323 .field("additional_directories", &self.additional_directories)
2324 .field(
2325 "github_token",
2326 &self.github_token.as_ref().map(|_| "<redacted>"),
2327 )
2328 .field(
2329 "github_token_provider",
2330 &self.github_token_provider.as_ref().map(|_| "<set>"),
2331 )
2332 .field("remote_session", &self.remote_session)
2333 .field("cloud", &self.cloud)
2334 .field(
2335 "include_sub_agent_streaming_events",
2336 &self.include_sub_agent_streaming_events,
2337 )
2338 .field("commands", &self.commands)
2339 .field("exp_assignments", &self.exp_assignments)
2340 .field("enable_managed_settings", &self.enable_managed_settings)
2341 .field("enable_experimental_mode", &self.enable_experimental_mode)
2342 .field("managed_settings", &self.managed_settings)
2343 .field(
2344 "session_fs_provider",
2345 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2346 )
2347 .field(
2348 "permission_handler",
2349 &self.permission_handler.as_ref().map(|_| "<set>"),
2350 )
2351 .field(
2352 "elicitation_handler",
2353 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2354 )
2355 .field(
2356 "mcp_auth_handler",
2357 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2358 )
2359 .field(
2360 "user_input_handler",
2361 &self.user_input_handler.as_ref().map(|_| "<set>"),
2362 )
2363 .field(
2364 "exit_plan_mode_handler",
2365 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2366 )
2367 .field(
2368 "auto_mode_switch_handler",
2369 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2370 )
2371 .field(
2372 "hooks_handler",
2373 &self.hooks_handler.as_ref().map(|_| "<set>"),
2374 )
2375 .field(
2376 "system_message_transform",
2377 &self.system_message_transform.as_ref().map(|_| "<set>"),
2378 )
2379 .finish()
2380 }
2381}
2382
2383impl Default for SessionConfig {
2384 fn default() -> Self {
2390 Self {
2391 session_id: None,
2392 model: None,
2393 client_name: None,
2394 reasoning_effort: None,
2395 reasoning_summary: None,
2396 context_tier: None,
2397 streaming: None,
2398 system_message: None,
2399 ask_user_variant: None,
2400 tools: None,
2401 canvases: None,
2402 canvas_handler: None,
2403 request_canvas_renderer: None,
2404 request_extensions: None,
2405 extension_sdk_path: None,
2406 extension_info: None,
2407 canvas_provider: None,
2408 available_tools: None,
2409 excluded_tools: None,
2410 excluded_builtin_agents: None,
2411 included_builtin_skills: None,
2412 mcp_servers: None,
2413 mcp_oauth_token_storage: None,
2414 enable_config_discovery: None,
2415 skip_embedding_retrieval: None,
2416 organization_custom_instructions: None,
2417 enable_on_demand_instruction_discovery: None,
2418 enable_file_hooks: None,
2419 enable_host_git_operations: None,
2420 enable_session_store: None,
2421 enable_skills: None,
2422 embedding_cache_storage: None,
2423 enable_mcp_apps: None,
2424 github_mcp_tool_config: None,
2425 skill_directories: None,
2426 instruction_directories: None,
2427 plugin_directories: None,
2428 large_output: None,
2429 tool_search: None,
2430 disabled_skills: None,
2431 disabled_mcp_servers: None,
2432 hooks: None,
2433 custom_agents: None,
2434 default_agent: None,
2435 agent: None,
2436 infinite_sessions: None,
2437 provider: None,
2438 capi: None,
2439 providers: None,
2440 models: None,
2441 enable_session_telemetry: None,
2442 enable_citations: None,
2443 enable_file_change_tracking: None,
2444 session_limits: None,
2445 model_capabilities: None,
2446 memory: None,
2447 config_directory: None,
2448 working_directory: None,
2449 additional_directories: None,
2450 github_token: None,
2451 github_token_provider: None,
2452 remote_session: None,
2453 cloud: None,
2454 include_sub_agent_streaming_events: None,
2455 commands: None,
2456 exp_assignments: None,
2457 enable_managed_settings: None,
2458 managed_settings: None,
2459 session_fs_provider: None,
2460 permission_handler: None,
2461 elicitation_handler: None,
2462 mcp_auth_handler: None,
2463 user_input_handler: None,
2464 exit_plan_mode_handler: None,
2465 auto_mode_switch_handler: None,
2466 hooks_handler: None,
2467 permission_policy: None,
2468 system_message_transform: None,
2469 skip_custom_instructions: None,
2470 custom_agents_local_only: None,
2471 enable_experimental_mode: None,
2472 coauthor_enabled: None,
2473 manage_schedule_enabled: None,
2474 }
2475 }
2476}
2477
2478pub(crate) struct SessionConfigRuntime {
2484 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2485 pub permission_policy: Option<crate::permission::Policy>,
2486 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2487 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2488 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2489 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2490 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2491 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2492 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2493 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2494 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2495 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2496 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2497 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2498 pub commands: Option<Vec<CommandDefinition>>,
2499}
2500
2501impl SessionConfig {
2502 pub(crate) fn into_wire(
2514 mut self,
2515 session_id: Option<SessionId>,
2516 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2517 if self.github_token.is_some() && self.github_token_provider.is_some() {
2518 return Err(crate::Error::with_message(
2519 crate::ErrorKind::InvalidConfig,
2520 "github_token and github_token_provider are mutually exclusive",
2521 ));
2522 }
2523 let permission_active =
2524 self.permission_handler.is_some() || self.permission_policy.is_some();
2525 let request_user_input = self.user_input_handler.is_some();
2526 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2527 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2528 let request_elicitation = self.elicitation_handler.is_some();
2529 let hooks_flag = self.hooks_handler.is_some();
2530
2531 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2532 if let Some(tools) = self.tools.as_mut() {
2533 for tool in tools.iter_mut() {
2534 if let Some(handler) = tool.handler.take()
2535 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2536 {
2537 return Err(crate::Error::with_message(
2538 crate::ErrorKind::InvalidConfig,
2539 format!("duplicate tool handler registered for name {:?}", tool.name),
2540 ));
2541 }
2542 }
2543 }
2544
2545 let wire_commands = self.commands.as_ref().map(|cmds| {
2546 cmds.iter()
2547 .map(|c| crate::wire::CommandWireDefinition {
2548 name: c.name.clone(),
2549 description: c.description.clone().unwrap_or_default(),
2550 })
2551 .collect()
2552 });
2553 let wire_canvases = self.canvases.clone();
2554 let canvas_handler = self.canvas_handler.clone();
2555 let bearer_token_providers =
2556 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2557
2558 let wire = crate::wire::SessionCreateWire {
2559 session_id,
2560 model: self.model,
2561 client_name: self.client_name,
2562 reasoning_effort: self.reasoning_effort,
2563 reasoning_summary: self.reasoning_summary,
2564 context_tier: self.context_tier,
2565 streaming: self.streaming,
2566 system_message: self.system_message,
2567 ask_user_variant: self.ask_user_variant,
2568 tools: self.tools,
2569 canvases: wire_canvases,
2570 request_canvas_renderer: self.request_canvas_renderer,
2571 request_extensions: self.request_extensions,
2572 extension_sdk_path: self.extension_sdk_path,
2573 extension_info: self.extension_info,
2574 canvas_provider: self.canvas_provider,
2575 available_tools: self.available_tools,
2576 excluded_tools: self.excluded_tools,
2577 excluded_builtin_agents: self.excluded_builtin_agents,
2578 tool_filter_precedence: "excluded",
2579 mcp_servers: self.mcp_servers,
2580 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2581 embedding_cache_storage: self.embedding_cache_storage,
2582 env_value_mode: "direct",
2583 enable_config_discovery: self.enable_config_discovery,
2584 skip_embedding_retrieval: self.skip_embedding_retrieval,
2585 organization_custom_instructions: self.organization_custom_instructions,
2586 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2587 enable_file_hooks: self.enable_file_hooks,
2588 enable_host_git_operations: self.enable_host_git_operations,
2589 enable_session_store: self.enable_session_store,
2590 enable_skills: self.enable_skills,
2591 request_user_input,
2592 request_permission: permission_active,
2593 request_exit_plan_mode,
2594 request_auto_mode_switch,
2595 request_elicitation,
2596 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2597 github_mcp_tool_config: self.github_mcp_tool_config,
2598 hooks: hooks_flag,
2599 skill_directories: self.skill_directories,
2600 instruction_directories: self.instruction_directories,
2601 plugin_directories: self.plugin_directories,
2602 large_output: self.large_output,
2603 tool_search: self.tool_search,
2604 disabled_skills: self.disabled_skills,
2605 disabled_mcp_servers: self.disabled_mcp_servers,
2606 custom_agents: self.custom_agents,
2607 custom_agents_local_only: self.custom_agents_local_only,
2608 default_agent: self.default_agent,
2609 agent: self.agent,
2610 infinite_sessions: self.infinite_sessions,
2611 provider: self.provider,
2612 capi: self.capi,
2613 providers: self.providers,
2614 models: self.models,
2615 enable_session_telemetry: self.enable_session_telemetry,
2616 enable_citations: self.enable_citations,
2617 enable_file_change_tracking: self.enable_file_change_tracking,
2618 session_limits: self.session_limits,
2619 model_capabilities: self.model_capabilities,
2620 memory: self.memory,
2621 config_dir: self.config_directory,
2622 working_directory: self.working_directory,
2623 additional_directories: self.additional_directories,
2624 github_token: self.github_token,
2625 github_token_provider_registration_id: None,
2626 remote_session: self.remote_session,
2627 cloud: self.cloud,
2628 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2629 enable_github_telemetry_forwarding: None,
2630 commands: wire_commands,
2631 exp_assignments: self.exp_assignments,
2632 enable_managed_settings: self.enable_managed_settings,
2633 is_experimental_mode: self.enable_experimental_mode,
2634 managed_settings: self.managed_settings,
2635 };
2636
2637 let runtime = SessionConfigRuntime {
2638 permission_handler: self.permission_handler,
2639 permission_policy: self.permission_policy,
2640 elicitation_handler: self.elicitation_handler,
2641 mcp_auth_handler: self.mcp_auth_handler,
2642 user_input_handler: self.user_input_handler,
2643 exit_plan_mode_handler: self.exit_plan_mode_handler,
2644 auto_mode_switch_handler: self.auto_mode_switch_handler,
2645 hooks_handler: self.hooks_handler,
2646 system_message_transform: self.system_message_transform,
2647 tool_handlers,
2648 canvas_handler,
2649 session_fs_provider: self.session_fs_provider,
2650 bearer_token_providers,
2651 github_token_provider: self.github_token_provider,
2652 commands: self.commands,
2653 };
2654
2655 Ok((wire, runtime))
2656 }
2657
2658 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2662 self.permission_handler = Some(handler);
2663 self
2664 }
2665
2666 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2669 self.elicitation_handler = Some(handler);
2670 self
2671 }
2672
2673 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2675 self.mcp_auth_handler = Some(handler);
2676 self
2677 }
2678
2679 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2682 self.user_input_handler = Some(handler);
2683 self
2684 }
2685
2686 pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self {
2688 self.ask_user_variant = Some(variant);
2689 self
2690 }
2691
2692 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2694 self.exit_plan_mode_handler = Some(handler);
2695 self
2696 }
2697
2698 pub fn with_auto_mode_switch_handler(
2700 mut self,
2701 handler: Arc<dyn AutoModeSwitchHandler>,
2702 ) -> Self {
2703 self.auto_mode_switch_handler = Some(handler);
2704 self
2705 }
2706
2707 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2712 self.commands = Some(commands);
2713 self
2714 }
2715
2716 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2720 self.session_fs_provider = Some(provider);
2721 self
2722 }
2723
2724 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2727 self.hooks_handler = Some(hooks);
2728 self
2729 }
2730
2731 pub fn with_system_message_transform(
2735 mut self,
2736 transform: Arc<dyn SystemMessageTransform>,
2737 ) -> Self {
2738 self.system_message_transform = Some(transform);
2739 self
2740 }
2741
2742 pub fn approve_all_permissions(mut self) -> Self {
2748 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2749 self
2750 }
2751
2752 pub fn deny_all_permissions(mut self) -> Self {
2755 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2756 self
2757 }
2758
2759 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2764 where
2765 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2766 {
2767 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2768 self
2769 }
2770
2771 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2773 self.session_id = Some(id.into());
2774 self
2775 }
2776
2777 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2779 self.model = Some(model.into());
2780 self
2781 }
2782
2783 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2785 self.client_name = Some(name.into());
2786 self
2787 }
2788
2789 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2791 self.reasoning_effort = Some(effort.into());
2792 self
2793 }
2794
2795 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2797 self.reasoning_summary = Some(summary);
2798 self
2799 }
2800
2801 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2803 self.context_tier = Some(tier.into());
2804 self
2805 }
2806
2807 pub fn with_streaming(mut self, streaming: bool) -> Self {
2809 self.streaming = Some(streaming);
2810 self
2811 }
2812
2813 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2815 self.system_message = Some(system_message);
2816 self
2817 }
2818
2819 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2821 self.tools = Some(tools.into_iter().collect());
2822 self
2823 }
2824
2825 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2830 self.canvases = Some(canvases.into_iter().collect());
2831 self
2832 }
2833
2834 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2836 self.canvas_handler = Some(handler);
2837 self
2838 }
2839
2840 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2842 self.request_canvas_renderer = Some(request);
2843 self
2844 }
2845
2846 pub fn with_request_extensions(mut self, request: bool) -> Self {
2848 self.request_extensions = Some(request);
2849 self
2850 }
2851
2852 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2856 self.extension_sdk_path = Some(path.into());
2857 self
2858 }
2859
2860 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2862 self.extension_info = Some(extension_info);
2863 self
2864 }
2865
2866 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2869 self.canvas_provider = Some(canvas_provider);
2870 self
2871 }
2872
2873 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2875 where
2876 I: IntoIterator<Item = S>,
2877 S: Into<String>,
2878 {
2879 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2880 self
2881 }
2882
2883 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2885 where
2886 I: IntoIterator<Item = S>,
2887 S: Into<String>,
2888 {
2889 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2890 self
2891 }
2892
2893 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2895 where
2896 I: IntoIterator<Item = S>,
2897 S: Into<String>,
2898 {
2899 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2900 self
2901 }
2902
2903 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2905 self.mcp_servers = Some(servers);
2906 self
2907 }
2908
2909 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2917 self.mcp_oauth_token_storage = Some(mode.into());
2918 self
2919 }
2920
2921 pub fn with_embedding_cache_storage(
2923 mut self,
2924 embedding_cache_storage: impl Into<String>,
2925 ) -> Self {
2926 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2927 self
2928 }
2929
2930 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2933 self.enable_config_discovery = Some(enable);
2934 self
2935 }
2936
2937 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2939 self.skip_embedding_retrieval = Some(value);
2940 self
2941 }
2942
2943 pub fn with_organization_custom_instructions(
2945 mut self,
2946 instructions: impl Into<String>,
2947 ) -> Self {
2948 self.organization_custom_instructions = Some(instructions.into());
2949 self
2950 }
2951
2952 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2954 self.enable_on_demand_instruction_discovery = Some(value);
2955 self
2956 }
2957
2958 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2960 self.enable_file_hooks = Some(value);
2961 self
2962 }
2963
2964 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2966 self.enable_host_git_operations = Some(value);
2967 self
2968 }
2969
2970 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2972 self.enable_session_store = Some(value);
2973 self
2974 }
2975
2976 pub fn with_enable_skills(mut self, value: bool) -> Self {
2978 self.enable_skills = Some(value);
2979 self
2980 }
2981
2982 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2988 self.enable_mcp_apps = Some(enable);
2989 self
2990 }
2991
2992 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
2994 self.github_mcp_tool_config = Some(config);
2995 self
2996 }
2997
2998 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3000 where
3001 I: IntoIterator<Item = P>,
3002 P: Into<PathBuf>,
3003 {
3004 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3005 self
3006 }
3007
3008 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
3010 where
3011 I: IntoIterator<Item = S>,
3012 S: Into<String>,
3013 {
3014 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
3015 self
3016 }
3017
3018 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3022 where
3023 I: IntoIterator<Item = P>,
3024 P: Into<PathBuf>,
3025 {
3026 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3027 self
3028 }
3029
3030 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3032 where
3033 I: IntoIterator<Item = P>,
3034 P: Into<PathBuf>,
3035 {
3036 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3037 self
3038 }
3039
3040 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3042 self.large_output = Some(config);
3043 self
3044 }
3045
3046 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
3049 self.tool_search = Some(config);
3050 self
3051 }
3052
3053 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3055 where
3056 I: IntoIterator<Item = S>,
3057 S: Into<String>,
3058 {
3059 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3060 self
3061 }
3062
3063 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
3065 where
3066 I: IntoIterator<Item = S>,
3067 S: Into<String>,
3068 {
3069 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
3070 self
3071 }
3072
3073 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3075 mut self,
3076 agents: I,
3077 ) -> Self {
3078 self.custom_agents = Some(agents.into_iter().collect());
3079 self
3080 }
3081
3082 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3084 self.default_agent = Some(agent);
3085 self
3086 }
3087
3088 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3091 self.agent = Some(name.into());
3092 self
3093 }
3094
3095 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3098 self.infinite_sessions = Some(config);
3099 self
3100 }
3101
3102 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3104 self.provider = Some(provider);
3105 self
3106 }
3107
3108 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3110 self.capi = Some(capi);
3111 self
3112 }
3113
3114 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3120 self.providers = Some(providers);
3121 self
3122 }
3123
3124 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3130 self.models = Some(models);
3131 self
3132 }
3133
3134 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3138 self.enable_session_telemetry = Some(enable);
3139 self
3140 }
3141
3142 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3144 self.enable_citations = Some(enable);
3145 self
3146 }
3147
3148 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
3151 self.enable_file_change_tracking = Some(enable);
3152 self
3153 }
3154
3155 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3157 self.session_limits = Some(limits);
3158 self
3159 }
3160
3161 pub fn with_model_capabilities(
3163 mut self,
3164 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3165 ) -> Self {
3166 self.model_capabilities = Some(capabilities);
3167 self
3168 }
3169
3170 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3172 self.memory = Some(memory);
3173 self
3174 }
3175
3176 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3178 self.config_directory = Some(dir.into());
3179 self
3180 }
3181
3182 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3185 self.working_directory = Some(dir.into());
3186 self
3187 }
3188
3189 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
3191 where
3192 I: IntoIterator<Item = P>,
3193 P: Into<PathBuf>,
3194 {
3195 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
3196 self
3197 }
3198
3199 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3204 self.github_token = Some(token.into());
3205 self
3206 }
3207
3208 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
3214 self.github_token_provider = Some(provider);
3215 self
3216 }
3217
3218 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3221 self.include_sub_agent_streaming_events = Some(include);
3222 self
3223 }
3224
3225 pub fn with_remote_session(
3227 mut self,
3228 mode: crate::generated::api_types::RemoteSessionMode,
3229 ) -> Self {
3230 self.remote_session = Some(mode);
3231 self
3232 }
3233
3234 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
3236 self.cloud = Some(cloud);
3237 self
3238 }
3239
3240 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3242 self.skip_custom_instructions = Some(value);
3243 self
3244 }
3245
3246 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3248 self.custom_agents_local_only = Some(value);
3249 self
3250 }
3251
3252 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
3254 self.enable_experimental_mode = Some(enable_experimental_mode);
3255 self
3256 }
3257
3258 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3260 self.coauthor_enabled = Some(value);
3261 self
3262 }
3263
3264 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3266 self.manage_schedule_enabled = Some(value);
3267 self
3268 }
3269
3270 #[doc(hidden)]
3278 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
3279 self.exp_assignments = Some(assignments);
3280 self
3281 }
3282
3283 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3290 self.enable_managed_settings = Some(enabled);
3291 self
3292 }
3293
3294 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
3299 self.managed_settings = Some(managed_settings);
3300 self
3301 }
3302}
3303#[derive(Clone)]
3310#[non_exhaustive]
3311pub struct ResumeSessionConfig {
3312 pub session_id: SessionId,
3314 pub model: Option<String>,
3317 pub client_name: Option<String>,
3319 pub reasoning_effort: Option<String>,
3321 pub reasoning_summary: Option<ReasoningSummary>,
3325 pub context_tier: Option<String>,
3328 pub streaming: Option<bool>,
3330 pub system_message: Option<SystemMessageConfig>,
3333 pub ask_user_variant: Option<AskUserVariant>,
3338 pub tools: Option<Vec<Tool>>,
3340 pub canvases: Option<Vec<CanvasDeclaration>>,
3342 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
3345 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3347 pub request_canvas_renderer: Option<bool>,
3349 pub request_extensions: Option<bool>,
3351 pub extension_sdk_path: Option<String>,
3355 pub extension_info: Option<ExtensionInfo>,
3357 pub canvas_provider: Option<CanvasProviderIdentity>,
3360 pub available_tools: Option<Vec<String>>,
3362 pub excluded_tools: Option<Vec<String>>,
3364 pub excluded_builtin_agents: Option<Vec<String>>,
3370 pub included_builtin_skills: Option<Vec<String>>,
3374 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3376 pub mcp_oauth_token_storage: Option<String>,
3379 pub enable_config_discovery: Option<bool>,
3382 pub skip_embedding_retrieval: Option<bool>,
3384 pub embedding_cache_storage: Option<String>,
3386 pub organization_custom_instructions: Option<String>,
3388 pub enable_on_demand_instruction_discovery: Option<bool>,
3390 pub enable_file_hooks: Option<bool>,
3392 pub enable_host_git_operations: Option<bool>,
3394 pub enable_session_store: Option<bool>,
3396 pub enable_skills: Option<bool>,
3398 pub enable_mcp_apps: Option<bool>,
3404 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
3409 pub skill_directories: Option<Vec<PathBuf>>,
3411 pub instruction_directories: Option<Vec<PathBuf>>,
3414 pub plugin_directories: Option<Vec<PathBuf>>,
3416 pub large_output: Option<LargeToolOutputConfig>,
3418 pub tool_search: Option<ToolSearchConfig>,
3421 pub disabled_skills: Option<Vec<String>>,
3423 pub disabled_mcp_servers: Option<Vec<String>>,
3426 pub hooks: Option<bool>,
3428 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3430 pub default_agent: Option<DefaultAgentConfig>,
3432 pub agent: Option<String>,
3434 pub infinite_sessions: Option<InfiniteSessionConfig>,
3436 pub provider: Option<ProviderConfig>,
3438 pub capi: Option<CapiSessionOptions>,
3444 pub providers: Option<Vec<NamedProviderConfig>>,
3450 pub models: Option<Vec<ProviderModelConfig>>,
3456 pub enable_session_telemetry: Option<bool>,
3464 pub enable_citations: Option<bool>,
3466 pub enable_file_change_tracking: Option<bool>,
3470 pub session_limits: Option<SessionLimitsConfig>,
3472 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3474 pub memory: Option<MemoryConfiguration>,
3476 pub config_directory: Option<PathBuf>,
3478 pub working_directory: Option<PathBuf>,
3480 pub additional_directories: Option<Vec<PathBuf>>,
3483 pub github_token: Option<String>,
3486 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
3489 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3492 pub include_sub_agent_streaming_events: Option<bool>,
3494 pub commands: Option<Vec<CommandDefinition>>,
3498 #[doc(hidden)]
3503 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3504 pub enable_managed_settings: Option<bool>,
3510 pub managed_settings: Option<ManagedSettings>,
3516 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3521 pub suppress_resume_event: Option<bool>,
3524 pub continue_pending_work: Option<bool>,
3532 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3535 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3538 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3540 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3543 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3546 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3549 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3551 pub(crate) permission_policy: Option<crate::permission::Policy>,
3553 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3555 pub skip_custom_instructions: Option<bool>,
3557 pub custom_agents_local_only: Option<bool>,
3559 pub enable_experimental_mode: Option<bool>,
3564 pub coauthor_enabled: Option<bool>,
3566 pub manage_schedule_enabled: Option<bool>,
3568}
3569
3570impl std::fmt::Debug for ResumeSessionConfig {
3571 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3572 f.debug_struct("ResumeSessionConfig")
3573 .field("session_id", &self.session_id)
3574 .field("model", &self.model)
3575 .field("client_name", &self.client_name)
3576 .field("reasoning_effort", &self.reasoning_effort)
3577 .field("reasoning_summary", &self.reasoning_summary)
3578 .field("context_tier", &self.context_tier)
3579 .field("streaming", &self.streaming)
3580 .field("system_message", &self.system_message)
3581 .field("ask_user_variant", &self.ask_user_variant)
3582 .field("tools", &self.tools)
3583 .field("canvases", &self.canvases)
3584 .field(
3585 "canvas_handler",
3586 &self.canvas_handler.as_ref().map(|_| "<set>"),
3587 )
3588 .field("open_canvases", &self.open_canvases)
3589 .field("request_canvas_renderer", &self.request_canvas_renderer)
3590 .field("request_extensions", &self.request_extensions)
3591 .field("extension_sdk_path", &self.extension_sdk_path)
3592 .field("extension_info", &self.extension_info)
3593 .field("canvas_provider", &self.canvas_provider)
3594 .field("available_tools", &self.available_tools)
3595 .field("excluded_tools", &self.excluded_tools)
3596 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3597 .field("included_builtin_skills", &self.included_builtin_skills)
3598 .field("mcp_servers", &self.mcp_servers)
3599 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3600 .field("embedding_cache_storage", &self.embedding_cache_storage)
3601 .field("enable_config_discovery", &self.enable_config_discovery)
3602 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3603 .field(
3604 "organization_custom_instructions",
3605 &self
3606 .organization_custom_instructions
3607 .as_ref()
3608 .map(|_| "<redacted>"),
3609 )
3610 .field(
3611 "enable_on_demand_instruction_discovery",
3612 &self.enable_on_demand_instruction_discovery,
3613 )
3614 .field("enable_file_hooks", &self.enable_file_hooks)
3615 .field(
3616 "enable_host_git_operations",
3617 &self.enable_host_git_operations,
3618 )
3619 .field("enable_session_store", &self.enable_session_store)
3620 .field("enable_skills", &self.enable_skills)
3621 .field("enable_mcp_apps", &self.enable_mcp_apps)
3622 .field("skill_directories", &self.skill_directories)
3623 .field("instruction_directories", &self.instruction_directories)
3624 .field("plugin_directories", &self.plugin_directories)
3625 .field("large_output", &self.large_output)
3626 .field("tool_search", &self.tool_search)
3627 .field("disabled_skills", &self.disabled_skills)
3628 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
3629 .field("hooks", &self.hooks)
3630 .field("custom_agents", &self.custom_agents)
3631 .field("default_agent", &self.default_agent)
3632 .field("agent", &self.agent)
3633 .field("infinite_sessions", &self.infinite_sessions)
3634 .field("provider", &self.provider)
3635 .field("capi", &self.capi)
3636 .field("enable_session_telemetry", &self.enable_session_telemetry)
3637 .field("enable_citations", &self.enable_citations)
3638 .field(
3639 "enable_file_change_tracking",
3640 &self.enable_file_change_tracking,
3641 )
3642 .field("session_limits", &self.session_limits)
3643 .field("model_capabilities", &self.model_capabilities)
3644 .field("memory", &self.memory)
3645 .field("config_directory", &self.config_directory)
3646 .field("working_directory", &self.working_directory)
3647 .field("additional_directories", &self.additional_directories)
3648 .field(
3649 "github_token",
3650 &self.github_token.as_ref().map(|_| "<redacted>"),
3651 )
3652 .field(
3653 "github_token_provider",
3654 &self.github_token_provider.as_ref().map(|_| "<set>"),
3655 )
3656 .field("remote_session", &self.remote_session)
3657 .field(
3658 "include_sub_agent_streaming_events",
3659 &self.include_sub_agent_streaming_events,
3660 )
3661 .field("commands", &self.commands)
3662 .field("exp_assignments", &self.exp_assignments)
3663 .field("enable_managed_settings", &self.enable_managed_settings)
3664 .field("enable_experimental_mode", &self.enable_experimental_mode)
3665 .field("managed_settings", &self.managed_settings)
3666 .field(
3667 "session_fs_provider",
3668 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3669 )
3670 .field(
3671 "permission_handler",
3672 &self.permission_handler.as_ref().map(|_| "<set>"),
3673 )
3674 .field(
3675 "elicitation_handler",
3676 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3677 )
3678 .field(
3679 "user_input_handler",
3680 &self.user_input_handler.as_ref().map(|_| "<set>"),
3681 )
3682 .field(
3683 "exit_plan_mode_handler",
3684 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3685 )
3686 .field(
3687 "auto_mode_switch_handler",
3688 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3689 )
3690 .field(
3691 "hooks_handler",
3692 &self.hooks_handler.as_ref().map(|_| "<set>"),
3693 )
3694 .field(
3695 "system_message_transform",
3696 &self.system_message_transform.as_ref().map(|_| "<set>"),
3697 )
3698 .field("suppress_resume_event", &self.suppress_resume_event)
3699 .field("continue_pending_work", &self.continue_pending_work)
3700 .finish()
3701 }
3702}
3703
3704impl ResumeSessionConfig {
3705 pub(crate) fn into_wire(
3713 mut self,
3714 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3715 if self.github_token.is_some() && self.github_token_provider.is_some() {
3716 return Err(crate::Error::with_message(
3717 crate::ErrorKind::InvalidConfig,
3718 "github_token and github_token_provider are mutually exclusive",
3719 ));
3720 }
3721 let permission_active =
3722 self.permission_handler.is_some() || self.permission_policy.is_some();
3723 let request_user_input = self.user_input_handler.is_some();
3724 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3725 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3726 let request_elicitation = self.elicitation_handler.is_some();
3727 let hooks_flag = self.hooks_handler.is_some();
3728
3729 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3730 if let Some(tools) = self.tools.as_mut() {
3731 for tool in tools.iter_mut() {
3732 if let Some(handler) = tool.handler.take()
3733 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3734 {
3735 return Err(crate::Error::with_message(
3736 crate::ErrorKind::InvalidConfig,
3737 format!("duplicate tool handler registered for name {:?}", tool.name),
3738 ));
3739 }
3740 }
3741 }
3742
3743 let wire_commands = self.commands.as_ref().map(|cmds| {
3744 cmds.iter()
3745 .map(|c| crate::wire::CommandWireDefinition {
3746 name: c.name.clone(),
3747 description: c.description.clone().unwrap_or_default(),
3748 })
3749 .collect()
3750 });
3751 let wire_canvases = self.canvases.clone();
3752 let canvas_handler = self.canvas_handler.clone();
3753 let bearer_token_providers =
3754 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3755
3756 let wire = crate::wire::SessionResumeWire {
3757 session_id: self.session_id,
3758 model: self.model,
3759 client_name: self.client_name,
3760 reasoning_effort: self.reasoning_effort,
3761 reasoning_summary: self.reasoning_summary,
3762 context_tier: self.context_tier,
3763 streaming: self.streaming,
3764 system_message: self.system_message,
3765 ask_user_variant: self.ask_user_variant,
3766 tools: self.tools,
3767 canvases: wire_canvases,
3768 open_canvases: self.open_canvases,
3769 request_canvas_renderer: self.request_canvas_renderer,
3770 request_extensions: self.request_extensions,
3771 extension_sdk_path: self.extension_sdk_path,
3772 extension_info: self.extension_info,
3773 canvas_provider: self.canvas_provider,
3774 available_tools: self.available_tools,
3775 excluded_tools: self.excluded_tools,
3776 excluded_builtin_agents: self.excluded_builtin_agents,
3777 tool_filter_precedence: "excluded",
3778 mcp_servers: self.mcp_servers,
3779 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3780 embedding_cache_storage: self.embedding_cache_storage,
3781 env_value_mode: "direct",
3782 enable_config_discovery: self.enable_config_discovery,
3783 skip_embedding_retrieval: self.skip_embedding_retrieval,
3784 organization_custom_instructions: self.organization_custom_instructions,
3785 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3786 enable_file_hooks: self.enable_file_hooks,
3787 enable_host_git_operations: self.enable_host_git_operations,
3788 enable_session_store: self.enable_session_store,
3789 enable_skills: self.enable_skills,
3790 request_user_input,
3791 request_permission: permission_active,
3792 request_exit_plan_mode,
3793 request_auto_mode_switch,
3794 request_elicitation,
3795 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3796 github_mcp_tool_config: self.github_mcp_tool_config,
3797 hooks: hooks_flag,
3798 skill_directories: self.skill_directories,
3799 instruction_directories: self.instruction_directories,
3800 plugin_directories: self.plugin_directories,
3801 large_output: self.large_output,
3802 tool_search: self.tool_search,
3803 disabled_skills: self.disabled_skills,
3804 disabled_mcp_servers: self.disabled_mcp_servers,
3805 custom_agents: self.custom_agents,
3806 custom_agents_local_only: self.custom_agents_local_only,
3807 default_agent: self.default_agent,
3808 agent: self.agent,
3809 infinite_sessions: self.infinite_sessions,
3810 provider: self.provider,
3811 capi: self.capi,
3812 providers: self.providers,
3813 models: self.models,
3814 enable_session_telemetry: self.enable_session_telemetry,
3815 enable_citations: self.enable_citations,
3816 enable_file_change_tracking: self.enable_file_change_tracking,
3817 session_limits: self.session_limits,
3818 model_capabilities: self.model_capabilities,
3819 memory: self.memory,
3820 config_dir: self.config_directory,
3821 working_directory: self.working_directory,
3822 additional_directories: self.additional_directories,
3823 github_token: self.github_token,
3824 github_token_provider_registration_id: None,
3825 remote_session: self.remote_session,
3826 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3827 enable_github_telemetry_forwarding: None,
3828 commands: wire_commands,
3829 exp_assignments: self.exp_assignments,
3830 enable_managed_settings: self.enable_managed_settings,
3831 is_experimental_mode: self.enable_experimental_mode,
3832 managed_settings: self.managed_settings,
3833 suppress_resume_event: self.suppress_resume_event,
3834 continue_pending_work: self.continue_pending_work,
3835 };
3836
3837 let runtime = SessionConfigRuntime {
3838 permission_handler: self.permission_handler,
3839 permission_policy: self.permission_policy,
3840 elicitation_handler: self.elicitation_handler,
3841 mcp_auth_handler: self.mcp_auth_handler,
3842 user_input_handler: self.user_input_handler,
3843 exit_plan_mode_handler: self.exit_plan_mode_handler,
3844 auto_mode_switch_handler: self.auto_mode_switch_handler,
3845 hooks_handler: self.hooks_handler,
3846 system_message_transform: self.system_message_transform,
3847 tool_handlers,
3848 canvas_handler,
3849 session_fs_provider: self.session_fs_provider,
3850 bearer_token_providers,
3851 github_token_provider: self.github_token_provider,
3852 commands: self.commands,
3853 };
3854
3855 Ok((wire, runtime))
3856 }
3857
3858 pub fn new(session_id: SessionId) -> Self {
3863 Self {
3864 session_id,
3865 model: None,
3866 client_name: None,
3867 reasoning_effort: None,
3868 reasoning_summary: None,
3869 context_tier: None,
3870 streaming: None,
3871 system_message: None,
3872 ask_user_variant: None,
3873 tools: None,
3874 canvases: None,
3875 canvas_handler: None,
3876 open_canvases: None,
3877 request_canvas_renderer: None,
3878 request_extensions: None,
3879 extension_sdk_path: None,
3880 extension_info: None,
3881 canvas_provider: None,
3882 available_tools: None,
3883 excluded_tools: None,
3884 excluded_builtin_agents: None,
3885 included_builtin_skills: None,
3886 mcp_servers: None,
3887 mcp_oauth_token_storage: None,
3888 enable_config_discovery: None,
3889 skip_embedding_retrieval: None,
3890 organization_custom_instructions: None,
3891 enable_on_demand_instruction_discovery: None,
3892 enable_file_hooks: None,
3893 enable_host_git_operations: None,
3894 enable_session_store: None,
3895 enable_skills: None,
3896 embedding_cache_storage: None,
3897 enable_mcp_apps: None,
3898 github_mcp_tool_config: None,
3899 skill_directories: None,
3900 instruction_directories: None,
3901 plugin_directories: None,
3902 large_output: None,
3903 tool_search: None,
3904 disabled_skills: None,
3905 disabled_mcp_servers: None,
3906 hooks: None,
3907 custom_agents: None,
3908 default_agent: None,
3909 agent: None,
3910 infinite_sessions: None,
3911 provider: None,
3912 capi: None,
3913 providers: None,
3914 models: None,
3915 enable_session_telemetry: None,
3916 enable_citations: None,
3917 enable_file_change_tracking: None,
3918 session_limits: None,
3919 model_capabilities: None,
3920 memory: None,
3921 config_directory: None,
3922 working_directory: None,
3923 additional_directories: None,
3924 github_token: None,
3925 github_token_provider: None,
3926 remote_session: None,
3927 include_sub_agent_streaming_events: None,
3928 commands: None,
3929 exp_assignments: None,
3930 enable_managed_settings: None,
3931 managed_settings: None,
3932 session_fs_provider: None,
3933 suppress_resume_event: None,
3934 continue_pending_work: None,
3935 permission_handler: None,
3936 elicitation_handler: None,
3937 mcp_auth_handler: None,
3938 user_input_handler: None,
3939 exit_plan_mode_handler: None,
3940 auto_mode_switch_handler: None,
3941 hooks_handler: None,
3942 permission_policy: None,
3943 system_message_transform: None,
3944 skip_custom_instructions: None,
3945 custom_agents_local_only: None,
3946 enable_experimental_mode: None,
3947 coauthor_enabled: None,
3948 manage_schedule_enabled: None,
3949 }
3950 }
3951
3952 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3954 self.permission_handler = Some(handler);
3955 self
3956 }
3957
3958 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3960 self.elicitation_handler = Some(handler);
3961 self
3962 }
3963
3964 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3966 self.mcp_auth_handler = Some(handler);
3967 self
3968 }
3969
3970 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3972 self.user_input_handler = Some(handler);
3973 self
3974 }
3975
3976 pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self {
3978 self.ask_user_variant = Some(variant);
3979 self
3980 }
3981
3982 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3984 self.exit_plan_mode_handler = Some(handler);
3985 self
3986 }
3987
3988 pub fn with_auto_mode_switch_handler(
3990 mut self,
3991 handler: Arc<dyn AutoModeSwitchHandler>,
3992 ) -> Self {
3993 self.auto_mode_switch_handler = Some(handler);
3994 self
3995 }
3996
3997 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
4000 self.hooks_handler = Some(hooks);
4001 self
4002 }
4003
4004 pub fn with_system_message_transform(
4006 mut self,
4007 transform: Arc<dyn SystemMessageTransform>,
4008 ) -> Self {
4009 self.system_message_transform = Some(transform);
4010 self
4011 }
4012
4013 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
4017 self.commands = Some(commands);
4018 self
4019 }
4020
4021 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
4024 self.session_fs_provider = Some(provider);
4025 self
4026 }
4027
4028 pub fn approve_all_permissions(mut self) -> Self {
4031 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
4032 self
4033 }
4034
4035 pub fn deny_all_permissions(mut self) -> Self {
4038 self.permission_policy = Some(crate::permission::Policy::DenyAll);
4039 self
4040 }
4041
4042 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
4045 where
4046 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
4047 {
4048 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
4049 self
4050 }
4051
4052 pub fn with_model(mut self, model: impl Into<String>) -> Self {
4054 self.model = Some(model.into());
4055 self
4056 }
4057
4058 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
4060 self.client_name = Some(name.into());
4061 self
4062 }
4063
4064 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4066 self.reasoning_effort = Some(effort.into());
4067 self
4068 }
4069
4070 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4072 self.reasoning_summary = Some(summary);
4073 self
4074 }
4075
4076 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
4079 self.context_tier = Some(tier.into());
4080 self
4081 }
4082
4083 pub fn with_streaming(mut self, streaming: bool) -> Self {
4085 self.streaming = Some(streaming);
4086 self
4087 }
4088
4089 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
4092 self.system_message = Some(system_message);
4093 self
4094 }
4095
4096 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
4098 self.tools = Some(tools.into_iter().collect());
4099 self
4100 }
4101
4102 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
4104 self.canvases = Some(canvases.into_iter().collect());
4105 self
4106 }
4107
4108 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
4110 self.canvas_handler = Some(handler);
4111 self
4112 }
4113
4114 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
4116 mut self,
4117 open_canvases: I,
4118 ) -> Self {
4119 self.open_canvases = Some(open_canvases.into_iter().collect());
4120 self
4121 }
4122
4123 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
4125 self.request_canvas_renderer = Some(request);
4126 self
4127 }
4128
4129 pub fn with_request_extensions(mut self, request: bool) -> Self {
4131 self.request_extensions = Some(request);
4132 self
4133 }
4134
4135 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
4139 self.extension_sdk_path = Some(path.into());
4140 self
4141 }
4142
4143 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
4145 self.extension_info = Some(extension_info);
4146 self
4147 }
4148
4149 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
4152 self.canvas_provider = Some(canvas_provider);
4153 self
4154 }
4155
4156 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
4158 where
4159 I: IntoIterator<Item = S>,
4160 S: Into<String>,
4161 {
4162 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
4163 self
4164 }
4165
4166 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
4168 where
4169 I: IntoIterator<Item = S>,
4170 S: Into<String>,
4171 {
4172 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
4173 self
4174 }
4175
4176 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
4178 where
4179 I: IntoIterator<Item = S>,
4180 S: Into<String>,
4181 {
4182 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
4183 self
4184 }
4185
4186 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
4188 self.mcp_servers = Some(servers);
4189 self
4190 }
4191
4192 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
4195 self.mcp_oauth_token_storage = Some(mode.into());
4196 self
4197 }
4198
4199 pub fn with_embedding_cache_storage(
4201 mut self,
4202 embedding_cache_storage: impl Into<String>,
4203 ) -> Self {
4204 self.embedding_cache_storage = Some(embedding_cache_storage.into());
4205 self
4206 }
4207
4208 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
4211 self.enable_config_discovery = Some(enable);
4212 self
4213 }
4214
4215 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
4217 self.skip_embedding_retrieval = Some(value);
4218 self
4219 }
4220
4221 pub fn with_organization_custom_instructions(
4223 mut self,
4224 instructions: impl Into<String>,
4225 ) -> Self {
4226 self.organization_custom_instructions = Some(instructions.into());
4227 self
4228 }
4229
4230 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
4232 self.enable_on_demand_instruction_discovery = Some(value);
4233 self
4234 }
4235
4236 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
4238 self.enable_file_hooks = Some(value);
4239 self
4240 }
4241
4242 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
4244 self.enable_host_git_operations = Some(value);
4245 self
4246 }
4247
4248 pub fn with_enable_session_store(mut self, value: bool) -> Self {
4250 self.enable_session_store = Some(value);
4251 self
4252 }
4253
4254 pub fn with_enable_skills(mut self, value: bool) -> Self {
4256 self.enable_skills = Some(value);
4257 self
4258 }
4259
4260 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
4266 self.enable_mcp_apps = Some(enable);
4267 self
4268 }
4269
4270 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
4272 self.github_mcp_tool_config = Some(config);
4273 self
4274 }
4275
4276 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
4278 where
4279 I: IntoIterator<Item = P>,
4280 P: Into<PathBuf>,
4281 {
4282 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
4283 self
4284 }
4285
4286 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
4288 where
4289 I: IntoIterator<Item = S>,
4290 S: Into<String>,
4291 {
4292 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
4293 self
4294 }
4295
4296 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
4300 where
4301 I: IntoIterator<Item = P>,
4302 P: Into<PathBuf>,
4303 {
4304 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
4305 self
4306 }
4307
4308 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
4310 where
4311 I: IntoIterator<Item = P>,
4312 P: Into<PathBuf>,
4313 {
4314 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
4315 self
4316 }
4317
4318 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
4320 self.large_output = Some(config);
4321 self
4322 }
4323
4324 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
4327 self.tool_search = Some(config);
4328 self
4329 }
4330
4331 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
4333 where
4334 I: IntoIterator<Item = S>,
4335 S: Into<String>,
4336 {
4337 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
4338 self
4339 }
4340
4341 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
4343 where
4344 I: IntoIterator<Item = S>,
4345 S: Into<String>,
4346 {
4347 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
4348 self
4349 }
4350
4351 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
4353 mut self,
4354 agents: I,
4355 ) -> Self {
4356 self.custom_agents = Some(agents.into_iter().collect());
4357 self
4358 }
4359
4360 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
4362 self.default_agent = Some(agent);
4363 self
4364 }
4365
4366 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
4368 self.agent = Some(name.into());
4369 self
4370 }
4371
4372 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
4374 self.infinite_sessions = Some(config);
4375 self
4376 }
4377
4378 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
4380 self.provider = Some(provider);
4381 self
4382 }
4383
4384 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
4386 self.capi = Some(capi);
4387 self
4388 }
4389
4390 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
4396 self.providers = Some(providers);
4397 self
4398 }
4399
4400 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
4406 self.models = Some(models);
4407 self
4408 }
4409
4410 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
4414 self.enable_session_telemetry = Some(enable);
4415 self
4416 }
4417
4418 pub fn with_enable_citations(mut self, enable: bool) -> Self {
4420 self.enable_citations = Some(enable);
4421 self
4422 }
4423
4424 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
4427 self.enable_file_change_tracking = Some(enable);
4428 self
4429 }
4430
4431 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
4433 self.session_limits = Some(limits);
4434 self
4435 }
4436
4437 pub fn with_model_capabilities(
4439 mut self,
4440 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
4441 ) -> Self {
4442 self.model_capabilities = Some(capabilities);
4443 self
4444 }
4445
4446 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
4448 self.memory = Some(memory);
4449 self
4450 }
4451
4452 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4454 self.config_directory = Some(dir.into());
4455 self
4456 }
4457
4458 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4460 self.working_directory = Some(dir.into());
4461 self
4462 }
4463
4464 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
4466 where
4467 I: IntoIterator<Item = P>,
4468 P: Into<PathBuf>,
4469 {
4470 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
4471 self
4472 }
4473
4474 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
4478 self.github_token = Some(token.into());
4479 self
4480 }
4481
4482 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
4488 self.github_token_provider = Some(provider);
4489 self
4490 }
4491
4492 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4494 self.include_sub_agent_streaming_events = Some(include);
4495 self
4496 }
4497
4498 pub fn with_remote_session(
4500 mut self,
4501 mode: crate::generated::api_types::RemoteSessionMode,
4502 ) -> Self {
4503 self.remote_session = Some(mode);
4504 self
4505 }
4506
4507 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4510 self.suppress_resume_event = Some(suppress);
4511 self
4512 }
4513
4514 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4520 self.continue_pending_work = Some(continue_pending);
4521 self
4522 }
4523
4524 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4526 self.skip_custom_instructions = Some(value);
4527 self
4528 }
4529
4530 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4532 self.custom_agents_local_only = Some(value);
4533 self
4534 }
4535
4536 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
4538 self.enable_experimental_mode = Some(enable_experimental_mode);
4539 self
4540 }
4541
4542 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4544 self.coauthor_enabled = Some(value);
4545 self
4546 }
4547
4548 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4550 self.manage_schedule_enabled = Some(value);
4551 self
4552 }
4553
4554 #[doc(hidden)]
4558 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4559 self.exp_assignments = Some(assignments);
4560 self
4561 }
4562
4563 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4566 self.enable_managed_settings = Some(enabled);
4567 self
4568 }
4569
4570 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
4574 self.managed_settings = Some(managed_settings);
4575 self
4576 }
4577}
4578
4579#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4585#[serde(rename_all = "camelCase")]
4586#[non_exhaustive]
4587pub struct SystemMessageConfig {
4588 #[serde(skip_serializing_if = "Option::is_none")]
4590 pub mode: Option<String>,
4591 #[serde(skip_serializing_if = "Option::is_none")]
4593 pub content: Option<String>,
4594 #[serde(skip_serializing_if = "Option::is_none")]
4596 pub sections: Option<HashMap<String, SectionOverride>>,
4597}
4598
4599impl SystemMessageConfig {
4600 pub fn new() -> Self {
4603 Self::default()
4604 }
4605
4606 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4609 self.mode = Some(mode.into());
4610 self
4611 }
4612
4613 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4616 self.content = Some(content.into());
4617 self
4618 }
4619
4620 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4622 self.sections = Some(sections);
4623 self
4624 }
4625}
4626
4627#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4633#[serde(rename_all = "camelCase")]
4634pub struct SectionOverride {
4635 #[serde(skip_serializing_if = "Option::is_none")]
4638 pub action: Option<String>,
4639 #[serde(skip_serializing_if = "Option::is_none")]
4641 pub content: Option<String>,
4642}
4643
4644#[derive(Debug, Clone, Serialize, Deserialize)]
4646#[serde(rename_all = "camelCase")]
4647pub struct CreateSessionResult {
4648 pub session_id: SessionId,
4650 #[serde(skip_serializing_if = "Option::is_none")]
4652 pub workspace_path: Option<PathBuf>,
4653 #[serde(default, alias = "remote_url")]
4655 pub remote_url: Option<String>,
4656 #[serde(skip_serializing_if = "Option::is_none")]
4658 pub capabilities: Option<SessionCapabilities>,
4659}
4660
4661#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4663#[serde(rename_all = "camelCase")]
4664pub(crate) struct ResumeSessionResult {
4665 #[serde(default)]
4667 pub session_id: Option<SessionId>,
4668 #[serde(default, skip_serializing_if = "Option::is_none")]
4670 pub workspace_path: Option<PathBuf>,
4671 #[serde(default, alias = "remote_url")]
4673 pub remote_url: Option<String>,
4674 #[serde(default, skip_serializing_if = "Option::is_none")]
4676 pub capabilities: Option<SessionCapabilities>,
4677 #[serde(
4679 default,
4680 alias = "openCanvasInstances",
4681 skip_serializing_if = "Option::is_none"
4682 )]
4683 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4684}
4685
4686#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4688#[serde(rename_all = "lowercase")]
4689pub enum LogLevel {
4690 #[default]
4692 Info,
4693 Warning,
4695 Error,
4697}
4698
4699#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4704#[serde(rename_all = "camelCase")]
4705pub struct LogOptions {
4706 #[serde(skip_serializing_if = "Option::is_none")]
4708 pub level: Option<LogLevel>,
4709 #[serde(skip_serializing_if = "Option::is_none")]
4712 pub ephemeral: Option<bool>,
4713}
4714
4715impl LogOptions {
4716 pub fn with_level(mut self, level: LogLevel) -> Self {
4718 self.level = Some(level);
4719 self
4720 }
4721
4722 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4724 self.ephemeral = Some(ephemeral);
4725 self
4726 }
4727}
4728
4729#[derive(Debug, Clone, Default)]
4733pub struct SetModelOptions {
4734 pub reasoning_effort: Option<String>,
4737 pub reasoning_summary: Option<ReasoningSummary>,
4741 pub context_tier: Option<ContextTier>,
4744 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4748}
4749
4750impl SetModelOptions {
4751 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4753 self.reasoning_effort = Some(effort.into());
4754 self
4755 }
4756
4757 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4759 self.reasoning_summary = Some(summary);
4760 self
4761 }
4762
4763 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4765 self.context_tier = Some(tier);
4766 self
4767 }
4768
4769 pub fn with_model_capabilities(
4771 mut self,
4772 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4773 ) -> Self {
4774 self.model_capabilities = Some(caps);
4775 self
4776 }
4777}
4778
4779#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4786#[serde(rename_all = "camelCase")]
4787pub struct PingResponse {
4788 #[serde(default)]
4790 pub message: String,
4791 #[serde(default)]
4793 pub timestamp: String,
4794 #[serde(skip_serializing_if = "Option::is_none")]
4796 pub protocol_version: Option<u32>,
4797}
4798
4799#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4801#[serde(rename_all = "camelCase")]
4802pub struct AttachmentLineRange {
4803 pub start: u32,
4805 pub end: u32,
4807}
4808
4809#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4811#[serde(rename_all = "camelCase")]
4812pub struct AttachmentSelectionPosition {
4813 pub line: u32,
4815 pub character: u32,
4817}
4818
4819#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4821#[serde(rename_all = "camelCase")]
4822pub struct AttachmentSelectionRange {
4823 pub start: AttachmentSelectionPosition,
4825 pub end: AttachmentSelectionPosition,
4827}
4828
4829#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4831#[serde(rename_all = "snake_case")]
4832#[non_exhaustive]
4833pub enum GitHubReferenceType {
4834 Issue,
4836 Pr,
4838 Discussion,
4840}
4841
4842#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4848#[serde(rename_all = "camelCase")]
4849pub struct GitHubRepoPointer {
4850 #[serde(skip_serializing_if = "Option::is_none")]
4852 pub id: Option<i64>,
4853 pub name: String,
4855 pub owner: String,
4857}
4858
4859#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4861#[serde(rename_all = "camelCase")]
4862pub struct GitHubFileDiffSide {
4863 pub path: String,
4865 pub r#ref: String,
4867 pub repo: GitHubRepoPointer,
4869}
4870
4871#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4873#[serde(rename_all = "camelCase")]
4874pub struct GitHubTreeComparisonSide {
4875 pub repo: GitHubRepoPointer,
4877 pub revision: String,
4879}
4880
4881#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4883#[serde(rename_all = "camelCase")]
4884pub struct GitHubSnippetLineRange {
4885 pub start: i64,
4887 pub end: i64,
4889}
4890
4891#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4893#[serde(
4894 tag = "type",
4895 rename_all = "camelCase",
4896 rename_all_fields = "camelCase"
4897)]
4898#[non_exhaustive]
4899pub enum Attachment {
4900 File {
4902 path: PathBuf,
4904 #[serde(skip_serializing_if = "Option::is_none")]
4906 display_name: Option<String>,
4907 #[serde(skip_serializing_if = "Option::is_none")]
4909 line_range: Option<AttachmentLineRange>,
4910 },
4911 Directory {
4913 path: PathBuf,
4915 #[serde(skip_serializing_if = "Option::is_none")]
4917 display_name: Option<String>,
4918 },
4919 Selection {
4921 file_path: PathBuf,
4923 text: String,
4925 #[serde(skip_serializing_if = "Option::is_none")]
4927 display_name: Option<String>,
4928 selection: AttachmentSelectionRange,
4930 },
4931 Blob {
4933 data: String,
4935 mime_type: String,
4937 #[serde(skip_serializing_if = "Option::is_none")]
4939 display_name: Option<String>,
4940 },
4941 #[serde(rename = "github_reference")]
4943 GitHubReference {
4944 number: u64,
4946 title: String,
4948 reference_type: GitHubReferenceType,
4950 state: String,
4952 url: String,
4954 },
4955 #[serde(rename = "github_commit")]
4957 GitHubCommit {
4958 message: String,
4960 oid: String,
4962 repo: GitHubRepoPointer,
4964 url: String,
4966 },
4967 #[serde(rename = "github_release")]
4969 GitHubRelease {
4970 name: String,
4972 repo: GitHubRepoPointer,
4974 tag_name: String,
4976 url: String,
4978 },
4979 #[serde(rename = "github_actions_job")]
4981 GitHubActionsJob {
4982 #[serde(skip_serializing_if = "Option::is_none")]
4985 conclusion: Option<String>,
4986 job_id: i64,
4988 job_name: String,
4990 repo: GitHubRepoPointer,
4992 url: String,
4994 workflow_name: String,
4996 },
4997 #[serde(rename = "github_repository")]
4999 GitHubRepository {
5000 #[serde(skip_serializing_if = "Option::is_none")]
5002 description: Option<String>,
5003 #[serde(skip_serializing_if = "Option::is_none")]
5006 r#ref: Option<String>,
5007 repo: GitHubRepoPointer,
5009 url: String,
5011 },
5012 #[serde(rename = "github_file_diff")]
5014 GitHubFileDiff {
5015 #[serde(skip_serializing_if = "Option::is_none")]
5017 base: Option<GitHubFileDiffSide>,
5018 #[serde(skip_serializing_if = "Option::is_none")]
5020 head: Option<GitHubFileDiffSide>,
5021 url: String,
5023 },
5024 #[serde(rename = "github_tree_comparison")]
5026 GitHubTreeComparison {
5027 base: GitHubTreeComparisonSide,
5029 head: GitHubTreeComparisonSide,
5031 url: String,
5033 },
5034 #[serde(rename = "github_url")]
5036 GitHubUrl {
5037 url: String,
5039 },
5040 #[serde(rename = "github_file")]
5042 GitHubFile {
5043 path: String,
5045 r#ref: String,
5047 repo: GitHubRepoPointer,
5049 url: String,
5051 },
5052 #[serde(rename = "github_snippet")]
5054 GitHubSnippet {
5055 line_range: GitHubSnippetLineRange,
5057 path: String,
5059 r#ref: String,
5061 repo: GitHubRepoPointer,
5063 url: String,
5065 },
5066}
5067
5068impl Attachment {
5069 pub fn display_name(&self) -> Option<&str> {
5071 match self {
5072 Self::File { display_name, .. }
5073 | Self::Directory { display_name, .. }
5074 | Self::Selection { display_name, .. }
5075 | Self::Blob { display_name, .. } => display_name.as_deref(),
5076 Self::GitHubReference { .. }
5077 | Self::GitHubCommit { .. }
5078 | Self::GitHubRelease { .. }
5079 | Self::GitHubActionsJob { .. }
5080 | Self::GitHubRepository { .. }
5081 | Self::GitHubFileDiff { .. }
5082 | Self::GitHubTreeComparison { .. }
5083 | Self::GitHubUrl { .. }
5084 | Self::GitHubFile { .. }
5085 | Self::GitHubSnippet { .. } => None,
5086 }
5087 }
5088
5089 pub fn label(&self) -> Option<String> {
5091 if let Some(display_name) = self
5092 .display_name()
5093 .map(str::trim)
5094 .filter(|name| !name.is_empty())
5095 {
5096 return Some(display_name.to_string());
5097 }
5098
5099 match self {
5100 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
5101 format!("#{}", number)
5102 } else {
5103 title.trim().to_string()
5104 }),
5105 _ => self.derived_display_name(),
5106 }
5107 }
5108
5109 pub fn ensure_display_name(&mut self) {
5111 if self
5112 .display_name()
5113 .map(str::trim)
5114 .is_some_and(|name| !name.is_empty())
5115 {
5116 return;
5117 }
5118
5119 let Some(derived_display_name) = self.derived_display_name() else {
5120 return;
5121 };
5122
5123 match self {
5124 Self::File { display_name, .. }
5125 | Self::Directory { display_name, .. }
5126 | Self::Selection { display_name, .. }
5127 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
5128 Self::GitHubReference { .. }
5129 | Self::GitHubCommit { .. }
5130 | Self::GitHubRelease { .. }
5131 | Self::GitHubActionsJob { .. }
5132 | Self::GitHubRepository { .. }
5133 | Self::GitHubFileDiff { .. }
5134 | Self::GitHubTreeComparison { .. }
5135 | Self::GitHubUrl { .. }
5136 | Self::GitHubFile { .. }
5137 | Self::GitHubSnippet { .. } => {}
5138 }
5139 }
5140
5141 fn derived_display_name(&self) -> Option<String> {
5142 match self {
5143 Self::File { path, .. } | Self::Directory { path, .. } => {
5144 Some(attachment_name_from_path(path))
5145 }
5146 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
5147 Self::Blob { .. } => Some("attachment".to_string()),
5148 Self::GitHubReference { .. }
5149 | Self::GitHubCommit { .. }
5150 | Self::GitHubRelease { .. }
5151 | Self::GitHubActionsJob { .. }
5152 | Self::GitHubRepository { .. }
5153 | Self::GitHubFileDiff { .. }
5154 | Self::GitHubTreeComparison { .. }
5155 | Self::GitHubUrl { .. }
5156 | Self::GitHubFile { .. }
5157 | Self::GitHubSnippet { .. } => None,
5158 }
5159 }
5160}
5161
5162fn attachment_name_from_path(path: &Path) -> String {
5163 path.file_name()
5164 .map(|name| name.to_string_lossy().into_owned())
5165 .filter(|name| !name.is_empty())
5166 .unwrap_or_else(|| {
5167 let full = path.to_string_lossy();
5168 if full.is_empty() {
5169 "attachment".to_string()
5170 } else {
5171 full.into_owned()
5172 }
5173 })
5174}
5175
5176pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
5178 for attachment in attachments {
5179 attachment.ensure_display_name();
5180 }
5181}
5182
5183#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5188#[serde(rename_all = "lowercase")]
5189#[non_exhaustive]
5190pub enum DeliveryMode {
5191 Enqueue,
5193 Immediate,
5195}
5196
5197#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5202#[serde(rename_all = "lowercase")]
5203#[non_exhaustive]
5204pub enum AgentMode {
5205 Interactive,
5207 Plan,
5209 Autopilot,
5211 Shell,
5213}
5214
5215#[derive(Debug, Clone)]
5244#[non_exhaustive]
5245pub struct MessageOptions {
5246 pub prompt: String,
5248 pub mode: Option<DeliveryMode>,
5254 pub agent_mode: Option<AgentMode>,
5258 pub attachments: Option<Vec<Attachment>>,
5260 pub wait_timeout: Option<Duration>,
5263 pub request_headers: Option<HashMap<String, String>>,
5267 pub traceparent: Option<String>,
5274 pub tracestate: Option<String>,
5278 pub display_prompt: Option<String>,
5280}
5281
5282impl MessageOptions {
5283 pub fn new(prompt: impl Into<String>) -> Self {
5285 Self {
5286 prompt: prompt.into(),
5287 mode: None,
5288 agent_mode: None,
5289 attachments: None,
5290 wait_timeout: None,
5291 request_headers: None,
5292 traceparent: None,
5293 tracestate: None,
5294 display_prompt: None,
5295 }
5296 }
5297
5298 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
5304 self.mode = Some(mode);
5305 self
5306 }
5307
5308 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
5312 self.agent_mode = Some(agent_mode);
5313 self
5314 }
5315
5316 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
5318 self.attachments = Some(attachments);
5319 self
5320 }
5321
5322 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
5324 self.wait_timeout = Some(timeout);
5325 self
5326 }
5327
5328 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
5330 self.request_headers = Some(headers);
5331 self
5332 }
5333
5334 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
5339 self.traceparent = ctx.traceparent;
5340 self.tracestate = ctx.tracestate;
5341 self
5342 }
5343
5344 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5346 self.traceparent = Some(traceparent.into());
5347 self
5348 }
5349
5350 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5352 self.tracestate = Some(tracestate.into());
5353 self
5354 }
5355
5356 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5358 self.display_prompt = Some(display_prompt.into());
5359 self
5360 }
5361}
5362
5363impl From<&str> for MessageOptions {
5364 fn from(prompt: &str) -> Self {
5365 Self::new(prompt)
5366 }
5367}
5368
5369impl From<String> for MessageOptions {
5370 fn from(prompt: String) -> Self {
5371 Self::new(prompt)
5372 }
5373}
5374
5375impl From<&String> for MessageOptions {
5376 fn from(prompt: &String) -> Self {
5377 Self::new(prompt.clone())
5378 }
5379}
5380
5381#[derive(Debug, Clone, Serialize, Deserialize)]
5383#[serde(rename_all = "camelCase")]
5384#[non_exhaustive]
5385pub struct GetStatusResponse {
5386 pub version: String,
5388 pub protocol_version: u32,
5390}
5391
5392#[derive(Debug, Clone, Serialize, Deserialize)]
5394#[serde(rename_all = "camelCase")]
5395#[non_exhaustive]
5396pub struct GetAuthStatusResponse {
5397 pub is_authenticated: bool,
5399 #[serde(skip_serializing_if = "Option::is_none")]
5402 pub auth_type: Option<String>,
5403 #[serde(skip_serializing_if = "Option::is_none")]
5405 pub host: Option<String>,
5406 #[serde(skip_serializing_if = "Option::is_none")]
5408 pub login: Option<String>,
5409 #[serde(skip_serializing_if = "Option::is_none")]
5411 pub status_message: Option<String>,
5412}
5413
5414#[derive(Debug, Clone, Serialize, Deserialize)]
5418#[serde(rename_all = "camelCase")]
5419pub struct SessionEventNotification {
5420 pub session_id: SessionId,
5422 pub event: SessionEvent,
5424}
5425
5426#[derive(Debug, Clone, Serialize, Deserialize)]
5433#[serde(rename_all = "camelCase")]
5434pub struct SessionEvent {
5435 pub id: String,
5437 pub timestamp: String,
5439 pub parent_id: Option<String>,
5441 #[serde(skip_serializing_if = "Option::is_none")]
5443 pub ephemeral: Option<bool>,
5444 #[serde(skip_serializing_if = "Option::is_none")]
5447 pub agent_id: Option<String>,
5448 #[serde(skip_serializing_if = "Option::is_none")]
5450 pub debug_cli_received_at_ms: Option<i64>,
5451 #[serde(skip_serializing_if = "Option::is_none")]
5453 pub debug_ws_forwarded_at_ms: Option<i64>,
5454 #[serde(rename = "type")]
5456 pub event_type: String,
5457 pub data: Value,
5459}
5460
5461impl SessionEvent {
5462 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5467 use serde::de::IntoDeserializer;
5468 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5469 self.event_type.as_str().into_deserializer();
5470 crate::generated::SessionEventType::deserialize(deserializer)
5471 .unwrap_or(crate::generated::SessionEventType::Unknown)
5472 }
5473
5474 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5480 serde_json::from_value(self.data.clone()).ok()
5481 }
5482
5483 pub fn is_transient_error(&self) -> bool {
5487 self.event_type == "session.error"
5488 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5489 }
5490}
5491
5492#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5497#[serde(rename_all = "camelCase")]
5498#[non_exhaustive]
5499pub struct ToolInvocation {
5500 pub session_id: SessionId,
5502 pub tool_call_id: String,
5504 pub tool_name: String,
5506 pub arguments: Value,
5508 #[serde(skip)]
5516 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5517 #[serde(default, skip_serializing_if = "Option::is_none")]
5522 pub traceparent: Option<String>,
5523 #[serde(default, skip_serializing_if = "Option::is_none")]
5526 pub tracestate: Option<String>,
5527}
5528
5529impl ToolInvocation {
5530 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5551 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5552 }
5553
5554 pub fn trace_context(&self) -> TraceContext {
5557 TraceContext {
5558 traceparent: self.traceparent.clone(),
5559 tracestate: self.tracestate.clone(),
5560 }
5561 }
5562}
5563
5564#[derive(Debug, Clone, Serialize, Deserialize)]
5566#[serde(rename_all = "camelCase")]
5567pub struct ToolBinaryResult {
5568 pub data: String,
5570 pub mime_type: String,
5572 pub r#type: String,
5574 #[serde(default, skip_serializing_if = "Option::is_none")]
5576 pub description: Option<String>,
5577}
5578
5579#[derive(Debug, Clone, Serialize, Deserialize)]
5586#[serde(rename_all = "camelCase")]
5587#[non_exhaustive]
5588pub struct ToolResultExpanded {
5589 pub text_result_for_llm: String,
5591 pub result_type: String,
5593 #[serde(default, skip_serializing_if = "Option::is_none")]
5595 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5596 #[serde(skip_serializing_if = "Option::is_none")]
5598 pub session_log: Option<String>,
5599 #[serde(skip_serializing_if = "Option::is_none")]
5601 pub error: Option<String>,
5602 #[serde(default, skip_serializing_if = "Option::is_none")]
5604 pub tool_telemetry: Option<HashMap<String, Value>>,
5605 #[serde(default, skip_serializing_if = "Option::is_none")]
5607 pub tool_references: Option<Vec<String>>,
5608}
5609
5610impl ToolResultExpanded {
5611 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5615 Self {
5616 text_result_for_llm: text_result_for_llm.into(),
5617 result_type: result_type.into(),
5618 binary_results_for_llm: None,
5619 session_log: None,
5620 error: None,
5621 tool_telemetry: None,
5622 tool_references: None,
5623 }
5624 }
5625
5626 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5628 self.binary_results_for_llm = Some(results);
5629 self
5630 }
5631
5632 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5634 self.session_log = Some(session_log.into());
5635 self
5636 }
5637
5638 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5640 self.error = Some(error.into());
5641 self
5642 }
5643
5644 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5646 self.tool_telemetry = Some(telemetry);
5647 self
5648 }
5649
5650 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5652 where
5653 I: IntoIterator<Item = S>,
5654 S: Into<String>,
5655 {
5656 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5657 self
5658 }
5659}
5660
5661#[derive(Debug, Clone, Serialize, Deserialize)]
5663#[serde(untagged)]
5664#[non_exhaustive]
5665pub enum ToolResult {
5666 Text(String),
5668 Expanded(ToolResultExpanded),
5670}
5671
5672#[derive(Debug, Clone, Serialize, Deserialize)]
5674#[serde(rename_all = "camelCase")]
5675pub struct ToolResultResponse {
5676 pub result: ToolResult,
5678}
5679
5680#[derive(Debug, Clone, Serialize, Deserialize)]
5682#[serde(rename_all = "camelCase")]
5683pub struct SessionMetadata {
5684 pub session_id: SessionId,
5686 pub start_time: String,
5688 pub modified_time: String,
5690 #[serde(skip_serializing_if = "Option::is_none")]
5692 pub summary: Option<String>,
5693 pub is_remote: bool,
5695}
5696
5697#[derive(Debug, Clone, Serialize, Deserialize)]
5699#[serde(rename_all = "camelCase")]
5700pub struct ListSessionsResponse {
5701 pub sessions: Vec<SessionMetadata>,
5703}
5704
5705#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5709#[serde(rename_all = "camelCase")]
5710pub struct SessionListFilter {
5711 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5713 pub working_directory: Option<String>,
5714 #[serde(default, skip_serializing_if = "Option::is_none")]
5716 pub git_root: Option<String>,
5717 #[serde(default, skip_serializing_if = "Option::is_none")]
5719 pub repository: Option<String>,
5720 #[serde(default, skip_serializing_if = "Option::is_none")]
5722 pub branch: Option<String>,
5723}
5724
5725#[derive(Debug, Clone, Serialize, Deserialize)]
5727#[serde(rename_all = "camelCase")]
5728pub struct GetSessionMetadataResponse {
5729 #[serde(skip_serializing_if = "Option::is_none")]
5731 pub session: Option<SessionMetadata>,
5732}
5733
5734#[derive(Debug, Clone, Serialize, Deserialize)]
5736#[serde(rename_all = "camelCase")]
5737pub struct GetLastSessionIdResponse {
5738 #[serde(skip_serializing_if = "Option::is_none")]
5740 pub session_id: Option<SessionId>,
5741}
5742
5743#[derive(Debug, Clone, Serialize, Deserialize)]
5745#[serde(rename_all = "camelCase")]
5746pub struct GetForegroundSessionResponse {
5747 #[serde(skip_serializing_if = "Option::is_none")]
5749 pub session_id: Option<SessionId>,
5750}
5751
5752#[derive(Debug, Clone, Serialize, Deserialize)]
5754#[serde(rename_all = "camelCase")]
5755pub struct GetMessagesResponse {
5756 pub events: Vec<SessionEvent>,
5758}
5759
5760#[derive(Debug, Clone, Serialize, Deserialize)]
5762#[serde(rename_all = "camelCase")]
5763pub struct ElicitationResult {
5764 pub action: String,
5766 #[serde(skip_serializing_if = "Option::is_none")]
5768 pub content: Option<Value>,
5769}
5770
5771#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5777#[serde(rename_all = "camelCase")]
5778#[non_exhaustive]
5779pub enum ElicitationMode {
5780 Form,
5782 Url,
5784 #[serde(other)]
5786 Unknown,
5787}
5788
5789#[derive(Debug, Clone, Serialize, Deserialize)]
5796#[serde(rename_all = "camelCase")]
5797pub struct ElicitationRequest {
5798 pub message: String,
5800 #[serde(skip_serializing_if = "Option::is_none")]
5802 pub requested_schema: Option<Value>,
5803 #[serde(skip_serializing_if = "Option::is_none")]
5805 pub mode: Option<ElicitationMode>,
5806 #[serde(skip_serializing_if = "Option::is_none")]
5808 pub elicitation_source: Option<String>,
5809 #[serde(skip_serializing_if = "Option::is_none")]
5811 pub url: Option<String>,
5812}
5813
5814#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5819#[serde(rename_all = "camelCase")]
5820pub struct SessionCapabilities {
5821 #[serde(skip_serializing_if = "Option::is_none")]
5823 pub ui: Option<UiCapabilities>,
5824}
5825
5826#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5828#[serde(rename_all = "camelCase")]
5829pub struct UiCapabilities {
5830 #[serde(skip_serializing_if = "Option::is_none")]
5832 pub elicitation: Option<bool>,
5833 #[serde(skip_serializing_if = "Option::is_none")]
5844 pub mcp_apps: Option<bool>,
5845 #[serde(skip_serializing_if = "Option::is_none")]
5847 pub canvases: Option<bool>,
5848}
5849
5850#[derive(Debug, Clone, Default)]
5852pub struct UiInputOptions<'a> {
5853 pub title: Option<&'a str>,
5855 pub description: Option<&'a str>,
5857 pub min_length: Option<u64>,
5859 pub max_length: Option<u64>,
5861 pub format: Option<InputFormat>,
5863 pub default: Option<&'a str>,
5865}
5866
5867#[derive(Debug, Clone, Copy)]
5869#[non_exhaustive]
5870pub enum InputFormat {
5871 Email,
5873 Uri,
5875 Date,
5877 DateTime,
5879}
5880
5881impl InputFormat {
5882 pub fn as_str(&self) -> &'static str {
5884 match self {
5885 Self::Email => "email",
5886 Self::Uri => "uri",
5887 Self::Date => "date",
5888 Self::DateTime => "date-time",
5889 }
5890 }
5891}
5892
5893pub use crate::generated::api_types::{
5898 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5899 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5900 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5901 PermissionDecisionApproveOnce, PermissionDecisionContext, PermissionDecisionOutcome,
5902 PermissionDecisionReject, PermissionDecisionSource, PermissionDecisionSurface,
5903 PermissionDecisionUserNotAvailable, PermissionResponseCapability,
5904};
5905
5906#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5912#[serde(rename_all = "kebab-case")]
5913#[non_exhaustive]
5914pub enum PermissionRequestKind {
5915 Shell,
5917 Write,
5919 Read,
5921 Url,
5923 Mcp,
5925 CustomTool,
5927 Memory,
5929 Hook,
5931 #[serde(other)]
5934 Unknown,
5935}
5936
5937#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5943#[serde(rename_all = "camelCase")]
5944pub struct PermissionRequestData {
5945 #[serde(default, skip_serializing_if = "Option::is_none")]
5949 pub kind: Option<PermissionRequestKind>,
5950 #[serde(default, skip_serializing_if = "Option::is_none")]
5953 pub tool_call_id: Option<String>,
5954 #[serde(default, skip_serializing_if = "Option::is_none")]
5956 pub managed_approval_required: Option<bool>,
5957 #[serde(default, skip_serializing_if = "is_false")]
5959 pub managed_settings_enabled: bool,
5960 #[serde(flatten)]
5964 pub extra: Value,
5965}
5966
5967#[derive(Debug, Clone, Serialize, Deserialize)]
5969#[serde(rename_all = "camelCase")]
5970pub struct ExitPlanModeData {
5971 #[serde(default)]
5973 pub summary: String,
5974 #[serde(default, skip_serializing_if = "Option::is_none")]
5976 pub plan_content: Option<String>,
5977 #[serde(default)]
5979 pub actions: Vec<String>,
5980 #[serde(default = "default_recommended_action")]
5982 pub recommended_action: String,
5983}
5984
5985fn default_recommended_action() -> String {
5986 "autopilot".to_string()
5987}
5988
5989impl Default for ExitPlanModeData {
5990 fn default() -> Self {
5991 Self {
5992 summary: String::new(),
5993 plan_content: None,
5994 actions: Vec::new(),
5995 recommended_action: default_recommended_action(),
5996 }
5997 }
5998}
5999
6000#[cfg(test)]
6001mod tests {
6002 use std::collections::HashMap;
6003 use std::path::PathBuf;
6004
6005 use serde_json::json;
6006
6007 use super::{
6008 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
6009 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
6010 CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry,
6011 ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
6012 InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
6013 MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig,
6014 ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
6015 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
6016 ToolResultResponse, ensure_attachment_display_names,
6017 };
6018 use crate::generated::session_events::TypedSessionEvent;
6019
6020 #[test]
6021 fn permission_response_capability_is_publicly_exported() {
6022 assert_eq!(
6023 serde_json::to_value(PermissionResponseCapability::Interactive).unwrap(),
6024 json!("interactive")
6025 );
6026 }
6027
6028 #[test]
6029 fn tool_builder_composes() {
6030 let tool = Tool::new("greet")
6031 .with_description("Say hello")
6032 .with_namespaced_name("hello/greet")
6033 .with_instructions("Pass the user's name")
6034 .with_parameters(json!({
6035 "type": "object",
6036 "properties": { "name": { "type": "string" } },
6037 "required": ["name"]
6038 }))
6039 .with_overrides_built_in_tool(true)
6040 .with_skip_permission(true);
6041 assert_eq!(tool.name, "greet");
6042 assert_eq!(tool.description, "Say hello");
6043 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
6044 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
6045 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
6046 assert!(tool.overrides_built_in_tool);
6047 assert!(tool.skip_permission);
6048 }
6049
6050 #[test]
6051 fn tool_defer_serialization() {
6052 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
6053 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
6054 let value = serde_json::to_value(&tool).unwrap();
6055 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
6056
6057 let plain = Tool::new("plain");
6058 let value = serde_json::to_value(&plain).unwrap();
6059 assert!(value.get("defer").is_none());
6060 }
6061
6062 #[test]
6063 fn tool_metadata_serialization() {
6064 use indexmap::IndexMap;
6065
6066 let mut metadata = IndexMap::new();
6067 metadata.insert(
6068 "github.com/copilot:safeForTelemetry".to_string(),
6069 json!({ "name": true, "inputsNames": false }),
6070 );
6071 let tool = Tool::new("lookup").with_metadata(metadata);
6072 let value = serde_json::to_value(&tool).unwrap();
6073 assert_eq!(
6074 value
6075 .get("metadata")
6076 .unwrap()
6077 .get("github.com/copilot:safeForTelemetry")
6078 .unwrap(),
6079 &json!({ "name": true, "inputsNames": false })
6080 );
6081
6082 let plain = Tool::new("plain");
6084 let value = serde_json::to_value(&plain).unwrap();
6085 assert!(value.get("metadata").is_none());
6086 }
6087
6088 #[test]
6089 fn custom_agent_config_builder_with_model() {
6090 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
6091 .with_model("claude-haiku-4.5")
6092 .with_display_name("My Agent");
6093 assert_eq!(agent.name, "my-agent");
6094 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
6095 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
6096 }
6097
6098 #[test]
6099 fn custom_agent_config_serializes_model() {
6100 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
6101 let wire = serde_json::to_value(&agent).unwrap();
6102 assert_eq!(wire["model"], "claude-haiku-4.5");
6103 assert_eq!(wire["name"], "model-agent");
6104 }
6105
6106 #[test]
6107 fn custom_agent_config_omits_model_when_none() {
6108 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
6109 let wire = serde_json::to_value(&agent).unwrap();
6110 assert!(wire.get("model").is_none());
6111 }
6112
6113 #[test]
6114 fn custom_agent_config_builder_with_reasoning_effort() {
6115 let agent =
6116 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6117 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
6118 }
6119
6120 #[test]
6121 fn custom_agent_config_serializes_reasoning_effort() {
6122 let agent =
6123 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6124 let wire = serde_json::to_value(&agent).unwrap();
6125 assert_eq!(wire["reasoningEffort"], "high");
6126 }
6127
6128 #[test]
6129 fn custom_agent_config_omits_reasoning_effort_when_none() {
6130 let agent = CustomAgentConfig::new("default-agent", "prompt");
6131 let wire = serde_json::to_value(&agent).unwrap();
6132 assert!(wire.get("reasoningEffort").is_none());
6133 }
6134
6135 #[test]
6136 #[should_panic(expected = "tool parameter schema must be a JSON object")]
6137 fn tool_with_parameters_panics_on_non_object_value() {
6138 let _ = Tool::new("noop").with_parameters(json!(null));
6139 }
6140
6141 #[test]
6142 fn tool_result_expanded_serializes_binary_results_for_llm() {
6143 let response = ToolResultResponse {
6144 result: ToolResult::Expanded(ToolResultExpanded {
6145 text_result_for_llm: "rendered chart".to_string(),
6146 result_type: "success".to_string(),
6147 binary_results_for_llm: Some(vec![ToolBinaryResult {
6148 data: "aW1n".to_string(),
6149 mime_type: "image/png".to_string(),
6150 r#type: "image".to_string(),
6151 description: Some("chart preview".to_string()),
6152 }]),
6153 session_log: None,
6154 error: None,
6155 tool_telemetry: None,
6156 tool_references: None,
6157 }),
6158 };
6159
6160 let wire = serde_json::to_value(&response).unwrap();
6161
6162 assert_eq!(
6163 wire,
6164 json!({
6165 "result": {
6166 "textResultForLlm": "rendered chart",
6167 "resultType": "success",
6168 "binaryResultsForLlm": [
6169 {
6170 "data": "aW1n",
6171 "mimeType": "image/png",
6172 "type": "image",
6173 "description": "chart preview"
6174 }
6175 ]
6176 }
6177 })
6178 );
6179 }
6180
6181 #[test]
6182 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
6183 let response = ToolResultResponse {
6184 result: ToolResult::Expanded(ToolResultExpanded {
6185 text_result_for_llm: "ok".to_string(),
6186 result_type: "success".to_string(),
6187 binary_results_for_llm: None,
6188 session_log: None,
6189 error: None,
6190 tool_telemetry: None,
6191 tool_references: None,
6192 }),
6193 };
6194
6195 let wire = serde_json::to_value(&response).unwrap();
6196
6197 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6198 assert!(wire["result"].get("binaryResultsForLlm").is_none());
6199 }
6200
6201 #[test]
6202 fn tool_result_expanded_serializes_tool_references() {
6203 let response = ToolResultResponse {
6204 result: ToolResult::Expanded(
6205 ToolResultExpanded::new("found 2 tools", "success")
6206 .with_tool_references(["get_weather", "check_status"]),
6207 ),
6208 };
6209
6210 let wire = serde_json::to_value(&response).unwrap();
6211
6212 assert_eq!(
6213 wire,
6214 json!({
6215 "result": {
6216 "textResultForLlm": "found 2 tools",
6217 "resultType": "success",
6218 "toolReferences": ["get_weather", "check_status"]
6219 }
6220 })
6221 );
6222 }
6223
6224 #[test]
6225 fn tool_result_expanded_omits_tool_references_when_none() {
6226 let response = ToolResultResponse {
6227 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
6228 };
6229
6230 let wire = serde_json::to_value(&response).unwrap();
6231
6232 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6233 assert!(wire["result"].get("toolReferences").is_none());
6234 }
6235
6236 #[test]
6237 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
6238 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
6241 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
6242
6243 assert_eq!(
6244 expanded.tool_references.as_deref(),
6245 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6246 );
6247 }
6248
6249 #[test]
6250 fn tool_result_expanded_deserializes_tool_references() {
6251 let wire = json!({
6252 "textResultForLlm": "found tools",
6253 "resultType": "success",
6254 "toolReferences": ["alpha", "beta"]
6255 });
6256
6257 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
6258
6259 assert_eq!(
6260 expanded.tool_references.as_deref(),
6261 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6262 );
6263 }
6264
6265 #[test]
6266 fn session_config_default_wire_flags_off_without_handlers() {
6267 let cfg = SessionConfig::default();
6268 assert_eq!(cfg.mcp_oauth_token_storage, None);
6269 let (wire, _runtime) = cfg
6273 .into_wire(Some(SessionId::from("default-flags")))
6274 .expect("default config has no duplicate handlers");
6275 assert!(!wire.request_user_input);
6276 assert!(!wire.request_permission);
6277 assert!(!wire.request_elicitation);
6278 assert!(!wire.request_exit_plan_mode);
6279 assert!(!wire.request_auto_mode_switch);
6280 assert!(!wire.hooks);
6281 assert!(!wire.request_mcp_apps);
6282 let json = serde_json::to_value(&wire).unwrap();
6283 assert!(json.get("askUserVariant").is_none());
6284 }
6285
6286 #[test]
6287 fn resume_session_config_new_wire_flags_off_without_handlers() {
6288 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
6289 assert_eq!(cfg.mcp_oauth_token_storage, None);
6290 let (wire, _runtime) = cfg
6291 .into_wire()
6292 .expect("default resume config has no duplicate handlers");
6293 assert!(!wire.request_user_input);
6294 assert!(!wire.request_permission);
6295 assert!(!wire.request_elicitation);
6296 assert!(!wire.request_exit_plan_mode);
6297 assert!(!wire.request_auto_mode_switch);
6298 assert!(!wire.hooks);
6299 assert!(!wire.request_mcp_apps);
6300 let json = serde_json::to_value(&wire).unwrap();
6301 assert!(json.get("askUserVariant").is_none());
6302 }
6303
6304 #[test]
6305 fn custom_agents_local_only_serializes_on_create_and_resume() {
6306 let (create_wire, _) = SessionConfig::default()
6307 .with_custom_agents_local_only(false)
6308 .into_wire(Some(SessionId::from("create-locality")))
6309 .expect("create config has no duplicate handlers");
6310 let create_json = serde_json::to_value(&create_wire).unwrap();
6311 assert_eq!(create_json["customAgentsLocalOnly"], false);
6312
6313 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
6314 .with_custom_agents_local_only(false)
6315 .into_wire()
6316 .expect("resume config has no duplicate handlers");
6317 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6318 assert_eq!(resume_json["customAgentsLocalOnly"], false);
6319
6320 let (unset_create_wire, _) = SessionConfig::default()
6321 .into_wire(Some(SessionId::from("create-unset")))
6322 .expect("create config has no duplicate handlers");
6323 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6324 assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
6325
6326 let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
6327 .into_wire()
6328 .expect("resume config has no duplicate handlers");
6329 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6330 assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
6331 }
6332
6333 #[test]
6334 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6335 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
6336 assert_eq!(cfg.enable_mcp_apps, Some(true));
6337
6338 let (wire, _runtime) = cfg
6339 .into_wire(Some(SessionId::from("enable-mcp-apps")))
6340 .expect("enable_mcp_apps config has no duplicate handlers");
6341 assert!(wire.request_mcp_apps);
6342
6343 let json = serde_json::to_value(&wire).unwrap();
6344 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6345 }
6346
6347 #[test]
6348 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6349 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
6350 .with_enable_mcp_apps(true);
6351 assert_eq!(cfg.enable_mcp_apps, Some(true));
6352
6353 let (wire, _runtime) = cfg
6354 .into_wire()
6355 .expect("resume enable_mcp_apps config has no duplicate handlers");
6356 assert!(wire.request_mcp_apps);
6357
6358 let json = serde_json::to_value(&wire).unwrap();
6359 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6360 }
6361
6362 #[test]
6363 fn github_mcp_tool_config_serializes_for_create_and_resume() {
6364 let github_config = GitHubMcpToolConfig::new()
6365 .with_enable_all_tools(true)
6366 .with_additional_toolsets(["repos"])
6367 .with_additional_tools(["get_issue"])
6368 .with_enable_insiders_mode(true)
6369 .with_disable_form_deferral(true);
6370
6371 let (create_wire, _) = SessionConfig::default()
6372 .with_github_mcp_tool_config(github_config.clone())
6373 .into_wire(Some(SessionId::from("github-mcp")))
6374 .expect("create config has no duplicate handlers");
6375 assert_eq!(
6376 serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6377 serde_json::json!({
6378 "enableAllTools": true,
6379 "additionalToolsets": ["repos"],
6380 "additionalTools": ["get_issue"],
6381 "enableInsidersMode": true,
6382 "disableFormDeferral": true,
6383 })
6384 );
6385
6386 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6387 .with_github_mcp_tool_config(github_config)
6388 .into_wire()
6389 .expect("resume config has no duplicate handlers");
6390 assert!(resume_wire.github_mcp_tool_config.is_some());
6391
6392 let (unset_wire, _) = SessionConfig::default()
6393 .into_wire(Some(SessionId::from("github-mcp-unset")))
6394 .expect("default config has no duplicate handlers");
6395 assert!(
6396 serde_json::to_value(&unset_wire)
6397 .unwrap()
6398 .get("githubMcpToolConfig")
6399 .is_none()
6400 );
6401 }
6402
6403 #[test]
6404 fn memory_configuration_constructors_and_serde() {
6405 assert!(MemoryConfiguration::enabled().enabled);
6406 assert!(!MemoryConfiguration::disabled().enabled);
6407 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6408
6409 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6410 assert_eq!(json, serde_json::json!({ "enabled": true }));
6411 }
6412
6413 #[test]
6414 fn session_config_with_memory_serializes() {
6415 let (wire, _runtime) = SessionConfig::default()
6416 .with_memory(MemoryConfiguration::enabled())
6417 .into_wire(Some(SessionId::from("memory-on")))
6418 .expect("no duplicate handlers");
6419 let json = serde_json::to_value(&wire).unwrap();
6420 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6421
6422 let (wire_off, _) = SessionConfig::default()
6423 .with_memory(MemoryConfiguration::disabled())
6424 .into_wire(Some(SessionId::from("memory-off")))
6425 .expect("no duplicate handlers");
6426 let json_off = serde_json::to_value(&wire_off).unwrap();
6427 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6428
6429 let (empty_wire, _) = SessionConfig::default()
6431 .into_wire(Some(SessionId::from("memory-unset")))
6432 .expect("no duplicate handlers");
6433 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6434 assert!(empty_json.get("memory").is_none());
6435 }
6436
6437 #[test]
6438 fn resume_session_config_with_memory_serializes() {
6439 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6440 .with_memory(MemoryConfiguration::enabled())
6441 .into_wire()
6442 .expect("no duplicate handlers");
6443 let json = serde_json::to_value(&wire).unwrap();
6444 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6445
6446 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6448 .into_wire()
6449 .expect("no duplicate handlers");
6450 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6451 assert!(empty_json.get("memory").is_none());
6452 }
6453
6454 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6455 CopilotExpAssignmentResponse {
6456 features: vec!["copilot_exp_flag".to_string()],
6457 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6458 configs: vec![ExpConfigEntry {
6459 id: "cfg-1".to_string(),
6460 parameters: HashMap::from([
6461 ("threshold".to_string(), ExpFlagValue::Integer(5)),
6462 ("enabled".to_string(), ExpFlagValue::Bool(true)),
6463 ]),
6464 }],
6465 assignment_context: context.to_string(),
6466 ..Default::default()
6467 }
6468 }
6469
6470 #[test]
6471 fn exp_flag_value_round_trips_all_variants() {
6472 let values = serde_json::json!({
6473 "s": "text",
6474 "i": 7,
6475 "f": 1.5,
6476 "b": true,
6477 "n": null,
6478 });
6479 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6480 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6481 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6482 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6483 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6484 assert_eq!(parsed["n"], ExpFlagValue::Null);
6485 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6486 }
6487
6488 #[test]
6489 fn session_config_with_exp_assignments_serializes() {
6490 let assignments = sample_exp_assignments("ctx-123");
6491 let expected = serde_json::to_value(&assignments).unwrap();
6492 let (wire, _runtime) = SessionConfig::default()
6493 .with_exp_assignments(assignments)
6494 .into_wire(Some(SessionId::from("exp-on")))
6495 .expect("no duplicate handlers");
6496 let json = serde_json::to_value(&wire).unwrap();
6497 assert_eq!(json["expAssignments"], expected);
6498 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6499 assert_eq!(
6500 json["expAssignments"]["Flights"]["copilot_exp_flag"],
6501 "treatment"
6502 );
6503
6504 let (empty_wire, _) = SessionConfig::default()
6506 .into_wire(Some(SessionId::from("exp-unset")))
6507 .expect("no duplicate handlers");
6508 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6509 assert!(empty_json.get("expAssignments").is_none());
6510 }
6511
6512 #[test]
6513 fn resume_session_config_with_exp_assignments_serializes() {
6514 let assignments = sample_exp_assignments("ctx-456");
6515 let expected = serde_json::to_value(&assignments).unwrap();
6516 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6517 .with_exp_assignments(assignments)
6518 .into_wire()
6519 .expect("no duplicate handlers");
6520 let json = serde_json::to_value(&wire).unwrap();
6521 assert_eq!(json["expAssignments"], expected);
6522
6523 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6525 .into_wire()
6526 .expect("no duplicate handlers");
6527 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6528 assert!(empty_json.get("expAssignments").is_none());
6529 }
6530
6531 #[test]
6532 fn session_config_clone_preserves_exp_assignments() {
6533 let assignments = sample_exp_assignments("ctx-clone");
6534 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6535 let cloned = config.clone();
6536
6537 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6538
6539 let (wire, _runtime) = cloned
6540 .into_wire(Some(SessionId::from("exp-clone")))
6541 .expect("no duplicate handlers");
6542 let json = serde_json::to_value(&wire).unwrap();
6543 assert_eq!(
6544 json["expAssignments"],
6545 serde_json::to_value(&assignments).unwrap()
6546 );
6547 }
6548
6549 #[test]
6550 fn resume_session_config_clone_preserves_exp_assignments() {
6551 let assignments = sample_exp_assignments("ctx-clone-resume");
6552 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6553 .with_exp_assignments(assignments.clone());
6554 let cloned = config.clone();
6555
6556 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6557
6558 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6559 let json = serde_json::to_value(&wire).unwrap();
6560 assert_eq!(
6561 json["expAssignments"],
6562 serde_json::to_value(&assignments).unwrap()
6563 );
6564 }
6565
6566 #[test]
6567 #[allow(clippy::field_reassign_with_default)]
6568 fn session_config_into_wire_serializes_bucket_b_fields() {
6569 use std::path::PathBuf;
6570
6571 use super::{CloudSessionOptions, CloudSessionRepository};
6572
6573 let mut cfg = SessionConfig::default();
6574 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6575 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6576 cfg.github_token = Some("ghs_secret".to_string());
6577 cfg.include_sub_agent_streaming_events = Some(false);
6578 cfg.enable_session_telemetry = Some(false);
6579 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
6580 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
6581 cfg.enable_on_demand_instruction_discovery = Some(false);
6582 cfg.cloud = Some(CloudSessionOptions::with_repository(
6583 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
6584 ));
6585
6586 let (wire, _runtime) = cfg
6587 .into_wire(Some(SessionId::from("custom-id")))
6588 .expect("no duplicate handlers");
6589 let wire_json = serde_json::to_value(&wire).unwrap();
6590 assert_eq!(wire_json["sessionId"], "custom-id");
6591 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6592 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6593 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6594 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6595 assert_eq!(wire_json["enableSessionTelemetry"], false);
6596 assert_eq!(wire_json["reasoningSummary"], "concise");
6597 assert_eq!(wire_json["remoteSession"], "export");
6598 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6599 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6600 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6601 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6602
6603 let (empty_wire, _) = SessionConfig::default()
6605 .into_wire(Some(SessionId::from("empty")))
6606 .expect("default has no duplicate handlers");
6607 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6608 assert!(empty_json.get("gitHubToken").is_none());
6609 assert!(empty_json.get("enableSessionTelemetry").is_none());
6610 assert!(empty_json.get("reasoningSummary").is_none());
6611 assert!(empty_json.get("remoteSession").is_none());
6612 assert!(
6613 empty_json
6614 .get("enableOnDemandInstructionDiscovery")
6615 .is_none()
6616 );
6617 assert!(empty_json.get("cloud").is_none());
6618 }
6619
6620 #[test]
6621 fn session_config_into_wire_serializes_named_providers_and_models() {
6622 let cfg = SessionConfig::default()
6623 .with_providers(vec![
6624 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6625 .with_provider_type("openai")
6626 .with_wire_api("responses")
6627 .with_api_key("sk-test"),
6628 ])
6629 .with_models(vec![
6630 ProviderModelConfig::new("gpt-x", "my-openai")
6631 .with_wire_model("gpt-x-2025")
6632 .with_max_output_tokens(2048),
6633 ]);
6634
6635 let (wire, _) = cfg
6636 .into_wire(Some(SessionId::from("sess-providers")))
6637 .expect("no duplicate handlers");
6638 let wire_json = serde_json::to_value(&wire).unwrap();
6639 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6640 assert_eq!(
6641 wire_json["providers"][0]["baseUrl"],
6642 "https://api.example.com/v1"
6643 );
6644 assert_eq!(wire_json["providers"][0]["type"], "openai");
6645 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6646 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6647 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
6648 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
6649 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
6650 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
6651
6652 let (empty_wire, _) = SessionConfig::default()
6653 .into_wire(Some(SessionId::from("empty")))
6654 .expect("default has no duplicate handlers");
6655 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6656 assert!(empty_json.get("providers").is_none());
6657 assert!(empty_json.get("models").is_none());
6658 }
6659
6660 #[test]
6661 fn resume_config_into_wire_serializes_named_providers_and_models() {
6662 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
6663 .with_providers(vec![
6664 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
6665 .with_provider_type("azure")
6666 .with_azure(AzureProviderOptions {
6667 api_version: Some("2024-10-21".to_string()),
6668 }),
6669 ])
6670 .with_models(vec![
6671 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
6672 ]);
6673
6674 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6675 let wire_json = serde_json::to_value(&wire).unwrap();
6676 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
6677 assert_eq!(wire_json["providers"][0]["type"], "azure");
6678 assert_eq!(
6679 wire_json["providers"][0]["azure"]["apiVersion"],
6680 "2024-10-21"
6681 );
6682 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
6683 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
6684 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
6685
6686 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
6687 .into_wire()
6688 .expect("default has no duplicate handlers");
6689 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6690 assert!(empty_json.get("providers").is_none());
6691 assert!(empty_json.get("models").is_none());
6692 }
6693
6694 #[test]
6695 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
6696 use std::path::PathBuf;
6697
6698 let cfg = SessionConfig {
6699 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
6700 disabled_mcp_servers: Some(vec![
6701 "local-files".to_string(),
6702 "remote-github".to_string(),
6703 ]),
6704 large_output: Some(
6705 LargeToolOutputConfig::new()
6706 .with_enabled(true)
6707 .with_max_size_bytes(1024)
6708 .with_output_directory(PathBuf::from("/tmp/large-output")),
6709 ),
6710 ..Default::default()
6711 };
6712
6713 let (wire, _) = cfg
6714 .into_wire(Some(SessionId::from("sess-1")))
6715 .expect("no duplicate handlers");
6716 let wire_json = serde_json::to_value(&wire).unwrap();
6717 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
6718 assert_eq!(
6719 wire_json["disabledMcpServers"],
6720 serde_json::json!(["local-files", "remote-github"])
6721 );
6722 assert_eq!(wire_json["largeOutput"]["enabled"], true);
6723 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
6724 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
6725
6726 let (empty_wire, _) = SessionConfig::default()
6727 .into_wire(Some(SessionId::from("empty")))
6728 .expect("default has no duplicate handlers");
6729 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6730 assert!(empty_json.get("pluginDirectories").is_none());
6731 assert!(empty_json.get("disabledMcpServers").is_none());
6732 assert!(empty_json.get("largeOutput").is_none());
6733 }
6734
6735 #[test]
6736 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
6737 use std::path::PathBuf;
6738
6739 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6740 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6741 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6742 cfg.github_token = Some("ghs_secret".to_string());
6743 cfg.include_sub_agent_streaming_events = Some(true);
6744 cfg.enable_session_telemetry = Some(false);
6745 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
6746 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
6747 cfg.enable_on_demand_instruction_discovery = Some(false);
6748
6749 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6750 let wire_json = serde_json::to_value(&wire).unwrap();
6751 assert_eq!(wire_json["sessionId"], "sess-1");
6752 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6753 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6754 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6755 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
6756 assert_eq!(wire_json["enableSessionTelemetry"], false);
6757 assert_eq!(wire_json["reasoningSummary"], "detailed");
6758 assert_eq!(wire_json["remoteSession"], "on");
6759 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6760
6761 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6763 .into_wire()
6764 .expect("default resume has no duplicate handlers");
6765 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6766 assert!(empty_json.get("reasoningSummary").is_none());
6767 assert!(empty_json.get("remoteSession").is_none());
6768 assert!(
6769 empty_json
6770 .get("enableOnDemandInstructionDiscovery")
6771 .is_none()
6772 );
6773 }
6774
6775 #[test]
6776 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
6777 use std::path::PathBuf;
6778
6779 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6780 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6781 cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]);
6782 cfg.large_output = Some(
6783 LargeToolOutputConfig::new()
6784 .with_enabled(false)
6785 .with_max_size_bytes(2048)
6786 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6787 );
6788
6789 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6790 let wire_json = serde_json::to_value(&wire).unwrap();
6791 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6792 assert_eq!(
6793 wire_json["disabledMcpServers"],
6794 serde_json::json!(["local-files-r"])
6795 );
6796 assert_eq!(wire_json["largeOutput"]["enabled"], false);
6797 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6798 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6799
6800 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6801 .into_wire()
6802 .expect("default resume has no duplicate handlers");
6803 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6804 assert!(empty_json.get("pluginDirectories").is_none());
6805 assert!(empty_json.get("disabledMcpServers").is_none());
6806 assert!(empty_json.get("largeOutput").is_none());
6807 }
6808
6809 #[test]
6810 fn session_config_clones_disabled_mcp_servers() {
6811 let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]);
6812 let mut create_clone = create.clone();
6813 create_clone
6814 .disabled_mcp_servers
6815 .as_mut()
6816 .expect("configured disabled MCP servers")
6817 .push("remote-github".to_string());
6818 assert_eq!(
6819 create.disabled_mcp_servers.as_deref(),
6820 Some(&["local-files".to_string()][..])
6821 );
6822
6823 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6824 .with_disabled_mcp_servers(["local-files"]);
6825 let mut resume_clone = resume.clone();
6826 resume_clone
6827 .disabled_mcp_servers
6828 .as_mut()
6829 .expect("configured disabled MCP servers")
6830 .push("remote-github".to_string());
6831 assert_eq!(
6832 resume.disabled_mcp_servers.as_deref(),
6833 Some(&["local-files".to_string()][..])
6834 );
6835 }
6836
6837 #[test]
6838 fn session_config_builder_composes() {
6839 use indexmap::IndexMap;
6840
6841 let cfg = SessionConfig::default()
6842 .with_session_id(SessionId::from("sess-1"))
6843 .with_model("claude-sonnet-4")
6844 .with_client_name("test-app")
6845 .with_reasoning_effort("medium")
6846 .with_reasoning_summary(ReasoningSummary::Concise)
6847 .with_context_tier("long_context")
6848 .with_streaming(true)
6849 .with_tools([Tool::new("greet")])
6850 .with_available_tools(["bash", "view"])
6851 .with_excluded_tools(["dangerous"])
6852 .with_mcp_servers(IndexMap::new())
6853 .with_mcp_oauth_token_storage("persistent")
6854 .with_enable_config_discovery(true)
6855 .with_enable_on_demand_instruction_discovery(true)
6856 .with_skill_directories([PathBuf::from("/tmp/skills")])
6857 .with_disabled_skills(["broken-skill"])
6858 .with_disabled_mcp_servers(["local-files"])
6859 .with_agent("researcher")
6860 .with_config_directory(PathBuf::from("/tmp/config"))
6861 .with_working_directory(PathBuf::from("/tmp/work"))
6862 .with_additional_directories([PathBuf::from("/tmp/shared")])
6863 .with_github_token("ghp_test")
6864 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6865 .with_enable_session_telemetry(false)
6866 .with_include_sub_agent_streaming_events(false)
6867 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6868
6869 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
6870 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
6871 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6872 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
6873 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
6874 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
6875 assert_eq!(cfg.streaming, Some(true));
6876 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6877 assert_eq!(
6878 cfg.available_tools.as_deref(),
6879 Some(&["bash".to_string(), "view".to_string()][..])
6880 );
6881 assert_eq!(
6882 cfg.excluded_tools.as_deref(),
6883 Some(&["dangerous".to_string()][..])
6884 );
6885 assert!(cfg.mcp_servers.is_some());
6886 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6887 assert_eq!(cfg.enable_config_discovery, Some(true));
6888 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
6889 assert_eq!(
6890 cfg.skill_directories.as_deref(),
6891 Some(&[PathBuf::from("/tmp/skills")][..])
6892 );
6893 assert_eq!(
6894 cfg.disabled_skills.as_deref(),
6895 Some(&["broken-skill".to_string()][..])
6896 );
6897 assert_eq!(
6898 cfg.disabled_mcp_servers.as_deref(),
6899 Some(&["local-files".to_string()][..])
6900 );
6901 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6902 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6903 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6904 assert_eq!(
6905 cfg.additional_directories.as_deref(),
6906 Some(&[PathBuf::from("/tmp/shared")][..])
6907 );
6908 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6909 assert_eq!(
6910 cfg.capi,
6911 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6912 );
6913 assert_eq!(cfg.enable_session_telemetry, Some(false));
6914 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
6915 assert_eq!(
6916 cfg.extension_info,
6917 Some(ExtensionInfo::new("github-app", "counter"))
6918 );
6919 }
6920
6921 #[test]
6922 fn resume_session_config_builder_composes() {
6923 use indexmap::IndexMap;
6924
6925 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
6926 .with_client_name("test-app")
6927 .with_reasoning_summary(ReasoningSummary::None)
6928 .with_context_tier("default")
6929 .with_streaming(true)
6930 .with_tools([Tool::new("greet")])
6931 .with_available_tools(["bash", "view"])
6932 .with_excluded_tools(["dangerous"])
6933 .with_mcp_servers(IndexMap::new())
6934 .with_mcp_oauth_token_storage("persistent")
6935 .with_enable_config_discovery(true)
6936 .with_enable_on_demand_instruction_discovery(false)
6937 .with_skill_directories([PathBuf::from("/tmp/skills")])
6938 .with_disabled_skills(["broken-skill"])
6939 .with_disabled_mcp_servers(["local-files"])
6940 .with_agent("researcher")
6941 .with_config_directory(PathBuf::from("/tmp/config"))
6942 .with_working_directory(PathBuf::from("/tmp/work"))
6943 .with_additional_directories([PathBuf::from("/tmp/shared")])
6944 .with_github_token("ghp_test")
6945 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6946 .with_enable_session_telemetry(false)
6947 .with_include_sub_agent_streaming_events(true)
6948 .with_suppress_resume_event(true)
6949 .with_continue_pending_work(true)
6950 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6951
6952 assert_eq!(cfg.session_id.as_str(), "sess-2");
6953 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6954 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
6955 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
6956 assert_eq!(cfg.streaming, Some(true));
6957 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6958 assert_eq!(
6959 cfg.available_tools.as_deref(),
6960 Some(&["bash".to_string(), "view".to_string()][..])
6961 );
6962 assert_eq!(
6963 cfg.excluded_tools.as_deref(),
6964 Some(&["dangerous".to_string()][..])
6965 );
6966 assert!(cfg.mcp_servers.is_some());
6967 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6968 assert_eq!(cfg.enable_config_discovery, Some(true));
6969 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
6970 assert_eq!(
6971 cfg.skill_directories.as_deref(),
6972 Some(&[PathBuf::from("/tmp/skills")][..])
6973 );
6974 assert_eq!(
6975 cfg.disabled_skills.as_deref(),
6976 Some(&["broken-skill".to_string()][..])
6977 );
6978 assert_eq!(
6979 cfg.disabled_mcp_servers.as_deref(),
6980 Some(&["local-files".to_string()][..])
6981 );
6982 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6983 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6984 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6985 assert_eq!(
6986 cfg.additional_directories.as_deref(),
6987 Some(&[PathBuf::from("/tmp/shared")][..])
6988 );
6989 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6990 assert_eq!(
6991 cfg.capi,
6992 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6993 );
6994 assert_eq!(cfg.enable_session_telemetry, Some(false));
6995 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
6996 assert_eq!(cfg.suppress_resume_event, Some(true));
6997 assert_eq!(cfg.continue_pending_work, Some(true));
6998 assert_eq!(
6999 cfg.extension_info,
7000 Some(ExtensionInfo::new("github-app", "counter"))
7001 );
7002 }
7003
7004 #[test]
7008 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
7009 let cfg =
7010 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
7011 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7012 let json = serde_json::to_value(&wire).unwrap();
7013 assert_eq!(json["continuePendingWork"], true);
7014
7015 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7017 .into_wire()
7018 .expect("no duplicate handlers");
7019 let json = serde_json::to_value(&wire).unwrap();
7020 assert!(json.get("continuePendingWork").is_none());
7021 }
7022
7023 #[test]
7024 fn session_configs_serialize_additional_directories() {
7025 let create = SessionConfig::default().with_additional_directories([
7026 PathBuf::from("/tmp/shared"),
7027 PathBuf::from("/tmp/generated"),
7028 ]);
7029 let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
7030 let create_json = serde_json::to_value(&create_wire).unwrap();
7031 assert_eq!(
7032 create_json["additionalDirectories"],
7033 serde_json::json!(["/tmp/shared", "/tmp/generated"])
7034 );
7035
7036 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
7037 .with_additional_directories([PathBuf::from("/tmp/resumed")]);
7038 let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
7039 let resume_json = serde_json::to_value(&resume_wire).unwrap();
7040 assert_eq!(
7041 resume_json["additionalDirectories"],
7042 serde_json::json!(["/tmp/resumed"])
7043 );
7044 }
7045
7046 #[test]
7050 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
7051 let cfg =
7052 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
7053 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7054 let json = serde_json::to_value(&wire).unwrap();
7055 assert_eq!(json["disableResume"], true);
7056 assert!(json.get("suppressResumeEvent").is_none());
7057 }
7058
7059 #[test]
7062 fn session_config_serializes_instruction_directories_to_camel_case() {
7063 let cfg =
7064 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
7065 let (wire, _) = cfg
7066 .into_wire(Some(SessionId::from("instr-on")))
7067 .expect("no duplicate handlers");
7068 let json = serde_json::to_value(&wire).unwrap();
7069 assert_eq!(
7070 json["instructionDirectories"],
7071 serde_json::json!(["/tmp/instr"])
7072 );
7073
7074 let (wire, _) = SessionConfig::default()
7076 .into_wire(Some(SessionId::from("instr-off")))
7077 .expect("no duplicate handlers");
7078 let json = serde_json::to_value(&wire).unwrap();
7079 assert!(json.get("instructionDirectories").is_none());
7080 }
7081
7082 #[test]
7085 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
7086 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
7087 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
7088 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7089 let json = serde_json::to_value(&wire).unwrap();
7090 assert_eq!(
7091 json["instructionDirectories"],
7092 serde_json::json!(["/tmp/instr"])
7093 );
7094
7095 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7096 .into_wire()
7097 .expect("no duplicate handlers");
7098 let json = serde_json::to_value(&wire).unwrap();
7099 assert!(json.get("instructionDirectories").is_none());
7100 }
7101
7102 #[test]
7103 fn custom_agent_config_builder_composes() {
7104 use indexmap::IndexMap;
7105
7106 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
7107 .with_display_name("Research Assistant")
7108 .with_description("Investigates technical questions.")
7109 .with_tools(["bash", "view"])
7110 .with_mcp_servers(IndexMap::new())
7111 .with_infer(true)
7112 .with_skills(["rust-coding-skill"]);
7113
7114 assert_eq!(cfg.name, "researcher");
7115 assert_eq!(cfg.prompt, "You are a research assistant.");
7116 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
7117 assert_eq!(
7118 cfg.description.as_deref(),
7119 Some("Investigates technical questions.")
7120 );
7121 assert_eq!(
7122 cfg.tools.as_deref(),
7123 Some(&["bash".to_string(), "view".to_string()][..])
7124 );
7125 assert!(cfg.mcp_servers.is_some());
7126 assert_eq!(cfg.infer, Some(true));
7127 assert_eq!(
7128 cfg.skills.as_deref(),
7129 Some(&["rust-coding-skill".to_string()][..])
7130 );
7131 }
7132
7133 #[test]
7134 fn mcp_servers_serialize_in_insertion_order() {
7135 use indexmap::IndexMap;
7136
7137 let order = [
7143 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
7144 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
7145 ];
7146 let mut servers = IndexMap::new();
7147 for name in order {
7148 servers.insert(
7149 name.to_string(),
7150 McpServerConfig::Stdio(McpStdioServerConfig {
7151 command: "run".to_string(),
7152 ..Default::default()
7153 }),
7154 );
7155 }
7156
7157 let (wire, _runtime) = SessionConfig::default()
7158 .with_mcp_servers(servers)
7159 .into_wire(None)
7160 .expect("into_wire should succeed");
7161 let json = serde_json::to_string(&wire).expect("serialize wire");
7162
7163 let positions: Vec<usize> = order
7164 .iter()
7165 .map(|name| {
7166 json.find(&format!("\"{name}\""))
7167 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
7168 })
7169 .collect();
7170 let mut ascending = positions.clone();
7171 ascending.sort_unstable();
7172 assert_eq!(
7173 positions, ascending,
7174 "mcp server keys must serialize in insertion order: {json}"
7175 );
7176 }
7177
7178 #[test]
7179 fn infinite_session_config_builder_composes() {
7180 let cfg = InfiniteSessionConfig::new()
7181 .with_enabled(true)
7182 .with_background_compaction_threshold(0.75)
7183 .with_buffer_exhaustion_threshold(0.92);
7184
7185 assert_eq!(cfg.enabled, Some(true));
7186 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
7187 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
7188 }
7189
7190 #[test]
7191 fn provider_config_builder_composes() {
7192 use std::collections::HashMap;
7193
7194 let mut headers = HashMap::new();
7195 headers.insert("X-Custom".to_string(), "value".to_string());
7196
7197 let cfg = ProviderConfig::new("https://api.example.com")
7198 .with_provider_type("openai")
7199 .with_wire_api("completions")
7200 .with_transport("websockets")
7201 .with_api_key("sk-test")
7202 .with_bearer_token("bearer-test")
7203 .with_headers(headers)
7204 .with_model_id("gpt-4")
7205 .with_wire_model("azure-gpt-4-deployment")
7206 .with_max_prompt_tokens(8192)
7207 .with_max_output_tokens(2048);
7208
7209 assert_eq!(cfg.base_url, "https://api.example.com");
7210 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
7211 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
7212 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
7213 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
7214 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
7215 assert_eq!(
7216 cfg.headers
7217 .as_ref()
7218 .and_then(|h| h.get("X-Custom"))
7219 .map(String::as_str),
7220 Some("value"),
7221 );
7222 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
7223 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
7224 assert_eq!(cfg.max_prompt_tokens, Some(8192));
7225 assert_eq!(cfg.max_output_tokens, Some(2048));
7226
7227 let wire = serde_json::to_value(&cfg).unwrap();
7229 assert_eq!(wire["modelId"], "gpt-4");
7230 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
7231 assert_eq!(wire["maxPromptTokens"], 8192);
7232 assert_eq!(wire["maxOutputTokens"], 2048);
7233
7234 let unset = ProviderConfig::new("https://api.example.com");
7235 let wire_unset = serde_json::to_value(&unset).unwrap();
7236 assert!(wire_unset.get("modelId").is_none());
7237 assert!(wire_unset.get("wireModel").is_none());
7238 assert!(wire_unset.get("maxPromptTokens").is_none());
7239 assert!(wire_unset.get("maxOutputTokens").is_none());
7240 }
7241
7242 #[test]
7243 fn capi_session_options_builder_composes_and_serializes() {
7244 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
7245
7246 assert_eq!(cfg.enable_web_socket_responses, Some(false));
7247
7248 let wire = serde_json::to_value(&cfg).unwrap();
7249 assert_eq!(
7250 wire,
7251 serde_json::json!({ "enableWebSocketResponses": false })
7252 );
7253
7254 let unset = CapiSessionOptions::new();
7255 let wire_unset = serde_json::to_value(&unset).unwrap();
7256 assert!(wire_unset.get("enableWebSocketResponses").is_none());
7257 }
7258
7259 #[test]
7260 fn session_config_with_capi_serializes() {
7261 let (wire, _) = SessionConfig::default()
7262 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7263 .into_wire(Some(SessionId::from("capi-create")))
7264 .expect("no duplicate handlers");
7265 let json = serde_json::to_value(&wire).unwrap();
7266 assert_eq!(
7267 json["capi"],
7268 serde_json::json!({ "enableWebSocketResponses": false })
7269 );
7270
7271 let (empty_wire, _) = SessionConfig::default()
7272 .into_wire(Some(SessionId::from("capi-create-unset")))
7273 .expect("no duplicate handlers");
7274 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7275 assert!(empty_json.get("capi").is_none());
7276 }
7277
7278 #[test]
7279 fn resume_session_config_with_capi_serializes() {
7280 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7281 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7282 .into_wire()
7283 .expect("no duplicate handlers");
7284 let json = serde_json::to_value(&wire).unwrap();
7285 assert_eq!(
7286 json["capi"],
7287 serde_json::json!({ "enableWebSocketResponses": false })
7288 );
7289
7290 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
7291 .into_wire()
7292 .expect("no duplicate handlers");
7293 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7294 assert!(empty_json.get("capi").is_none());
7295 }
7296
7297 #[test]
7298 fn system_message_config_builder_composes() {
7299 use std::collections::HashMap;
7300
7301 let cfg = SystemMessageConfig::new()
7302 .with_mode("replace")
7303 .with_content("Custom system message.")
7304 .with_sections(HashMap::new());
7305
7306 assert_eq!(cfg.mode.as_deref(), Some("replace"));
7307 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
7308 assert!(cfg.sections.is_some());
7309 }
7310
7311 #[test]
7312 fn delivery_mode_serializes_to_kebab_case_strings() {
7313 assert_eq!(
7314 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
7315 "\"enqueue\""
7316 );
7317 assert_eq!(
7318 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
7319 "\"immediate\""
7320 );
7321 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
7322 assert_eq!(parsed, DeliveryMode::Immediate);
7323 }
7324
7325 #[test]
7326 fn agent_mode_serializes_to_kebab_case_strings() {
7327 assert_eq!(
7328 serde_json::to_string(&AgentMode::Interactive).unwrap(),
7329 "\"interactive\""
7330 );
7331 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
7332 assert_eq!(
7333 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
7334 "\"autopilot\""
7335 );
7336 assert_eq!(
7337 serde_json::to_string(&AgentMode::Shell).unwrap(),
7338 "\"shell\""
7339 );
7340 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
7341 assert_eq!(parsed, AgentMode::Plan);
7342 }
7343
7344 #[test]
7345 fn connection_state_distinguishes_variants() {
7346 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
7349 }
7350
7351 #[test]
7357 fn session_event_round_trips_agent_id_on_envelope() {
7358 let wire = json!({
7359 "id": "evt-1",
7360 "timestamp": "2026-04-30T12:00:00Z",
7361 "parentId": null,
7362 "agentId": "sub-agent-42",
7363 "type": "assistant.message",
7364 "data": { "message": "hi" }
7365 });
7366
7367 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
7368 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7369
7370 let roundtripped = serde_json::to_value(&event).unwrap();
7372 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7373
7374 let main_agent_event: SessionEvent = serde_json::from_value(json!({
7376 "id": "evt-2",
7377 "timestamp": "2026-04-30T12:00:01Z",
7378 "parentId": null,
7379 "type": "session.idle",
7380 "data": {}
7381 }))
7382 .unwrap();
7383 assert!(main_agent_event.agent_id.is_none());
7384 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
7385 assert!(roundtripped.get("agentId").is_none());
7386 }
7387
7388 #[test]
7390 fn typed_session_event_round_trips_agent_id_on_envelope() {
7391 let wire = json!({
7392 "id": "evt-1",
7393 "timestamp": "2026-04-30T12:00:00Z",
7394 "parentId": null,
7395 "agentId": "sub-agent-42",
7396 "type": "session.idle",
7397 "data": {}
7398 });
7399
7400 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
7401 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7402
7403 let roundtripped = serde_json::to_value(&event).unwrap();
7404 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7405 }
7406
7407 #[test]
7408 fn connection_state_variants_compile() {
7409 let _ = ConnectionState::Disconnected;
7413 let _ = ConnectionState::Connecting;
7414 let _ = ConnectionState::Connected;
7415 let _ = ConnectionState::Error;
7416 }
7417
7418 #[test]
7419 fn deserializes_runtime_attachment_variants() {
7420 let attachments: Vec<Attachment> = serde_json::from_value(json!([
7421 {
7422 "type": "file",
7423 "path": "/tmp/file.rs",
7424 "displayName": "file.rs",
7425 "lineRange": { "start": 7, "end": 12 }
7426 },
7427 {
7428 "type": "directory",
7429 "path": "/tmp/project",
7430 "displayName": "project"
7431 },
7432 {
7433 "type": "selection",
7434 "filePath": "/tmp/lib.rs",
7435 "displayName": "lib.rs",
7436 "text": "fn main() {}",
7437 "selection": {
7438 "start": { "line": 1, "character": 2 },
7439 "end": { "line": 3, "character": 4 }
7440 }
7441 },
7442 {
7443 "type": "blob",
7444 "data": "Zm9v",
7445 "mimeType": "image/png",
7446 "displayName": "image.png"
7447 },
7448 {
7449 "type": "github_reference",
7450 "number": 42,
7451 "title": "Fix rendering",
7452 "referenceType": "issue",
7453 "state": "open",
7454 "url": "https://github.com/example/repo/issues/42"
7455 }
7456 ]))
7457 .expect("attachments should deserialize");
7458
7459 assert_eq!(attachments.len(), 5);
7460 assert!(matches!(
7461 &attachments[0],
7462 Attachment::File {
7463 path,
7464 display_name,
7465 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7466 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7467 ));
7468 assert!(matches!(
7469 &attachments[1],
7470 Attachment::Directory { path, display_name }
7471 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7472 ));
7473 assert!(matches!(
7474 &attachments[2],
7475 Attachment::Selection {
7476 file_path,
7477 display_name,
7478 selection:
7479 AttachmentSelectionRange {
7480 start: AttachmentSelectionPosition { line: 1, character: 2 },
7481 end: AttachmentSelectionPosition { line: 3, character: 4 },
7482 },
7483 ..
7484 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
7485 ));
7486 assert!(matches!(
7487 &attachments[3],
7488 Attachment::Blob {
7489 data,
7490 mime_type,
7491 display_name,
7492 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
7493 ));
7494 assert!(matches!(
7495 &attachments[4],
7496 Attachment::GitHubReference {
7497 number: 42,
7498 title,
7499 reference_type: GitHubReferenceType::Issue,
7500 state,
7501 url,
7502 } if title == "Fix rendering"
7503 && state == "open"
7504 && url == "https://github.com/example/repo/issues/42"
7505 ));
7506 }
7507
7508 #[test]
7509 fn ensures_display_names_for_variants_that_support_them() {
7510 let mut attachments = vec![
7511 Attachment::File {
7512 path: PathBuf::from("/tmp/file.rs"),
7513 display_name: None,
7514 line_range: None,
7515 },
7516 Attachment::Selection {
7517 file_path: PathBuf::from("/tmp/src/lib.rs"),
7518 display_name: None,
7519 text: "fn main() {}".to_string(),
7520 selection: AttachmentSelectionRange {
7521 start: AttachmentSelectionPosition {
7522 line: 0,
7523 character: 0,
7524 },
7525 end: AttachmentSelectionPosition {
7526 line: 0,
7527 character: 10,
7528 },
7529 },
7530 },
7531 Attachment::Blob {
7532 data: "Zm9v".to_string(),
7533 mime_type: "image/png".to_string(),
7534 display_name: None,
7535 },
7536 Attachment::GitHubReference {
7537 number: 7,
7538 title: "Track regressions".to_string(),
7539 reference_type: GitHubReferenceType::Issue,
7540 state: "open".to_string(),
7541 url: "https://example.com/issues/7".to_string(),
7542 },
7543 ];
7544
7545 ensure_attachment_display_names(&mut attachments);
7546
7547 assert_eq!(attachments[0].display_name(), Some("file.rs"));
7548 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
7549 assert_eq!(attachments[2].display_name(), Some("attachment"));
7550 assert_eq!(attachments[3].display_name(), None);
7551 assert_eq!(
7552 attachments[3].label(),
7553 Some("Track regressions".to_string())
7554 );
7555 }
7556
7557 #[test]
7558 fn github_anchored_attachment_variants_round_trip() {
7559 let cases = vec![
7560 (
7561 "github_commit",
7562 json!({
7563 "type": "github_commit",
7564 "message": "Fix the thing",
7565 "oid": "abc123",
7566 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
7567 "url": "https://github.com/octocat/repo/commit/abc123"
7568 }),
7569 ),
7570 (
7571 "github_release",
7572 json!({
7573 "type": "github_release",
7574 "name": "v1.2.3",
7575 "repo": { "name": "repo", "owner": "octocat" },
7576 "tagName": "v1.2.3",
7577 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
7578 }),
7579 ),
7580 (
7581 "github_actions_job",
7582 json!({
7583 "type": "github_actions_job",
7584 "conclusion": "failure",
7585 "jobId": 99,
7586 "jobName": "build",
7587 "repo": { "name": "repo", "owner": "octocat" },
7588 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
7589 "workflowName": "CI"
7590 }),
7591 ),
7592 (
7593 "github_repository",
7594 json!({
7595 "type": "github_repository",
7596 "description": "An example repository",
7597 "ref": "main",
7598 "repo": { "name": "repo", "owner": "octocat" },
7599 "url": "https://github.com/octocat/repo"
7600 }),
7601 ),
7602 (
7603 "github_file_diff",
7604 json!({
7605 "type": "github_file_diff",
7606 "base": {
7607 "path": "src/lib.rs",
7608 "ref": "main",
7609 "repo": { "name": "repo", "owner": "octocat" }
7610 },
7611 "head": {
7612 "path": "src/lib.rs",
7613 "ref": "feature",
7614 "repo": { "name": "repo", "owner": "octocat" }
7615 },
7616 "url": "https://github.com/octocat/repo/compare/main...feature"
7617 }),
7618 ),
7619 (
7620 "github_tree_comparison",
7621 json!({
7622 "type": "github_tree_comparison",
7623 "base": {
7624 "repo": { "name": "repo", "owner": "octocat" },
7625 "revision": "main"
7626 },
7627 "head": {
7628 "repo": { "name": "repo", "owner": "octocat" },
7629 "revision": "feature"
7630 },
7631 "url": "https://github.com/octocat/repo/compare/main...feature"
7632 }),
7633 ),
7634 (
7635 "github_url",
7636 json!({
7637 "type": "github_url",
7638 "url": "https://github.com/octocat/repo/wiki"
7639 }),
7640 ),
7641 (
7642 "github_file",
7643 json!({
7644 "type": "github_file",
7645 "path": "src/main.rs",
7646 "ref": "main",
7647 "repo": { "name": "repo", "owner": "octocat" },
7648 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
7649 }),
7650 ),
7651 (
7652 "github_snippet",
7653 json!({
7654 "type": "github_snippet",
7655 "lineRange": { "start": 10, "end": 20 },
7656 "path": "src/main.rs",
7657 "ref": "main",
7658 "repo": { "name": "repo", "owner": "octocat" },
7659 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
7660 }),
7661 ),
7662 ];
7663
7664 for (expected_type, input) in cases {
7665 let attachment: Attachment = serde_json::from_value(input.clone())
7666 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
7667
7668 let serialized_string = serde_json::to_string(&attachment)
7673 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
7674
7675 assert_eq!(
7677 serialized_string.matches("\"type\":").count(),
7678 1,
7679 "{expected_type} must serialize a single `type` key"
7680 );
7681
7682 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
7683 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
7684 assert_eq!(
7685 serialized.get("type").and_then(|value| value.as_str()),
7686 Some(expected_type),
7687 "{expected_type} must serialize the correct discriminator"
7688 );
7689
7690 assert_eq!(
7692 serialized, input,
7693 "{expected_type} should round-trip without data loss"
7694 );
7695 let reparsed: Attachment = serde_json::from_value(serialized)
7696 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
7697 assert_eq!(
7698 reparsed, attachment,
7699 "{expected_type} should re-deserialize to the same value"
7700 );
7701 }
7702 }
7703}
7704
7705#[cfg(test)]
7706mod permission_builder_tests {
7707 use std::sync::Arc;
7708
7709 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
7710 use crate::permission;
7711 use crate::types::{
7712 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
7713 SessionId,
7714 };
7715
7716 fn data() -> PermissionRequestData {
7717 PermissionRequestData {
7718 extra: serde_json::json!({"tool": "shell"}),
7719 ..Default::default()
7720 }
7721 }
7722
7723 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7726 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7727 }
7728
7729 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7730 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7731 }
7732
7733 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
7734 handler
7735 .handle(SessionId::from("s1"), RequestId::new("1"), data())
7736 .await
7737 }
7738
7739 #[tokio::test]
7740 async fn approve_all_with_handler_present_approves() {
7741 let cfg = SessionConfig::default()
7742 .with_permission_handler(Arc::new(ApproveAllHandler))
7743 .approve_all_permissions();
7744 let h = resolve_create(cfg).expect("policy + handler yields handler");
7745 assert!(matches!(
7746 dispatch(&h).await,
7747 PermissionResult::Decision {
7748 decision: PermissionDecision::ApproveOnce(_),
7749 ..
7750 }
7751 ));
7752 }
7753
7754 #[tokio::test]
7755 async fn approve_all_standalone_produces_handler() {
7756 let cfg = SessionConfig::default().approve_all_permissions();
7757 let h = resolve_create(cfg).expect("policy alone yields handler");
7758 assert!(matches!(
7759 dispatch(&h).await,
7760 PermissionResult::Decision {
7761 decision: PermissionDecision::ApproveOnce(_),
7762 ..
7763 }
7764 ));
7765 }
7766
7767 #[tokio::test]
7770 async fn approve_all_is_order_independent() {
7771 let a = SessionConfig::default()
7772 .with_permission_handler(Arc::new(ApproveAllHandler))
7773 .approve_all_permissions();
7774 let b = SessionConfig::default()
7775 .approve_all_permissions()
7776 .with_permission_handler(Arc::new(ApproveAllHandler));
7777 let ha = resolve_create(a).unwrap();
7778 let hb = resolve_create(b).unwrap();
7779 assert!(matches!(
7780 dispatch(&ha).await,
7781 PermissionResult::Decision {
7782 decision: PermissionDecision::ApproveOnce(_),
7783 ..
7784 }
7785 ));
7786 assert!(matches!(
7787 dispatch(&hb).await,
7788 PermissionResult::Decision {
7789 decision: PermissionDecision::ApproveOnce(_),
7790 ..
7791 }
7792 ));
7793 }
7794
7795 #[tokio::test]
7796 async fn deny_all_is_order_independent() {
7797 let a = SessionConfig::default()
7798 .with_permission_handler(Arc::new(ApproveAllHandler))
7799 .deny_all_permissions();
7800 let b = SessionConfig::default()
7801 .deny_all_permissions()
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 approve_permissions_if_consults_predicate() {
7823 let cfg = SessionConfig::default().approve_permissions_if(|d| {
7824 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7825 });
7826 let h = resolve_create(cfg).unwrap();
7827 assert!(matches!(
7828 dispatch(&h).await,
7829 PermissionResult::Decision {
7830 decision: PermissionDecision::Reject(_),
7831 ..
7832 }
7833 ));
7834 }
7835
7836 #[tokio::test]
7837 async fn approve_permissions_if_is_order_independent() {
7838 let predicate = |d: &PermissionRequestData| {
7839 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7840 };
7841 let a = SessionConfig::default()
7842 .with_permission_handler(Arc::new(ApproveAllHandler))
7843 .approve_permissions_if(predicate);
7844 let b = SessionConfig::default()
7845 .approve_permissions_if(predicate)
7846 .with_permission_handler(Arc::new(ApproveAllHandler));
7847 let ha = resolve_create(a).unwrap();
7848 let hb = resolve_create(b).unwrap();
7849 assert!(matches!(
7850 dispatch(&ha).await,
7851 PermissionResult::Decision {
7852 decision: PermissionDecision::Reject(_),
7853 ..
7854 }
7855 ));
7856 assert!(matches!(
7857 dispatch(&hb).await,
7858 PermissionResult::Decision {
7859 decision: PermissionDecision::Reject(_),
7860 ..
7861 }
7862 ));
7863 }
7864
7865 #[tokio::test]
7866 async fn resume_session_config_approve_all_works() {
7867 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
7868 .with_permission_handler(Arc::new(ApproveAllHandler))
7869 .approve_all_permissions();
7870 let h = resolve_resume(cfg).unwrap();
7871 assert!(matches!(
7872 dispatch(&h).await,
7873 PermissionResult::Decision {
7874 decision: PermissionDecision::ApproveOnce(_),
7875 ..
7876 }
7877 ));
7878 }
7879
7880 #[tokio::test]
7881 async fn resume_session_config_approve_all_is_order_independent() {
7882 let a = ResumeSessionConfig::new(SessionId::from("s1"))
7883 .with_permission_handler(Arc::new(ApproveAllHandler))
7884 .approve_all_permissions();
7885 let b = ResumeSessionConfig::new(SessionId::from("s1"))
7886 .approve_all_permissions()
7887 .with_permission_handler(Arc::new(ApproveAllHandler));
7888 let ha = resolve_resume(a).unwrap();
7889 let hb = resolve_resume(b).unwrap();
7890 assert!(matches!(
7891 dispatch(&ha).await,
7892 PermissionResult::Decision {
7893 decision: PermissionDecision::ApproveOnce(_),
7894 ..
7895 }
7896 ));
7897 assert!(matches!(
7898 dispatch(&hb).await,
7899 PermissionResult::Decision {
7900 decision: PermissionDecision::ApproveOnce(_),
7901 ..
7902 }
7903 ));
7904 }
7905
7906 #[test]
7907 fn session_config_enable_experimental_mode_serializes_when_set() {
7908 let cfg = SessionConfig::default().with_enable_experimental_mode(false);
7909 assert_eq!(cfg.enable_experimental_mode, Some(false));
7910
7911 let (wire, _runtime) = cfg
7912 .into_wire(Some(SessionId::from("experimental-mode")))
7913 .expect("enable_experimental_mode config has no duplicate handlers");
7914 assert_eq!(wire.is_experimental_mode, Some(false));
7915
7916 let json = serde_json::to_value(&wire).unwrap();
7917 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7918 }
7919
7920 #[test]
7921 fn session_config_enable_experimental_mode_omitted_when_none() {
7922 let cfg = SessionConfig::default();
7923 assert_eq!(cfg.enable_experimental_mode, None);
7924
7925 let (wire, _runtime) = cfg
7926 .into_wire(Some(SessionId::from("no-experimental-mode")))
7927 .expect("default config has no duplicate handlers");
7928 assert_eq!(wire.is_experimental_mode, None);
7929
7930 let json = serde_json::to_value(&wire).unwrap();
7931 assert!(json.get("isExperimentalMode").is_none());
7932 }
7933
7934 #[test]
7935 fn resume_session_config_enable_experimental_mode_serializes_when_set() {
7936 let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
7937 .with_enable_experimental_mode(false);
7938 assert_eq!(cfg.enable_experimental_mode, Some(false));
7939
7940 let (wire, _runtime) = cfg
7941 .into_wire()
7942 .expect("resume enable_experimental_mode config has no duplicate handlers");
7943 assert_eq!(wire.is_experimental_mode, Some(false));
7944
7945 let json = serde_json::to_value(&wire).unwrap();
7946 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7947 }
7948
7949 #[test]
7950 fn resume_session_config_enable_experimental_mode_omitted_when_none() {
7951 let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
7952 assert_eq!(cfg.enable_experimental_mode, None);
7953
7954 let (wire, _runtime) = cfg
7955 .into_wire()
7956 .expect("default resume config has no duplicate handlers");
7957 assert_eq!(wire.is_experimental_mode, None);
7958
7959 let json = serde_json::to_value(&wire).unwrap();
7960 assert!(json.get("isExperimentalMode").is_none());
7961 }
7962}
7963
7964#[cfg(test)]
7965mod is_terminal_tests {
7966 use super::Tool;
7967
7968 #[test]
7969 fn is_terminal_serializes_as_camel_case_when_set() {
7970 let tool = Tool {
7971 name: "clear_context".to_owned(),
7972 is_terminal: true,
7973 ..Default::default()
7974 };
7975 let value = serde_json::to_value(&tool).expect("tool serializes");
7976 assert_eq!(
7977 value.get("isTerminal"),
7978 Some(&serde_json::Value::Bool(true))
7979 );
7980 }
7981
7982 #[test]
7983 fn is_terminal_is_omitted_when_false() {
7984 let tool = Tool {
7985 name: "plain".to_owned(),
7986 ..Default::default()
7987 };
7988 let value = serde_json::to_value(&tool).expect("tool serializes");
7989 assert!(value.get("isTerminal").is_none());
7990 }
7991
7992 #[test]
7995 fn is_terminal_appears_in_debug_output() {
7996 let terminal = Tool {
7997 name: "clear_context".to_owned(),
7998 is_terminal: true,
7999 ..Default::default()
8000 };
8001 assert!(format!("{terminal:?}").contains("is_terminal: true"));
8002
8003 let plain = Tool {
8004 name: "plain".to_owned(),
8005 ..Default::default()
8006 };
8007 assert!(format!("{plain:?}").contains("is_terminal: false"));
8008 }
8009}