1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11
12use indexmap::IndexMap;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16use crate::canvas::{CanvasDeclaration, CanvasHandler};
17pub use crate::copilot_request_handler::{
18 CopilotHttpRequest, CopilotHttpResponse, CopilotHttpResponseBody, CopilotRequestContext,
19 CopilotRequestError, CopilotRequestHandler, CopilotRequestTransport, CopilotWebSocketForwarder,
20 CopilotWebSocketForwarderBuilder, CopilotWebSocketHandler, CopilotWebSocketMessage,
21 CopilotWebSocketResponse, WebSocketTransform, forward_http,
22};
23use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance};
24use crate::generated::session_events::ReasoningSummary;
25pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig};
27use crate::handler::{
28 AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler,
29 PermissionHandler, UserInputHandler,
30};
31use crate::hooks::SessionHooks;
32use crate::provider_token::BearerTokenProvider;
33pub use crate::session_fs::{
34 DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig,
35 SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult,
36 SessionFsSqliteQueryType, SessionFsSqliteTransactionError,
37 SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionStatement,
38};
39pub use crate::trace_context::{TraceContext, TraceContextProvider};
40use crate::transforms::SystemMessageTransform;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45#[allow(dead_code)]
46#[non_exhaustive]
47pub(crate) enum ConnectionState {
48 Disconnected,
50 Connecting,
52 Connected,
54 Error,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
63#[non_exhaustive]
64pub enum SessionLifecycleEventType {
65 #[serde(rename = "session.created")]
67 Created,
68 #[serde(rename = "session.deleted")]
70 Deleted,
71 #[serde(rename = "session.updated")]
73 Updated,
74 #[serde(rename = "session.foreground")]
76 Foreground,
77 #[serde(rename = "session.background")]
79 Background,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct SessionLifecycleEventMetadata {
85 #[serde(rename = "startTime")]
87 pub start_time: String,
88 #[serde(rename = "modifiedTime")]
90 pub modified_time: String,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub summary: Option<String>,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct SessionLifecycleEvent {
100 #[serde(rename = "type")]
102 pub event_type: SessionLifecycleEventType,
103 #[serde(rename = "sessionId")]
105 pub session_id: SessionId,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub metadata: Option<SessionLifecycleEventMetadata>,
109}
110
111#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
117#[serde(transparent)]
118pub struct SessionId(String);
119
120impl SessionId {
121 pub fn new(id: impl Into<String>) -> Self {
123 Self(id.into())
124 }
125
126 pub fn as_str(&self) -> &str {
128 &self.0
129 }
130
131 pub fn into_inner(self) -> String {
133 self.0
134 }
135}
136
137impl std::ops::Deref for SessionId {
138 type Target = str;
139
140 fn deref(&self) -> &str {
141 &self.0
142 }
143}
144
145impl std::fmt::Display for SessionId {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 f.write_str(&self.0)
148 }
149}
150
151impl From<String> for SessionId {
152 fn from(s: String) -> Self {
153 Self(s)
154 }
155}
156
157impl From<&str> for SessionId {
158 fn from(s: &str) -> Self {
159 Self(s.to_owned())
160 }
161}
162
163impl AsRef<str> for SessionId {
164 fn as_ref(&self) -> &str {
165 &self.0
166 }
167}
168
169impl std::borrow::Borrow<str> for SessionId {
170 fn borrow(&self) -> &str {
171 &self.0
172 }
173}
174
175impl From<SessionId> for String {
176 fn from(id: SessionId) -> String {
177 id.0
178 }
179}
180
181impl PartialEq<str> for SessionId {
182 fn eq(&self, other: &str) -> bool {
183 self.0 == other
184 }
185}
186
187impl PartialEq<String> for SessionId {
188 fn eq(&self, other: &String) -> bool {
189 &self.0 == other
190 }
191}
192
193impl PartialEq<SessionId> for String {
194 fn eq(&self, other: &SessionId) -> bool {
195 self == &other.0
196 }
197}
198
199impl PartialEq<&str> for SessionId {
200 fn eq(&self, other: &&str) -> bool {
201 self.0 == *other
202 }
203}
204
205impl PartialEq<&SessionId> for SessionId {
206 fn eq(&self, other: &&SessionId) -> bool {
207 self.0 == other.0
208 }
209}
210
211impl PartialEq<SessionId> for &SessionId {
212 fn eq(&self, other: &SessionId) -> bool {
213 self.0 == other.0
214 }
215}
216
217#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
223#[serde(transparent)]
224pub struct RequestId(String);
225
226impl RequestId {
227 pub fn new(id: impl Into<String>) -> Self {
229 Self(id.into())
230 }
231
232 pub fn into_inner(self) -> String {
234 self.0
235 }
236}
237
238impl std::ops::Deref for RequestId {
239 type Target = str;
240
241 fn deref(&self) -> &str {
242 &self.0
243 }
244}
245
246impl std::fmt::Display for RequestId {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 f.write_str(&self.0)
249 }
250}
251
252impl From<String> for RequestId {
253 fn from(s: String) -> Self {
254 Self(s)
255 }
256}
257
258impl From<&str> for RequestId {
259 fn from(s: &str) -> Self {
260 Self(s.to_owned())
261 }
262}
263
264impl AsRef<str> for RequestId {
265 fn as_ref(&self) -> &str {
266 &self.0
267 }
268}
269
270impl std::borrow::Borrow<str> for RequestId {
271 fn borrow(&self) -> &str {
272 &self.0
273 }
274}
275
276impl From<RequestId> for String {
277 fn from(id: RequestId) -> String {
278 id.0
279 }
280}
281
282impl PartialEq<str> for RequestId {
283 fn eq(&self, other: &str) -> bool {
284 self.0 == other
285 }
286}
287
288impl PartialEq<String> for RequestId {
289 fn eq(&self, other: &String) -> bool {
290 &self.0 == other
291 }
292}
293
294impl PartialEq<RequestId> for String {
295 fn eq(&self, other: &RequestId) -> bool {
296 self == &other.0
297 }
298}
299
300impl PartialEq<&str> for RequestId {
301 fn eq(&self, other: &&str) -> bool {
302 self.0 == *other
303 }
304}
305
306#[derive(Clone, Default, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322#[non_exhaustive]
323pub struct Tool {
324 pub name: String,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub namespaced_name: Option<String>,
330 #[serde(default)]
332 pub description: String,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub instructions: Option<String>,
336 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
338 pub parameters: IndexMap<String, Value>,
339 #[serde(default, skip_serializing_if = "is_false")]
343 pub overrides_built_in_tool: bool,
344 #[serde(default, skip_serializing_if = "is_false")]
348 pub skip_permission: bool,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pub defer: Option<DeferMode>,
356 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
361 pub metadata: IndexMap<String, Value>,
362 #[serde(skip)]
374 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
375}
376
377#[inline]
378fn is_false(b: &bool) -> bool {
379 !*b
380}
381
382#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
385#[serde(rename_all = "lowercase")]
386pub enum DeferMode {
387 Auto,
389 Never,
391}
392
393impl Tool {
394 pub fn new(name: impl Into<String>) -> Self {
414 Self {
415 name: name.into(),
416 ..Default::default()
417 }
418 }
419
420 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
423 self.namespaced_name = Some(namespaced_name.into());
424 self
425 }
426
427 pub fn with_description(mut self, description: impl Into<String>) -> Self {
429 self.description = description.into();
430 self
431 }
432
433 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
435 self.instructions = Some(instructions.into());
436 self
437 }
438
439 pub fn with_parameters(mut self, parameters: Value) -> Self {
453 self.parameters = crate::tool::tool_parameters(parameters);
454 self
455 }
456
457 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
461 self.overrides_built_in_tool = overrides;
462 self
463 }
464
465 pub fn with_skip_permission(mut self, skip: bool) -> Self {
469 self.skip_permission = skip;
470 self
471 }
472
473 pub fn with_defer(mut self, defer: DeferMode) -> Self {
477 self.defer = Some(defer);
478 self
479 }
480
481 pub fn with_metadata(mut self, metadata: IndexMap<String, Value>) -> Self {
484 self.metadata = metadata;
485 self
486 }
487
488 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
492 self.handler = Some(handler);
493 self
494 }
495
496 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
501 self.handler.as_ref()
502 }
503}
504
505impl std::fmt::Debug for Tool {
506 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
507 f.debug_struct("Tool")
508 .field("name", &self.name)
509 .field("namespaced_name", &self.namespaced_name)
510 .field("description", &self.description)
511 .field("instructions", &self.instructions)
512 .field("parameters", &self.parameters)
513 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
514 .field("skip_permission", &self.skip_permission)
515 .field("defer", &self.defer)
516 .field("metadata", &self.metadata)
517 .field(
518 "handler",
519 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
520 )
521 .finish()
522 }
523}
524
525#[non_exhaustive]
528#[derive(Debug, Clone)]
529pub struct CommandContext {
530 pub session_id: SessionId,
532 pub command: String,
534 pub command_name: String,
536 pub args: String,
538}
539
540#[async_trait::async_trait]
546pub trait CommandHandler: Send + Sync {
547 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
549}
550
551#[non_exhaustive]
557#[derive(Clone)]
558pub struct CommandDefinition {
559 pub name: String,
561 pub description: Option<String>,
563 pub handler: Arc<dyn CommandHandler>,
565}
566
567impl CommandDefinition {
568 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
571 Self {
572 name: name.into(),
573 description: None,
574 handler,
575 }
576 }
577
578 pub fn with_description(mut self, description: impl Into<String>) -> Self {
580 self.description = Some(description.into());
581 self
582 }
583}
584
585impl std::fmt::Debug for CommandDefinition {
586 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
587 f.debug_struct("CommandDefinition")
588 .field("name", &self.name)
589 .field("description", &self.description)
590 .field("handler", &"<set>")
591 .finish()
592 }
593}
594
595impl Serialize for CommandDefinition {
596 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
597 use serde::ser::SerializeStruct;
598 let len = if self.description.is_some() { 2 } else { 1 };
599 let mut state = serializer.serialize_struct("CommandDefinition", len)?;
600 state.serialize_field("name", &self.name)?;
601 if let Some(description) = &self.description {
602 state.serialize_field("description", description)?;
603 }
604 state.end()
605 }
606}
607
608#[derive(Debug, Clone, Default, Serialize, Deserialize)]
615#[serde(rename_all = "camelCase")]
616#[non_exhaustive]
617pub struct CustomAgentConfig {
618 pub name: String,
620 #[serde(default, skip_serializing_if = "Option::is_none")]
622 pub display_name: Option<String>,
623 #[serde(default, skip_serializing_if = "Option::is_none")]
625 pub description: Option<String>,
626 #[serde(default, skip_serializing_if = "Option::is_none")]
628 pub tools: Option<Vec<String>>,
629 pub prompt: String,
631 #[serde(default, skip_serializing_if = "Option::is_none")]
633 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
634 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub infer: Option<bool>,
637 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub skills: Option<Vec<String>>,
640 #[serde(default, skip_serializing_if = "Option::is_none")]
645 pub model: Option<String>,
646 #[serde(default, skip_serializing_if = "Option::is_none")]
651 pub reasoning_effort: Option<String>,
652}
653
654impl CustomAgentConfig {
655 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
662 Self {
663 name: name.into(),
664 prompt: prompt.into(),
665 ..Self::default()
666 }
667 }
668
669 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
671 self.display_name = Some(display_name.into());
672 self
673 }
674
675 pub fn with_description(mut self, description: impl Into<String>) -> Self {
677 self.description = Some(description.into());
678 self
679 }
680
681 pub fn with_tools<I, S>(mut self, tools: I) -> Self
684 where
685 I: IntoIterator<Item = S>,
686 S: Into<String>,
687 {
688 self.tools = Some(tools.into_iter().map(Into::into).collect());
689 self
690 }
691
692 pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
694 self.mcp_servers = Some(mcp_servers);
695 self
696 }
697
698 pub fn with_infer(mut self, infer: bool) -> Self {
700 self.infer = Some(infer);
701 self
702 }
703
704 pub fn with_skills<I, S>(mut self, skills: I) -> Self
706 where
707 I: IntoIterator<Item = S>,
708 S: Into<String>,
709 {
710 self.skills = Some(skills.into_iter().map(Into::into).collect());
711 self
712 }
713
714 pub fn with_model(mut self, model: impl Into<String>) -> Self {
716 self.model = Some(model.into());
717 self
718 }
719
720 pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into<String>) -> Self {
722 self.reasoning_effort = Some(reasoning_effort.into());
723 self
724 }
725}
726
727#[derive(Debug, Clone, Default, Serialize, Deserialize)]
734#[serde(rename_all = "camelCase")]
735pub struct DefaultAgentConfig {
736 #[serde(default, skip_serializing_if = "Option::is_none")]
738 pub excluded_tools: Option<Vec<String>>,
739}
740
741#[derive(Debug, Clone, Default, Serialize, Deserialize)]
747#[serde(rename_all = "camelCase")]
748#[non_exhaustive]
749pub struct LargeToolOutputConfig {
750 #[serde(default, skip_serializing_if = "Option::is_none")]
752 pub enabled: Option<bool>,
753 #[serde(default, skip_serializing_if = "Option::is_none")]
756 pub max_size_bytes: Option<u64>,
757 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
760 pub output_directory: Option<PathBuf>,
761}
762
763impl LargeToolOutputConfig {
764 pub fn new() -> Self {
767 Self::default()
768 }
769
770 pub fn with_enabled(mut self, enabled: bool) -> Self {
772 self.enabled = Some(enabled);
773 self
774 }
775
776 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
778 self.max_size_bytes = Some(max_size_bytes);
779 self
780 }
781
782 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
784 self.output_directory = Some(output_directory.into());
785 self
786 }
787}
788
789#[derive(Debug, Clone, Default, Serialize, Deserialize)]
795#[serde(rename_all = "camelCase")]
796#[non_exhaustive]
797pub struct ToolSearchConfig {
798 #[serde(default, skip_serializing_if = "Option::is_none")]
800 pub enabled: Option<bool>,
801 #[serde(default, skip_serializing_if = "Option::is_none")]
804 pub defer_threshold: Option<u32>,
805}
806
807impl ToolSearchConfig {
808 pub fn new() -> Self {
811 Self::default()
812 }
813
814 pub fn with_enabled(mut self, enabled: bool) -> Self {
816 self.enabled = Some(enabled);
817 self
818 }
819
820 pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
823 self.defer_threshold = Some(defer_threshold);
824 self
825 }
826}
827
828#[derive(Debug, Clone, Default, Serialize, Deserialize)]
833#[serde(rename_all = "camelCase")]
834#[non_exhaustive]
835pub struct GitHubMcpToolConfig {
836 #[serde(default, skip_serializing_if = "Option::is_none")]
838 pub enable_all_tools: Option<bool>,
839 #[serde(default, skip_serializing_if = "Option::is_none")]
841 pub additional_toolsets: Option<Vec<String>>,
842 #[serde(default, skip_serializing_if = "Option::is_none")]
844 pub additional_tools: Option<Vec<String>>,
845 #[serde(default, skip_serializing_if = "Option::is_none")]
847 pub enable_insiders_mode: Option<bool>,
848 #[serde(default, skip_serializing_if = "Option::is_none")]
852 pub disable_form_deferral: Option<bool>,
853}
854
855impl GitHubMcpToolConfig {
856 pub fn new() -> Self {
858 Self::default()
859 }
860
861 pub fn with_enable_all_tools(mut self, value: bool) -> Self {
863 self.enable_all_tools = Some(value);
864 self
865 }
866
867 pub fn with_additional_toolsets<I, S>(mut self, values: I) -> Self
869 where
870 I: IntoIterator<Item = S>,
871 S: Into<String>,
872 {
873 self.additional_toolsets = Some(values.into_iter().map(Into::into).collect());
874 self
875 }
876
877 pub fn with_additional_tools<I, S>(mut self, values: I) -> Self
879 where
880 I: IntoIterator<Item = S>,
881 S: Into<String>,
882 {
883 self.additional_tools = Some(values.into_iter().map(Into::into).collect());
884 self
885 }
886
887 pub fn with_enable_insiders_mode(mut self, value: bool) -> Self {
889 self.enable_insiders_mode = Some(value);
890 self
891 }
892
893 pub fn with_disable_form_deferral(mut self, value: bool) -> Self {
897 self.disable_form_deferral = Some(value);
898 self
899 }
900}
901
902#[derive(Debug, Clone, Default, Serialize, Deserialize)]
909#[serde(rename_all = "camelCase")]
910#[non_exhaustive]
911pub struct InfiniteSessionConfig {
912 #[serde(default, skip_serializing_if = "Option::is_none")]
914 pub enabled: Option<bool>,
915 #[serde(default, skip_serializing_if = "Option::is_none")]
918 pub background_compaction_threshold: Option<f64>,
919 #[serde(default, skip_serializing_if = "Option::is_none")]
922 pub buffer_exhaustion_threshold: Option<f64>,
923}
924
925impl InfiniteSessionConfig {
926 pub fn new() -> Self {
929 Self::default()
930 }
931
932 pub fn with_enabled(mut self, enabled: bool) -> Self {
935 self.enabled = Some(enabled);
936 self
937 }
938
939 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
942 self.background_compaction_threshold = Some(threshold);
943 self
944 }
945
946 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
949 self.buffer_exhaustion_threshold = Some(threshold);
950 self
951 }
952}
953
954#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
965#[serde(rename_all = "camelCase")]
966#[non_exhaustive]
967pub struct MemoryConfiguration {
968 pub enabled: bool,
970}
971
972impl MemoryConfiguration {
973 pub fn enabled() -> Self {
975 Self { enabled: true }
976 }
977
978 pub fn disabled() -> Self {
980 Self { enabled: false }
981 }
982
983 pub fn with_enabled(mut self, enabled: bool) -> Self {
985 self.enabled = enabled;
986 self
987 }
988}
989
990#[derive(Debug, Clone, Serialize, Deserialize)]
992#[serde(rename_all = "camelCase")]
993#[non_exhaustive]
994pub struct CloudSessionRepository {
995 pub owner: String,
997 pub name: String,
999 #[serde(skip_serializing_if = "Option::is_none")]
1001 pub branch: Option<String>,
1002}
1003
1004impl CloudSessionRepository {
1005 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
1007 Self {
1008 owner: owner.into(),
1009 name: name.into(),
1010 branch: None,
1011 }
1012 }
1013
1014 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
1016 self.branch = Some(branch.into());
1017 self
1018 }
1019}
1020
1021#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1023#[serde(rename_all = "camelCase")]
1024#[non_exhaustive]
1025pub struct CloudSessionOptions {
1026 #[serde(skip_serializing_if = "Option::is_none")]
1028 pub repository: Option<CloudSessionRepository>,
1029}
1030
1031impl CloudSessionOptions {
1032 pub fn with_repository(repository: CloudSessionRepository) -> Self {
1034 Self {
1035 repository: Some(repository),
1036 }
1037 }
1038}
1039
1040#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1042#[serde(rename_all = "camelCase")]
1043pub struct ExtensionInfo {
1044 pub source: String,
1046 pub name: String,
1048}
1049
1050impl ExtensionInfo {
1051 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
1053 Self {
1054 source: source.into(),
1055 name: name.into(),
1056 }
1057 }
1058}
1059
1060#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1071#[serde(rename_all = "camelCase")]
1072pub struct CanvasProviderIdentity {
1073 pub id: String,
1075 #[serde(skip_serializing_if = "Option::is_none")]
1077 pub name: Option<String>,
1078}
1079
1080impl CanvasProviderIdentity {
1081 pub fn new(id: impl Into<String>) -> Self {
1083 Self {
1084 id: id.into(),
1085 name: None,
1086 }
1087 }
1088
1089 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1091 self.name = Some(name.into());
1092 self
1093 }
1094}
1095
1096#[derive(Debug, Clone, Serialize, Deserialize)]
1130#[serde(tag = "type", rename_all = "lowercase")]
1131#[non_exhaustive]
1132pub enum McpServerConfig {
1133 #[serde(alias = "local")]
1137 Stdio(McpStdioServerConfig),
1138 Http(McpHttpServerConfig),
1140 Sse(McpHttpServerConfig),
1142}
1143
1144#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1148#[serde(rename_all = "camelCase")]
1149pub struct McpStdioServerConfig {
1150 #[serde(default, skip_serializing_if = "Option::is_none")]
1156 pub tools: Option<Vec<String>>,
1157 #[serde(default, skip_serializing_if = "Option::is_none")]
1159 pub timeout: Option<i64>,
1160 pub command: String,
1162 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1164 pub args: Vec<String>,
1165 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1168 pub env: HashMap<String, String>,
1169 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1171 pub working_directory: Option<String>,
1172}
1173
1174#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1178#[serde(rename_all = "camelCase")]
1179pub struct McpHttpServerConfig {
1180 #[serde(default, skip_serializing_if = "Option::is_none")]
1186 pub tools: Option<Vec<String>>,
1187 #[serde(default, skip_serializing_if = "Option::is_none")]
1189 pub timeout: Option<i64>,
1190 pub url: String,
1192 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1194 pub headers: HashMap<String, String>,
1195}
1196
1197#[derive(Clone, Default, Serialize, Deserialize)]
1203#[serde(rename_all = "camelCase")]
1204#[non_exhaustive]
1205pub struct ProviderConfig {
1206 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1209 pub provider_type: Option<String>,
1210 #[serde(default, skip_serializing_if = "Option::is_none")]
1213 pub wire_api: Option<String>,
1214 #[serde(default, skip_serializing_if = "Option::is_none")]
1219 pub transport: Option<String>,
1220 pub base_url: String,
1222 #[serde(default, skip_serializing_if = "Option::is_none")]
1224 pub api_key: Option<String>,
1225 #[serde(default, skip_serializing_if = "Option::is_none")]
1229 pub bearer_token: Option<String>,
1230 #[serde(skip)]
1233 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1234 #[serde(default, skip_serializing_if = "Option::is_none")]
1235 pub(crate) has_bearer_token_provider: Option<bool>,
1236 #[serde(default, skip_serializing_if = "Option::is_none")]
1238 pub azure: Option<AzureProviderOptions>,
1239 #[serde(default, skip_serializing_if = "Option::is_none")]
1241 pub headers: Option<HashMap<String, String>>,
1242 #[serde(default, skip_serializing_if = "Option::is_none")]
1246 pub model_id: Option<String>,
1247 #[serde(default, skip_serializing_if = "Option::is_none")]
1254 pub wire_model: Option<String>,
1255 #[serde(default, skip_serializing_if = "Option::is_none")]
1260 pub max_prompt_tokens: Option<i64>,
1261 #[serde(default, skip_serializing_if = "Option::is_none")]
1264 pub max_output_tokens: Option<i64>,
1265}
1266
1267impl std::fmt::Debug for ProviderConfig {
1268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1269 f.debug_struct("ProviderConfig")
1270 .field("provider_type", &self.provider_type)
1271 .field("wire_api", &self.wire_api)
1272 .field("transport", &self.transport)
1273 .field("base_url", &self.base_url)
1274 .field("api_key", &self.api_key)
1275 .field("bearer_token", &self.bearer_token)
1276 .field(
1277 "bearer_token_provider",
1278 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1279 )
1280 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1281 .field("azure", &self.azure)
1282 .field("headers", &self.headers)
1283 .field("model_id", &self.model_id)
1284 .field("wire_model", &self.wire_model)
1285 .field("max_prompt_tokens", &self.max_prompt_tokens)
1286 .field("max_output_tokens", &self.max_output_tokens)
1287 .finish()
1288 }
1289}
1290
1291impl ProviderConfig {
1292 pub fn new(base_url: impl Into<String>) -> Self {
1295 Self {
1296 base_url: base_url.into(),
1297 ..Self::default()
1298 }
1299 }
1300
1301 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1303 self.provider_type = Some(provider_type.into());
1304 self
1305 }
1306
1307 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1309 self.wire_api = Some(wire_api.into());
1310 self
1311 }
1312
1313 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1316 self.transport = Some(transport.into());
1317 self
1318 }
1319
1320 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1322 self.api_key = Some(api_key.into());
1323 self
1324 }
1325
1326 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1329 self.bearer_token = Some(bearer_token.into());
1330 self
1331 }
1332
1333 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1339 self.bearer_token_provider = Some(provider);
1340 self
1341 }
1342
1343 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1345 self.azure = Some(azure);
1346 self
1347 }
1348
1349 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1351 self.headers = Some(headers);
1352 self
1353 }
1354
1355 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1358 self.model_id = Some(model_id.into());
1359 self
1360 }
1361
1362 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1367 self.wire_model = Some(wire_model.into());
1368 self
1369 }
1370
1371 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1375 self.max_prompt_tokens = Some(max);
1376 self
1377 }
1378
1379 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1382 self.max_output_tokens = Some(max);
1383 self
1384 }
1385}
1386
1387#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1400#[serde(rename_all = "camelCase")]
1401#[non_exhaustive]
1402pub struct CapiSessionOptions {
1403 #[serde(default, skip_serializing_if = "Option::is_none")]
1409 pub enable_web_socket_responses: Option<bool>,
1410}
1411
1412impl CapiSessionOptions {
1413 pub fn new() -> Self {
1415 Self::default()
1416 }
1417
1418 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1420 self.enable_web_socket_responses = Some(enable);
1421 self
1422 }
1423}
1424
1425#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1427#[serde(rename_all = "camelCase")]
1428pub struct AzureProviderOptions {
1429 #[serde(default, skip_serializing_if = "Option::is_none")]
1431 pub api_version: Option<String>,
1432}
1433
1434#[derive(Clone, Default, Serialize, Deserialize)]
1445#[serde(rename_all = "camelCase")]
1446#[non_exhaustive]
1447pub struct NamedProviderConfig {
1448 pub name: String,
1451 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1454 pub provider_type: Option<String>,
1455 #[serde(default, skip_serializing_if = "Option::is_none")]
1458 pub wire_api: Option<String>,
1459 pub base_url: String,
1461 #[serde(default, skip_serializing_if = "Option::is_none")]
1463 pub api_key: Option<String>,
1464 #[serde(default, skip_serializing_if = "Option::is_none")]
1467 pub bearer_token: Option<String>,
1468 #[serde(skip)]
1471 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1472 #[serde(default, skip_serializing_if = "Option::is_none")]
1473 pub(crate) has_bearer_token_provider: Option<bool>,
1474 #[serde(default, skip_serializing_if = "Option::is_none")]
1476 pub azure: Option<AzureProviderOptions>,
1477 #[serde(default, skip_serializing_if = "Option::is_none")]
1479 pub headers: Option<HashMap<String, String>>,
1480}
1481
1482impl std::fmt::Debug for NamedProviderConfig {
1483 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1484 f.debug_struct("NamedProviderConfig")
1485 .field("name", &self.name)
1486 .field("provider_type", &self.provider_type)
1487 .field("wire_api", &self.wire_api)
1488 .field("base_url", &self.base_url)
1489 .field("api_key", &self.api_key)
1490 .field("bearer_token", &self.bearer_token)
1491 .field(
1492 "bearer_token_provider",
1493 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1494 )
1495 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1496 .field("azure", &self.azure)
1497 .field("headers", &self.headers)
1498 .finish()
1499 }
1500}
1501
1502impl NamedProviderConfig {
1503 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1506 Self {
1507 name: name.into(),
1508 base_url: base_url.into(),
1509 ..Self::default()
1510 }
1511 }
1512
1513 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1515 self.provider_type = Some(provider_type.into());
1516 self
1517 }
1518
1519 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1521 self.wire_api = Some(wire_api.into());
1522 self
1523 }
1524
1525 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1527 self.api_key = Some(api_key.into());
1528 self
1529 }
1530
1531 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1534 self.bearer_token = Some(bearer_token.into());
1535 self
1536 }
1537
1538 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1544 self.bearer_token_provider = Some(provider);
1545 self
1546 }
1547
1548 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1550 self.azure = Some(azure);
1551 self
1552 }
1553
1554 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1556 self.headers = Some(headers);
1557 self
1558 }
1559}
1560
1561fn prepare_bearer_token_providers(
1562 provider: &mut Option<ProviderConfig>,
1563 providers: &mut Option<Vec<NamedProviderConfig>>,
1564) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1565 let mut bearer_token_providers = HashMap::new();
1566
1567 if let Some(provider) = provider.as_mut()
1568 && let Some(token_provider) = provider.bearer_token_provider.take()
1569 {
1570 provider.has_bearer_token_provider = Some(true);
1571 bearer_token_providers.insert("default".to_string(), token_provider);
1572 }
1573
1574 if let Some(providers) = providers.as_mut() {
1575 for provider in providers {
1576 if let Some(token_provider) = provider.bearer_token_provider.take() {
1577 provider.has_bearer_token_provider = Some(true);
1578 bearer_token_providers.insert(provider.name.clone(), token_provider);
1579 }
1580 }
1581 }
1582
1583 bearer_token_providers
1584}
1585
1586#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1594#[serde(rename_all = "camelCase")]
1595#[non_exhaustive]
1596pub struct ProviderModelConfig {
1597 pub id: String,
1600 pub provider: String,
1602 #[serde(default, skip_serializing_if = "Option::is_none")]
1605 pub wire_model: Option<String>,
1606 #[serde(default, skip_serializing_if = "Option::is_none")]
1609 pub model_id: Option<String>,
1610 #[serde(default, skip_serializing_if = "Option::is_none")]
1612 pub name: Option<String>,
1613 #[serde(default, skip_serializing_if = "Option::is_none")]
1615 pub max_prompt_tokens: Option<i64>,
1616 #[serde(default, skip_serializing_if = "Option::is_none")]
1618 pub max_context_window_tokens: Option<i64>,
1619 #[serde(default, skip_serializing_if = "Option::is_none")]
1621 pub max_output_tokens: Option<i64>,
1622 #[serde(default, skip_serializing_if = "Option::is_none")]
1625 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1626}
1627
1628impl ProviderModelConfig {
1629 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1632 Self {
1633 id: id.into(),
1634 provider: provider.into(),
1635 ..Self::default()
1636 }
1637 }
1638
1639 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1641 self.wire_model = Some(wire_model.into());
1642 self
1643 }
1644
1645 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1648 self.model_id = Some(model_id.into());
1649 self
1650 }
1651
1652 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1654 self.name = Some(name.into());
1655 self
1656 }
1657
1658 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1660 self.max_prompt_tokens = Some(max);
1661 self
1662 }
1663
1664 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1666 self.max_context_window_tokens = Some(max);
1667 self
1668 }
1669
1670 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1672 self.max_output_tokens = Some(max);
1673 self
1674 }
1675
1676 pub fn with_capabilities(
1678 mut self,
1679 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1680 ) -> Self {
1681 self.capabilities = Some(capabilities);
1682 self
1683 }
1684}
1685
1686#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1690#[serde(untagged)]
1691pub enum ExpFlagValue {
1692 Bool(bool),
1694 Integer(i64),
1696 Float(f64),
1698 String(String),
1700 Null,
1702}
1703
1704#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1708#[serde(rename_all = "PascalCase")]
1709pub struct ExpConfigEntry {
1710 pub id: String,
1712 pub parameters: HashMap<String, ExpFlagValue>,
1714}
1715
1716#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1722#[serde(rename_all = "PascalCase")]
1723pub struct CopilotExpAssignmentResponse {
1724 #[serde(default)]
1726 pub features: Vec<String>,
1727 #[serde(default)]
1729 pub flights: HashMap<String, String>,
1730 #[serde(default)]
1732 pub configs: Vec<ExpConfigEntry>,
1733 #[serde(default, skip_serializing_if = "Option::is_none")]
1735 pub parameter_groups: Option<Value>,
1736 #[serde(default, skip_serializing_if = "Option::is_none")]
1738 pub flighting_version: Option<i64>,
1739 #[serde(default, skip_serializing_if = "Option::is_none")]
1741 pub impression_id: Option<String>,
1742 #[serde(default)]
1744 pub assignment_context: String,
1745}
1746
1747#[derive(Clone)]
1799#[non_exhaustive]
1800pub struct SessionConfig {
1801 pub session_id: Option<SessionId>,
1803 pub model: Option<String>,
1805 pub client_name: Option<String>,
1807 pub reasoning_effort: Option<String>,
1809 pub reasoning_summary: Option<ReasoningSummary>,
1813 pub context_tier: Option<String>,
1816 pub streaming: Option<bool>,
1818 pub system_message: Option<SystemMessageConfig>,
1820 pub tools: Option<Vec<Tool>>,
1822 pub canvases: Option<Vec<CanvasDeclaration>>,
1824 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1829 pub request_canvas_renderer: Option<bool>,
1831 pub request_extensions: Option<bool>,
1833 pub extension_sdk_path: Option<String>,
1837 pub extension_info: Option<ExtensionInfo>,
1839 pub canvas_provider: Option<CanvasProviderIdentity>,
1842 pub available_tools: Option<Vec<String>>,
1844 pub excluded_tools: Option<Vec<String>>,
1846 pub excluded_builtin_agents: Option<Vec<String>>,
1852 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
1854 pub mcp_oauth_token_storage: Option<String>,
1863 pub enable_config_discovery: Option<bool>,
1866 pub skip_embedding_retrieval: Option<bool>,
1868 pub embedding_cache_storage: Option<String>,
1871 pub organization_custom_instructions: Option<String>,
1873 pub enable_on_demand_instruction_discovery: Option<bool>,
1875 pub enable_file_hooks: Option<bool>,
1877 pub enable_host_git_operations: Option<bool>,
1879 pub enable_session_store: Option<bool>,
1881 pub enable_skills: Option<bool>,
1883 pub enable_mcp_apps: Option<bool>,
1910 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
1915 pub skill_directories: Option<Vec<PathBuf>>,
1917 pub instruction_directories: Option<Vec<PathBuf>>,
1920 pub plugin_directories: Option<Vec<PathBuf>>,
1922 pub large_output: Option<LargeToolOutputConfig>,
1924 pub tool_search: Option<ToolSearchConfig>,
1928 pub disabled_skills: Option<Vec<String>>,
1931 pub disabled_mcp_servers: Option<Vec<String>>,
1935 pub hooks: Option<bool>,
1939 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1941 pub default_agent: Option<DefaultAgentConfig>,
1945 pub agent: Option<String>,
1948 pub infinite_sessions: Option<InfiniteSessionConfig>,
1951 pub provider: Option<ProviderConfig>,
1955 pub capi: Option<CapiSessionOptions>,
1961 pub providers: Option<Vec<NamedProviderConfig>>,
1968 pub models: Option<Vec<ProviderModelConfig>>,
1974 pub enable_session_telemetry: Option<bool>,
1982 pub enable_citations: Option<bool>,
1984 pub session_limits: Option<SessionLimitsConfig>,
1986 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1989 pub memory: Option<MemoryConfiguration>,
1991 pub config_directory: Option<PathBuf>,
1994 pub working_directory: Option<PathBuf>,
1997 pub additional_directories: Option<Vec<PathBuf>>,
2001 pub github_token: Option<String>,
2007 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2013 pub cloud: Option<CloudSessionOptions>,
2016 pub include_sub_agent_streaming_events: Option<bool>,
2020 pub commands: Option<Vec<CommandDefinition>>,
2024 #[doc(hidden)]
2031 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
2032 pub enable_managed_settings: Option<bool>,
2039 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2044 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2048 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2051 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2054 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2058 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2061 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2064 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2068 pub(crate) permission_policy: Option<crate::permission::Policy>,
2072 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2077 pub skip_custom_instructions: Option<bool>,
2081 pub custom_agents_local_only: Option<bool>,
2085 pub enable_experimental_mode: Option<bool>,
2090 pub coauthor_enabled: Option<bool>,
2094 pub manage_schedule_enabled: Option<bool>,
2098}
2099
2100impl std::fmt::Debug for SessionConfig {
2101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2102 f.debug_struct("SessionConfig")
2103 .field("session_id", &self.session_id)
2104 .field("model", &self.model)
2105 .field("client_name", &self.client_name)
2106 .field("reasoning_effort", &self.reasoning_effort)
2107 .field("reasoning_summary", &self.reasoning_summary)
2108 .field("context_tier", &self.context_tier)
2109 .field("streaming", &self.streaming)
2110 .field("system_message", &self.system_message)
2111 .field("tools", &self.tools)
2112 .field("canvases", &self.canvases)
2113 .field(
2114 "canvas_handler",
2115 &self.canvas_handler.as_ref().map(|_| "<set>"),
2116 )
2117 .field("request_canvas_renderer", &self.request_canvas_renderer)
2118 .field("request_extensions", &self.request_extensions)
2119 .field("extension_sdk_path", &self.extension_sdk_path)
2120 .field("extension_info", &self.extension_info)
2121 .field("canvas_provider", &self.canvas_provider)
2122 .field("available_tools", &self.available_tools)
2123 .field("excluded_tools", &self.excluded_tools)
2124 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2125 .field("mcp_servers", &self.mcp_servers)
2126 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2127 .field("embedding_cache_storage", &self.embedding_cache_storage)
2128 .field("enable_config_discovery", &self.enable_config_discovery)
2129 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2130 .field(
2131 "organization_custom_instructions",
2132 &self
2133 .organization_custom_instructions
2134 .as_ref()
2135 .map(|_| "<redacted>"),
2136 )
2137 .field(
2138 "enable_on_demand_instruction_discovery",
2139 &self.enable_on_demand_instruction_discovery,
2140 )
2141 .field("enable_file_hooks", &self.enable_file_hooks)
2142 .field(
2143 "enable_host_git_operations",
2144 &self.enable_host_git_operations,
2145 )
2146 .field("enable_session_store", &self.enable_session_store)
2147 .field("enable_skills", &self.enable_skills)
2148 .field("enable_mcp_apps", &self.enable_mcp_apps)
2149 .field("skill_directories", &self.skill_directories)
2150 .field("instruction_directories", &self.instruction_directories)
2151 .field("plugin_directories", &self.plugin_directories)
2152 .field("large_output", &self.large_output)
2153 .field("tool_search", &self.tool_search)
2154 .field("disabled_skills", &self.disabled_skills)
2155 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
2156 .field("hooks", &self.hooks)
2157 .field("custom_agents", &self.custom_agents)
2158 .field("default_agent", &self.default_agent)
2159 .field("agent", &self.agent)
2160 .field("infinite_sessions", &self.infinite_sessions)
2161 .field("provider", &self.provider)
2162 .field("capi", &self.capi)
2163 .field("enable_session_telemetry", &self.enable_session_telemetry)
2164 .field("enable_citations", &self.enable_citations)
2165 .field("session_limits", &self.session_limits)
2166 .field("model_capabilities", &self.model_capabilities)
2167 .field("memory", &self.memory)
2168 .field("config_directory", &self.config_directory)
2169 .field("working_directory", &self.working_directory)
2170 .field("additional_directories", &self.additional_directories)
2171 .field(
2172 "github_token",
2173 &self.github_token.as_ref().map(|_| "<redacted>"),
2174 )
2175 .field("remote_session", &self.remote_session)
2176 .field("cloud", &self.cloud)
2177 .field(
2178 "include_sub_agent_streaming_events",
2179 &self.include_sub_agent_streaming_events,
2180 )
2181 .field("commands", &self.commands)
2182 .field("exp_assignments", &self.exp_assignments)
2183 .field("enable_managed_settings", &self.enable_managed_settings)
2184 .field("enable_experimental_mode", &self.enable_experimental_mode)
2185 .field(
2186 "session_fs_provider",
2187 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2188 )
2189 .field(
2190 "permission_handler",
2191 &self.permission_handler.as_ref().map(|_| "<set>"),
2192 )
2193 .field(
2194 "elicitation_handler",
2195 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2196 )
2197 .field(
2198 "mcp_auth_handler",
2199 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2200 )
2201 .field(
2202 "user_input_handler",
2203 &self.user_input_handler.as_ref().map(|_| "<set>"),
2204 )
2205 .field(
2206 "exit_plan_mode_handler",
2207 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2208 )
2209 .field(
2210 "auto_mode_switch_handler",
2211 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2212 )
2213 .field(
2214 "hooks_handler",
2215 &self.hooks_handler.as_ref().map(|_| "<set>"),
2216 )
2217 .field(
2218 "system_message_transform",
2219 &self.system_message_transform.as_ref().map(|_| "<set>"),
2220 )
2221 .finish()
2222 }
2223}
2224
2225impl Default for SessionConfig {
2226 fn default() -> Self {
2232 Self {
2233 session_id: None,
2234 model: None,
2235 client_name: None,
2236 reasoning_effort: None,
2237 reasoning_summary: None,
2238 context_tier: None,
2239 streaming: None,
2240 system_message: None,
2241 tools: None,
2242 canvases: None,
2243 canvas_handler: None,
2244 request_canvas_renderer: None,
2245 request_extensions: None,
2246 extension_sdk_path: None,
2247 extension_info: None,
2248 canvas_provider: None,
2249 available_tools: None,
2250 excluded_tools: None,
2251 excluded_builtin_agents: None,
2252 mcp_servers: None,
2253 mcp_oauth_token_storage: None,
2254 enable_config_discovery: None,
2255 skip_embedding_retrieval: None,
2256 organization_custom_instructions: None,
2257 enable_on_demand_instruction_discovery: None,
2258 enable_file_hooks: None,
2259 enable_host_git_operations: None,
2260 enable_session_store: None,
2261 enable_skills: None,
2262 embedding_cache_storage: None,
2263 enable_mcp_apps: None,
2264 github_mcp_tool_config: None,
2265 skill_directories: None,
2266 instruction_directories: None,
2267 plugin_directories: None,
2268 large_output: None,
2269 tool_search: None,
2270 disabled_skills: None,
2271 disabled_mcp_servers: None,
2272 hooks: None,
2273 custom_agents: None,
2274 default_agent: None,
2275 agent: None,
2276 infinite_sessions: None,
2277 provider: None,
2278 capi: None,
2279 providers: None,
2280 models: None,
2281 enable_session_telemetry: None,
2282 enable_citations: None,
2283 session_limits: None,
2284 model_capabilities: None,
2285 memory: None,
2286 config_directory: None,
2287 working_directory: None,
2288 additional_directories: None,
2289 github_token: None,
2290 remote_session: None,
2291 cloud: None,
2292 include_sub_agent_streaming_events: None,
2293 commands: None,
2294 exp_assignments: None,
2295 enable_managed_settings: None,
2296 session_fs_provider: None,
2297 permission_handler: None,
2298 elicitation_handler: None,
2299 mcp_auth_handler: None,
2300 user_input_handler: None,
2301 exit_plan_mode_handler: None,
2302 auto_mode_switch_handler: None,
2303 hooks_handler: None,
2304 permission_policy: None,
2305 system_message_transform: None,
2306 skip_custom_instructions: None,
2307 custom_agents_local_only: None,
2308 enable_experimental_mode: None,
2309 coauthor_enabled: None,
2310 manage_schedule_enabled: None,
2311 }
2312 }
2313}
2314
2315pub(crate) struct SessionConfigRuntime {
2321 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2322 pub permission_policy: Option<crate::permission::Policy>,
2323 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2324 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2325 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2326 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2327 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2328 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2329 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2330 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2331 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2332 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2333 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2334 pub commands: Option<Vec<CommandDefinition>>,
2335}
2336
2337impl SessionConfig {
2338 pub(crate) fn into_wire(
2350 mut self,
2351 session_id: Option<SessionId>,
2352 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2353 let permission_active =
2354 self.permission_handler.is_some() || self.permission_policy.is_some();
2355 let request_user_input = self.user_input_handler.is_some();
2356 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2357 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2358 let request_elicitation = self.elicitation_handler.is_some();
2359 let hooks_flag = self.hooks_handler.is_some();
2360
2361 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2362 if let Some(tools) = self.tools.as_mut() {
2363 for tool in tools.iter_mut() {
2364 if let Some(handler) = tool.handler.take()
2365 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2366 {
2367 return Err(crate::Error::with_message(
2368 crate::ErrorKind::InvalidConfig,
2369 format!("duplicate tool handler registered for name {:?}", tool.name),
2370 ));
2371 }
2372 }
2373 }
2374
2375 let wire_commands = self.commands.as_ref().map(|cmds| {
2376 cmds.iter()
2377 .map(|c| crate::wire::CommandWireDefinition {
2378 name: c.name.clone(),
2379 description: c.description.clone(),
2380 })
2381 .collect()
2382 });
2383 let wire_canvases = self.canvases.clone();
2384 let canvas_handler = self.canvas_handler.clone();
2385 let bearer_token_providers =
2386 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2387
2388 let wire = crate::wire::SessionCreateWire {
2389 session_id,
2390 model: self.model,
2391 client_name: self.client_name,
2392 reasoning_effort: self.reasoning_effort,
2393 reasoning_summary: self.reasoning_summary,
2394 context_tier: self.context_tier,
2395 streaming: self.streaming,
2396 system_message: self.system_message,
2397 tools: self.tools,
2398 canvases: wire_canvases,
2399 request_canvas_renderer: self.request_canvas_renderer,
2400 request_extensions: self.request_extensions,
2401 extension_sdk_path: self.extension_sdk_path,
2402 extension_info: self.extension_info,
2403 canvas_provider: self.canvas_provider,
2404 available_tools: self.available_tools,
2405 excluded_tools: self.excluded_tools,
2406 excluded_builtin_agents: self.excluded_builtin_agents,
2407 tool_filter_precedence: "excluded",
2408 mcp_servers: self.mcp_servers,
2409 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2410 embedding_cache_storage: self.embedding_cache_storage,
2411 env_value_mode: "direct",
2412 enable_config_discovery: self.enable_config_discovery,
2413 skip_embedding_retrieval: self.skip_embedding_retrieval,
2414 organization_custom_instructions: self.organization_custom_instructions,
2415 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2416 enable_file_hooks: self.enable_file_hooks,
2417 enable_host_git_operations: self.enable_host_git_operations,
2418 enable_session_store: self.enable_session_store,
2419 enable_skills: self.enable_skills,
2420 request_user_input,
2421 request_permission: permission_active,
2422 request_exit_plan_mode,
2423 request_auto_mode_switch,
2424 request_elicitation,
2425 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2426 github_mcp_tool_config: self.github_mcp_tool_config,
2427 hooks: hooks_flag,
2428 skill_directories: self.skill_directories,
2429 instruction_directories: self.instruction_directories,
2430 plugin_directories: self.plugin_directories,
2431 large_output: self.large_output,
2432 tool_search: self.tool_search,
2433 disabled_skills: self.disabled_skills,
2434 disabled_mcp_servers: self.disabled_mcp_servers,
2435 custom_agents: self.custom_agents,
2436 custom_agents_local_only: self.custom_agents_local_only,
2437 default_agent: self.default_agent,
2438 agent: self.agent,
2439 infinite_sessions: self.infinite_sessions,
2440 provider: self.provider,
2441 capi: self.capi,
2442 providers: self.providers,
2443 models: self.models,
2444 enable_session_telemetry: self.enable_session_telemetry,
2445 enable_citations: self.enable_citations,
2446 session_limits: self.session_limits,
2447 model_capabilities: self.model_capabilities,
2448 memory: self.memory,
2449 config_dir: self.config_directory,
2450 working_directory: self.working_directory,
2451 additional_directories: self.additional_directories,
2452 github_token: self.github_token,
2453 remote_session: self.remote_session,
2454 cloud: self.cloud,
2455 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2456 enable_github_telemetry_forwarding: None,
2457 commands: wire_commands,
2458 exp_assignments: self.exp_assignments,
2459 enable_managed_settings: self.enable_managed_settings,
2460 is_experimental_mode: self.enable_experimental_mode,
2461 };
2462
2463 let runtime = SessionConfigRuntime {
2464 permission_handler: self.permission_handler,
2465 permission_policy: self.permission_policy,
2466 elicitation_handler: self.elicitation_handler,
2467 mcp_auth_handler: self.mcp_auth_handler,
2468 user_input_handler: self.user_input_handler,
2469 exit_plan_mode_handler: self.exit_plan_mode_handler,
2470 auto_mode_switch_handler: self.auto_mode_switch_handler,
2471 hooks_handler: self.hooks_handler,
2472 system_message_transform: self.system_message_transform,
2473 tool_handlers,
2474 canvas_handler,
2475 session_fs_provider: self.session_fs_provider,
2476 bearer_token_providers,
2477 commands: self.commands,
2478 };
2479
2480 Ok((wire, runtime))
2481 }
2482
2483 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2487 self.permission_handler = Some(handler);
2488 self
2489 }
2490
2491 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2494 self.elicitation_handler = Some(handler);
2495 self
2496 }
2497
2498 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2500 self.mcp_auth_handler = Some(handler);
2501 self
2502 }
2503
2504 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2507 self.user_input_handler = Some(handler);
2508 self
2509 }
2510
2511 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2513 self.exit_plan_mode_handler = Some(handler);
2514 self
2515 }
2516
2517 pub fn with_auto_mode_switch_handler(
2519 mut self,
2520 handler: Arc<dyn AutoModeSwitchHandler>,
2521 ) -> Self {
2522 self.auto_mode_switch_handler = Some(handler);
2523 self
2524 }
2525
2526 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2531 self.commands = Some(commands);
2532 self
2533 }
2534
2535 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2539 self.session_fs_provider = Some(provider);
2540 self
2541 }
2542
2543 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2546 self.hooks_handler = Some(hooks);
2547 self
2548 }
2549
2550 pub fn with_system_message_transform(
2554 mut self,
2555 transform: Arc<dyn SystemMessageTransform>,
2556 ) -> Self {
2557 self.system_message_transform = Some(transform);
2558 self
2559 }
2560
2561 pub fn approve_all_permissions(mut self) -> Self {
2567 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2568 self
2569 }
2570
2571 pub fn deny_all_permissions(mut self) -> Self {
2574 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2575 self
2576 }
2577
2578 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2583 where
2584 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2585 {
2586 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2587 self
2588 }
2589
2590 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2592 self.session_id = Some(id.into());
2593 self
2594 }
2595
2596 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2598 self.model = Some(model.into());
2599 self
2600 }
2601
2602 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2604 self.client_name = Some(name.into());
2605 self
2606 }
2607
2608 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2610 self.reasoning_effort = Some(effort.into());
2611 self
2612 }
2613
2614 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2616 self.reasoning_summary = Some(summary);
2617 self
2618 }
2619
2620 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2622 self.context_tier = Some(tier.into());
2623 self
2624 }
2625
2626 pub fn with_streaming(mut self, streaming: bool) -> Self {
2628 self.streaming = Some(streaming);
2629 self
2630 }
2631
2632 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2634 self.system_message = Some(system_message);
2635 self
2636 }
2637
2638 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2640 self.tools = Some(tools.into_iter().collect());
2641 self
2642 }
2643
2644 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2649 self.canvases = Some(canvases.into_iter().collect());
2650 self
2651 }
2652
2653 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2655 self.canvas_handler = Some(handler);
2656 self
2657 }
2658
2659 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2661 self.request_canvas_renderer = Some(request);
2662 self
2663 }
2664
2665 pub fn with_request_extensions(mut self, request: bool) -> Self {
2667 self.request_extensions = Some(request);
2668 self
2669 }
2670
2671 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2675 self.extension_sdk_path = Some(path.into());
2676 self
2677 }
2678
2679 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2681 self.extension_info = Some(extension_info);
2682 self
2683 }
2684
2685 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2688 self.canvas_provider = Some(canvas_provider);
2689 self
2690 }
2691
2692 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2694 where
2695 I: IntoIterator<Item = S>,
2696 S: Into<String>,
2697 {
2698 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2699 self
2700 }
2701
2702 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2704 where
2705 I: IntoIterator<Item = S>,
2706 S: Into<String>,
2707 {
2708 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2709 self
2710 }
2711
2712 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2714 where
2715 I: IntoIterator<Item = S>,
2716 S: Into<String>,
2717 {
2718 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2719 self
2720 }
2721
2722 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2724 self.mcp_servers = Some(servers);
2725 self
2726 }
2727
2728 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2736 self.mcp_oauth_token_storage = Some(mode.into());
2737 self
2738 }
2739
2740 pub fn with_embedding_cache_storage(
2742 mut self,
2743 embedding_cache_storage: impl Into<String>,
2744 ) -> Self {
2745 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2746 self
2747 }
2748
2749 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2752 self.enable_config_discovery = Some(enable);
2753 self
2754 }
2755
2756 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2758 self.skip_embedding_retrieval = Some(value);
2759 self
2760 }
2761
2762 pub fn with_organization_custom_instructions(
2764 mut self,
2765 instructions: impl Into<String>,
2766 ) -> Self {
2767 self.organization_custom_instructions = Some(instructions.into());
2768 self
2769 }
2770
2771 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2773 self.enable_on_demand_instruction_discovery = Some(value);
2774 self
2775 }
2776
2777 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2779 self.enable_file_hooks = Some(value);
2780 self
2781 }
2782
2783 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2785 self.enable_host_git_operations = Some(value);
2786 self
2787 }
2788
2789 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2791 self.enable_session_store = Some(value);
2792 self
2793 }
2794
2795 pub fn with_enable_skills(mut self, value: bool) -> Self {
2797 self.enable_skills = Some(value);
2798 self
2799 }
2800
2801 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2807 self.enable_mcp_apps = Some(enable);
2808 self
2809 }
2810
2811 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
2813 self.github_mcp_tool_config = Some(config);
2814 self
2815 }
2816
2817 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2819 where
2820 I: IntoIterator<Item = P>,
2821 P: Into<PathBuf>,
2822 {
2823 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2824 self
2825 }
2826
2827 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2831 where
2832 I: IntoIterator<Item = P>,
2833 P: Into<PathBuf>,
2834 {
2835 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2836 self
2837 }
2838
2839 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2841 where
2842 I: IntoIterator<Item = P>,
2843 P: Into<PathBuf>,
2844 {
2845 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2846 self
2847 }
2848
2849 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2851 self.large_output = Some(config);
2852 self
2853 }
2854
2855 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
2858 self.tool_search = Some(config);
2859 self
2860 }
2861
2862 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2864 where
2865 I: IntoIterator<Item = S>,
2866 S: Into<String>,
2867 {
2868 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2869 self
2870 }
2871
2872 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
2874 where
2875 I: IntoIterator<Item = S>,
2876 S: Into<String>,
2877 {
2878 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
2879 self
2880 }
2881
2882 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2884 mut self,
2885 agents: I,
2886 ) -> Self {
2887 self.custom_agents = Some(agents.into_iter().collect());
2888 self
2889 }
2890
2891 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2893 self.default_agent = Some(agent);
2894 self
2895 }
2896
2897 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2900 self.agent = Some(name.into());
2901 self
2902 }
2903
2904 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2907 self.infinite_sessions = Some(config);
2908 self
2909 }
2910
2911 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2913 self.provider = Some(provider);
2914 self
2915 }
2916
2917 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
2919 self.capi = Some(capi);
2920 self
2921 }
2922
2923 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
2929 self.providers = Some(providers);
2930 self
2931 }
2932
2933 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
2939 self.models = Some(models);
2940 self
2941 }
2942
2943 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2947 self.enable_session_telemetry = Some(enable);
2948 self
2949 }
2950
2951 pub fn with_enable_citations(mut self, enable: bool) -> Self {
2953 self.enable_citations = Some(enable);
2954 self
2955 }
2956
2957 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
2959 self.session_limits = Some(limits);
2960 self
2961 }
2962
2963 pub fn with_model_capabilities(
2965 mut self,
2966 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2967 ) -> Self {
2968 self.model_capabilities = Some(capabilities);
2969 self
2970 }
2971
2972 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
2974 self.memory = Some(memory);
2975 self
2976 }
2977
2978 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2980 self.config_directory = Some(dir.into());
2981 self
2982 }
2983
2984 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2987 self.working_directory = Some(dir.into());
2988 self
2989 }
2990
2991 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
2993 where
2994 I: IntoIterator<Item = P>,
2995 P: Into<PathBuf>,
2996 {
2997 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
2998 self
2999 }
3000
3001 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3006 self.github_token = Some(token.into());
3007 self
3008 }
3009
3010 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3013 self.include_sub_agent_streaming_events = Some(include);
3014 self
3015 }
3016
3017 pub fn with_remote_session(
3019 mut self,
3020 mode: crate::generated::api_types::RemoteSessionMode,
3021 ) -> Self {
3022 self.remote_session = Some(mode);
3023 self
3024 }
3025
3026 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
3028 self.cloud = Some(cloud);
3029 self
3030 }
3031
3032 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3034 self.skip_custom_instructions = Some(value);
3035 self
3036 }
3037
3038 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3040 self.custom_agents_local_only = Some(value);
3041 self
3042 }
3043
3044 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
3046 self.enable_experimental_mode = Some(enable_experimental_mode);
3047 self
3048 }
3049
3050 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3052 self.coauthor_enabled = Some(value);
3053 self
3054 }
3055
3056 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3058 self.manage_schedule_enabled = Some(value);
3059 self
3060 }
3061
3062 #[doc(hidden)]
3070 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
3071 self.exp_assignments = Some(assignments);
3072 self
3073 }
3074
3075 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3081 self.enable_managed_settings = Some(enabled);
3082 self
3083 }
3084}
3085#[derive(Clone)]
3092#[non_exhaustive]
3093pub struct ResumeSessionConfig {
3094 pub session_id: SessionId,
3096 pub model: Option<String>,
3099 pub client_name: Option<String>,
3101 pub reasoning_effort: Option<String>,
3103 pub reasoning_summary: Option<ReasoningSummary>,
3107 pub context_tier: Option<String>,
3110 pub streaming: Option<bool>,
3112 pub system_message: Option<SystemMessageConfig>,
3115 pub tools: Option<Vec<Tool>>,
3117 pub canvases: Option<Vec<CanvasDeclaration>>,
3119 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
3122 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3124 pub request_canvas_renderer: Option<bool>,
3126 pub request_extensions: Option<bool>,
3128 pub extension_sdk_path: Option<String>,
3132 pub extension_info: Option<ExtensionInfo>,
3134 pub canvas_provider: Option<CanvasProviderIdentity>,
3137 pub available_tools: Option<Vec<String>>,
3139 pub excluded_tools: Option<Vec<String>>,
3141 pub excluded_builtin_agents: Option<Vec<String>>,
3147 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3149 pub mcp_oauth_token_storage: Option<String>,
3152 pub enable_config_discovery: Option<bool>,
3155 pub skip_embedding_retrieval: Option<bool>,
3157 pub embedding_cache_storage: Option<String>,
3159 pub organization_custom_instructions: Option<String>,
3161 pub enable_on_demand_instruction_discovery: Option<bool>,
3163 pub enable_file_hooks: Option<bool>,
3165 pub enable_host_git_operations: Option<bool>,
3167 pub enable_session_store: Option<bool>,
3169 pub enable_skills: Option<bool>,
3171 pub enable_mcp_apps: Option<bool>,
3177 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
3182 pub skill_directories: Option<Vec<PathBuf>>,
3184 pub instruction_directories: Option<Vec<PathBuf>>,
3187 pub plugin_directories: Option<Vec<PathBuf>>,
3189 pub large_output: Option<LargeToolOutputConfig>,
3191 pub tool_search: Option<ToolSearchConfig>,
3194 pub disabled_skills: Option<Vec<String>>,
3196 pub disabled_mcp_servers: Option<Vec<String>>,
3199 pub hooks: Option<bool>,
3201 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3203 pub default_agent: Option<DefaultAgentConfig>,
3205 pub agent: Option<String>,
3207 pub infinite_sessions: Option<InfiniteSessionConfig>,
3209 pub provider: Option<ProviderConfig>,
3211 pub capi: Option<CapiSessionOptions>,
3217 pub providers: Option<Vec<NamedProviderConfig>>,
3223 pub models: Option<Vec<ProviderModelConfig>>,
3229 pub enable_session_telemetry: Option<bool>,
3237 pub enable_citations: Option<bool>,
3239 pub session_limits: Option<SessionLimitsConfig>,
3241 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3243 pub memory: Option<MemoryConfiguration>,
3245 pub config_directory: Option<PathBuf>,
3247 pub working_directory: Option<PathBuf>,
3249 pub additional_directories: Option<Vec<PathBuf>>,
3252 pub github_token: Option<String>,
3255 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3258 pub include_sub_agent_streaming_events: Option<bool>,
3260 pub commands: Option<Vec<CommandDefinition>>,
3264 #[doc(hidden)]
3269 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3270 pub enable_managed_settings: Option<bool>,
3276 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3281 pub suppress_resume_event: Option<bool>,
3284 pub continue_pending_work: Option<bool>,
3292 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3295 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3298 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3300 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3303 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3306 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3309 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3311 pub(crate) permission_policy: Option<crate::permission::Policy>,
3313 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3315 pub skip_custom_instructions: Option<bool>,
3317 pub custom_agents_local_only: Option<bool>,
3319 pub enable_experimental_mode: Option<bool>,
3324 pub coauthor_enabled: Option<bool>,
3326 pub manage_schedule_enabled: Option<bool>,
3328}
3329
3330impl std::fmt::Debug for ResumeSessionConfig {
3331 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3332 f.debug_struct("ResumeSessionConfig")
3333 .field("session_id", &self.session_id)
3334 .field("model", &self.model)
3335 .field("client_name", &self.client_name)
3336 .field("reasoning_effort", &self.reasoning_effort)
3337 .field("reasoning_summary", &self.reasoning_summary)
3338 .field("context_tier", &self.context_tier)
3339 .field("streaming", &self.streaming)
3340 .field("system_message", &self.system_message)
3341 .field("tools", &self.tools)
3342 .field("canvases", &self.canvases)
3343 .field(
3344 "canvas_handler",
3345 &self.canvas_handler.as_ref().map(|_| "<set>"),
3346 )
3347 .field("open_canvases", &self.open_canvases)
3348 .field("request_canvas_renderer", &self.request_canvas_renderer)
3349 .field("request_extensions", &self.request_extensions)
3350 .field("extension_sdk_path", &self.extension_sdk_path)
3351 .field("extension_info", &self.extension_info)
3352 .field("canvas_provider", &self.canvas_provider)
3353 .field("available_tools", &self.available_tools)
3354 .field("excluded_tools", &self.excluded_tools)
3355 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3356 .field("mcp_servers", &self.mcp_servers)
3357 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3358 .field("embedding_cache_storage", &self.embedding_cache_storage)
3359 .field("enable_config_discovery", &self.enable_config_discovery)
3360 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3361 .field(
3362 "organization_custom_instructions",
3363 &self
3364 .organization_custom_instructions
3365 .as_ref()
3366 .map(|_| "<redacted>"),
3367 )
3368 .field(
3369 "enable_on_demand_instruction_discovery",
3370 &self.enable_on_demand_instruction_discovery,
3371 )
3372 .field("enable_file_hooks", &self.enable_file_hooks)
3373 .field(
3374 "enable_host_git_operations",
3375 &self.enable_host_git_operations,
3376 )
3377 .field("enable_session_store", &self.enable_session_store)
3378 .field("enable_skills", &self.enable_skills)
3379 .field("enable_mcp_apps", &self.enable_mcp_apps)
3380 .field("skill_directories", &self.skill_directories)
3381 .field("instruction_directories", &self.instruction_directories)
3382 .field("plugin_directories", &self.plugin_directories)
3383 .field("large_output", &self.large_output)
3384 .field("tool_search", &self.tool_search)
3385 .field("disabled_skills", &self.disabled_skills)
3386 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
3387 .field("hooks", &self.hooks)
3388 .field("custom_agents", &self.custom_agents)
3389 .field("default_agent", &self.default_agent)
3390 .field("agent", &self.agent)
3391 .field("infinite_sessions", &self.infinite_sessions)
3392 .field("provider", &self.provider)
3393 .field("capi", &self.capi)
3394 .field("enable_session_telemetry", &self.enable_session_telemetry)
3395 .field("enable_citations", &self.enable_citations)
3396 .field("session_limits", &self.session_limits)
3397 .field("model_capabilities", &self.model_capabilities)
3398 .field("memory", &self.memory)
3399 .field("config_directory", &self.config_directory)
3400 .field("working_directory", &self.working_directory)
3401 .field("additional_directories", &self.additional_directories)
3402 .field(
3403 "github_token",
3404 &self.github_token.as_ref().map(|_| "<redacted>"),
3405 )
3406 .field("remote_session", &self.remote_session)
3407 .field(
3408 "include_sub_agent_streaming_events",
3409 &self.include_sub_agent_streaming_events,
3410 )
3411 .field("commands", &self.commands)
3412 .field("exp_assignments", &self.exp_assignments)
3413 .field("enable_managed_settings", &self.enable_managed_settings)
3414 .field("enable_experimental_mode", &self.enable_experimental_mode)
3415 .field(
3416 "session_fs_provider",
3417 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3418 )
3419 .field(
3420 "permission_handler",
3421 &self.permission_handler.as_ref().map(|_| "<set>"),
3422 )
3423 .field(
3424 "elicitation_handler",
3425 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3426 )
3427 .field(
3428 "user_input_handler",
3429 &self.user_input_handler.as_ref().map(|_| "<set>"),
3430 )
3431 .field(
3432 "exit_plan_mode_handler",
3433 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3434 )
3435 .field(
3436 "auto_mode_switch_handler",
3437 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3438 )
3439 .field(
3440 "hooks_handler",
3441 &self.hooks_handler.as_ref().map(|_| "<set>"),
3442 )
3443 .field(
3444 "system_message_transform",
3445 &self.system_message_transform.as_ref().map(|_| "<set>"),
3446 )
3447 .field("suppress_resume_event", &self.suppress_resume_event)
3448 .field("continue_pending_work", &self.continue_pending_work)
3449 .finish()
3450 }
3451}
3452
3453impl ResumeSessionConfig {
3454 pub(crate) fn into_wire(
3462 mut self,
3463 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3464 let permission_active =
3465 self.permission_handler.is_some() || self.permission_policy.is_some();
3466 let request_user_input = self.user_input_handler.is_some();
3467 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3468 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3469 let request_elicitation = self.elicitation_handler.is_some();
3470 let hooks_flag = self.hooks_handler.is_some();
3471
3472 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3473 if let Some(tools) = self.tools.as_mut() {
3474 for tool in tools.iter_mut() {
3475 if let Some(handler) = tool.handler.take()
3476 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3477 {
3478 return Err(crate::Error::with_message(
3479 crate::ErrorKind::InvalidConfig,
3480 format!("duplicate tool handler registered for name {:?}", tool.name),
3481 ));
3482 }
3483 }
3484 }
3485
3486 let wire_commands = self.commands.as_ref().map(|cmds| {
3487 cmds.iter()
3488 .map(|c| crate::wire::CommandWireDefinition {
3489 name: c.name.clone(),
3490 description: c.description.clone(),
3491 })
3492 .collect()
3493 });
3494 let wire_canvases = self.canvases.clone();
3495 let canvas_handler = self.canvas_handler.clone();
3496 let bearer_token_providers =
3497 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3498
3499 let wire = crate::wire::SessionResumeWire {
3500 session_id: self.session_id,
3501 model: self.model,
3502 client_name: self.client_name,
3503 reasoning_effort: self.reasoning_effort,
3504 reasoning_summary: self.reasoning_summary,
3505 context_tier: self.context_tier,
3506 streaming: self.streaming,
3507 system_message: self.system_message,
3508 tools: self.tools,
3509 canvases: wire_canvases,
3510 open_canvases: self.open_canvases,
3511 request_canvas_renderer: self.request_canvas_renderer,
3512 request_extensions: self.request_extensions,
3513 extension_sdk_path: self.extension_sdk_path,
3514 extension_info: self.extension_info,
3515 canvas_provider: self.canvas_provider,
3516 available_tools: self.available_tools,
3517 excluded_tools: self.excluded_tools,
3518 excluded_builtin_agents: self.excluded_builtin_agents,
3519 tool_filter_precedence: "excluded",
3520 mcp_servers: self.mcp_servers,
3521 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3522 embedding_cache_storage: self.embedding_cache_storage,
3523 env_value_mode: "direct",
3524 enable_config_discovery: self.enable_config_discovery,
3525 skip_embedding_retrieval: self.skip_embedding_retrieval,
3526 organization_custom_instructions: self.organization_custom_instructions,
3527 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3528 enable_file_hooks: self.enable_file_hooks,
3529 enable_host_git_operations: self.enable_host_git_operations,
3530 enable_session_store: self.enable_session_store,
3531 enable_skills: self.enable_skills,
3532 request_user_input,
3533 request_permission: permission_active,
3534 request_exit_plan_mode,
3535 request_auto_mode_switch,
3536 request_elicitation,
3537 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3538 github_mcp_tool_config: self.github_mcp_tool_config,
3539 hooks: hooks_flag,
3540 skill_directories: self.skill_directories,
3541 instruction_directories: self.instruction_directories,
3542 plugin_directories: self.plugin_directories,
3543 large_output: self.large_output,
3544 tool_search: self.tool_search,
3545 disabled_skills: self.disabled_skills,
3546 disabled_mcp_servers: self.disabled_mcp_servers,
3547 custom_agents: self.custom_agents,
3548 custom_agents_local_only: self.custom_agents_local_only,
3549 default_agent: self.default_agent,
3550 agent: self.agent,
3551 infinite_sessions: self.infinite_sessions,
3552 provider: self.provider,
3553 capi: self.capi,
3554 providers: self.providers,
3555 models: self.models,
3556 enable_session_telemetry: self.enable_session_telemetry,
3557 enable_citations: self.enable_citations,
3558 session_limits: self.session_limits,
3559 model_capabilities: self.model_capabilities,
3560 memory: self.memory,
3561 config_dir: self.config_directory,
3562 working_directory: self.working_directory,
3563 additional_directories: self.additional_directories,
3564 github_token: self.github_token,
3565 remote_session: self.remote_session,
3566 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3567 enable_github_telemetry_forwarding: None,
3568 commands: wire_commands,
3569 exp_assignments: self.exp_assignments,
3570 enable_managed_settings: self.enable_managed_settings,
3571 is_experimental_mode: self.enable_experimental_mode,
3572 suppress_resume_event: self.suppress_resume_event,
3573 continue_pending_work: self.continue_pending_work,
3574 };
3575
3576 let runtime = SessionConfigRuntime {
3577 permission_handler: self.permission_handler,
3578 permission_policy: self.permission_policy,
3579 elicitation_handler: self.elicitation_handler,
3580 mcp_auth_handler: self.mcp_auth_handler,
3581 user_input_handler: self.user_input_handler,
3582 exit_plan_mode_handler: self.exit_plan_mode_handler,
3583 auto_mode_switch_handler: self.auto_mode_switch_handler,
3584 hooks_handler: self.hooks_handler,
3585 system_message_transform: self.system_message_transform,
3586 tool_handlers,
3587 canvas_handler,
3588 session_fs_provider: self.session_fs_provider,
3589 bearer_token_providers,
3590 commands: self.commands,
3591 };
3592
3593 Ok((wire, runtime))
3594 }
3595
3596 pub fn new(session_id: SessionId) -> Self {
3601 Self {
3602 session_id,
3603 model: None,
3604 client_name: None,
3605 reasoning_effort: None,
3606 reasoning_summary: None,
3607 context_tier: None,
3608 streaming: None,
3609 system_message: None,
3610 tools: None,
3611 canvases: None,
3612 canvas_handler: None,
3613 open_canvases: None,
3614 request_canvas_renderer: None,
3615 request_extensions: None,
3616 extension_sdk_path: None,
3617 extension_info: None,
3618 canvas_provider: None,
3619 available_tools: None,
3620 excluded_tools: None,
3621 excluded_builtin_agents: None,
3622 mcp_servers: None,
3623 mcp_oauth_token_storage: None,
3624 enable_config_discovery: None,
3625 skip_embedding_retrieval: None,
3626 organization_custom_instructions: None,
3627 enable_on_demand_instruction_discovery: None,
3628 enable_file_hooks: None,
3629 enable_host_git_operations: None,
3630 enable_session_store: None,
3631 enable_skills: None,
3632 embedding_cache_storage: None,
3633 enable_mcp_apps: None,
3634 github_mcp_tool_config: None,
3635 skill_directories: None,
3636 instruction_directories: None,
3637 plugin_directories: None,
3638 large_output: None,
3639 tool_search: None,
3640 disabled_skills: None,
3641 disabled_mcp_servers: None,
3642 hooks: None,
3643 custom_agents: None,
3644 default_agent: None,
3645 agent: None,
3646 infinite_sessions: None,
3647 provider: None,
3648 capi: None,
3649 providers: None,
3650 models: None,
3651 enable_session_telemetry: None,
3652 enable_citations: None,
3653 session_limits: None,
3654 model_capabilities: None,
3655 memory: None,
3656 config_directory: None,
3657 working_directory: None,
3658 additional_directories: None,
3659 github_token: None,
3660 remote_session: None,
3661 include_sub_agent_streaming_events: None,
3662 commands: None,
3663 exp_assignments: None,
3664 enable_managed_settings: None,
3665 session_fs_provider: None,
3666 suppress_resume_event: None,
3667 continue_pending_work: None,
3668 permission_handler: None,
3669 elicitation_handler: None,
3670 mcp_auth_handler: None,
3671 user_input_handler: None,
3672 exit_plan_mode_handler: None,
3673 auto_mode_switch_handler: None,
3674 hooks_handler: None,
3675 permission_policy: None,
3676 system_message_transform: None,
3677 skip_custom_instructions: None,
3678 custom_agents_local_only: None,
3679 enable_experimental_mode: None,
3680 coauthor_enabled: None,
3681 manage_schedule_enabled: None,
3682 }
3683 }
3684
3685 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3687 self.permission_handler = Some(handler);
3688 self
3689 }
3690
3691 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3693 self.elicitation_handler = Some(handler);
3694 self
3695 }
3696
3697 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3699 self.mcp_auth_handler = Some(handler);
3700 self
3701 }
3702
3703 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3705 self.user_input_handler = Some(handler);
3706 self
3707 }
3708
3709 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3711 self.exit_plan_mode_handler = Some(handler);
3712 self
3713 }
3714
3715 pub fn with_auto_mode_switch_handler(
3717 mut self,
3718 handler: Arc<dyn AutoModeSwitchHandler>,
3719 ) -> Self {
3720 self.auto_mode_switch_handler = Some(handler);
3721 self
3722 }
3723
3724 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3727 self.hooks_handler = Some(hooks);
3728 self
3729 }
3730
3731 pub fn with_system_message_transform(
3733 mut self,
3734 transform: Arc<dyn SystemMessageTransform>,
3735 ) -> Self {
3736 self.system_message_transform = Some(transform);
3737 self
3738 }
3739
3740 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3744 self.commands = Some(commands);
3745 self
3746 }
3747
3748 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3751 self.session_fs_provider = Some(provider);
3752 self
3753 }
3754
3755 pub fn approve_all_permissions(mut self) -> Self {
3758 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3759 self
3760 }
3761
3762 pub fn deny_all_permissions(mut self) -> Self {
3765 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3766 self
3767 }
3768
3769 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3772 where
3773 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3774 {
3775 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3776 self
3777 }
3778
3779 pub fn with_model(mut self, model: impl Into<String>) -> Self {
3781 self.model = Some(model.into());
3782 self
3783 }
3784
3785 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3787 self.client_name = Some(name.into());
3788 self
3789 }
3790
3791 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3793 self.reasoning_effort = Some(effort.into());
3794 self
3795 }
3796
3797 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3799 self.reasoning_summary = Some(summary);
3800 self
3801 }
3802
3803 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3806 self.context_tier = Some(tier.into());
3807 self
3808 }
3809
3810 pub fn with_streaming(mut self, streaming: bool) -> Self {
3812 self.streaming = Some(streaming);
3813 self
3814 }
3815
3816 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3819 self.system_message = Some(system_message);
3820 self
3821 }
3822
3823 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3825 self.tools = Some(tools.into_iter().collect());
3826 self
3827 }
3828
3829 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3831 self.canvases = Some(canvases.into_iter().collect());
3832 self
3833 }
3834
3835 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
3837 self.canvas_handler = Some(handler);
3838 self
3839 }
3840
3841 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
3843 mut self,
3844 open_canvases: I,
3845 ) -> Self {
3846 self.open_canvases = Some(open_canvases.into_iter().collect());
3847 self
3848 }
3849
3850 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
3852 self.request_canvas_renderer = Some(request);
3853 self
3854 }
3855
3856 pub fn with_request_extensions(mut self, request: bool) -> Self {
3858 self.request_extensions = Some(request);
3859 self
3860 }
3861
3862 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
3866 self.extension_sdk_path = Some(path.into());
3867 self
3868 }
3869
3870 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
3872 self.extension_info = Some(extension_info);
3873 self
3874 }
3875
3876 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
3879 self.canvas_provider = Some(canvas_provider);
3880 self
3881 }
3882
3883 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
3885 where
3886 I: IntoIterator<Item = S>,
3887 S: Into<String>,
3888 {
3889 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
3890 self
3891 }
3892
3893 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
3895 where
3896 I: IntoIterator<Item = S>,
3897 S: Into<String>,
3898 {
3899 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
3900 self
3901 }
3902
3903 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
3905 where
3906 I: IntoIterator<Item = S>,
3907 S: Into<String>,
3908 {
3909 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
3910 self
3911 }
3912
3913 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
3915 self.mcp_servers = Some(servers);
3916 self
3917 }
3918
3919 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3922 self.mcp_oauth_token_storage = Some(mode.into());
3923 self
3924 }
3925
3926 pub fn with_embedding_cache_storage(
3928 mut self,
3929 embedding_cache_storage: impl Into<String>,
3930 ) -> Self {
3931 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3932 self
3933 }
3934
3935 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3938 self.enable_config_discovery = Some(enable);
3939 self
3940 }
3941
3942 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3944 self.skip_embedding_retrieval = Some(value);
3945 self
3946 }
3947
3948 pub fn with_organization_custom_instructions(
3950 mut self,
3951 instructions: impl Into<String>,
3952 ) -> Self {
3953 self.organization_custom_instructions = Some(instructions.into());
3954 self
3955 }
3956
3957 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3959 self.enable_on_demand_instruction_discovery = Some(value);
3960 self
3961 }
3962
3963 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3965 self.enable_file_hooks = Some(value);
3966 self
3967 }
3968
3969 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3971 self.enable_host_git_operations = Some(value);
3972 self
3973 }
3974
3975 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3977 self.enable_session_store = Some(value);
3978 self
3979 }
3980
3981 pub fn with_enable_skills(mut self, value: bool) -> Self {
3983 self.enable_skills = Some(value);
3984 self
3985 }
3986
3987 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3993 self.enable_mcp_apps = Some(enable);
3994 self
3995 }
3996
3997 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
3999 self.github_mcp_tool_config = Some(config);
4000 self
4001 }
4002
4003 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
4005 where
4006 I: IntoIterator<Item = P>,
4007 P: Into<PathBuf>,
4008 {
4009 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
4010 self
4011 }
4012
4013 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
4017 where
4018 I: IntoIterator<Item = P>,
4019 P: Into<PathBuf>,
4020 {
4021 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
4022 self
4023 }
4024
4025 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
4027 where
4028 I: IntoIterator<Item = P>,
4029 P: Into<PathBuf>,
4030 {
4031 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
4032 self
4033 }
4034
4035 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
4037 self.large_output = Some(config);
4038 self
4039 }
4040
4041 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
4044 self.tool_search = Some(config);
4045 self
4046 }
4047
4048 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
4050 where
4051 I: IntoIterator<Item = S>,
4052 S: Into<String>,
4053 {
4054 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
4055 self
4056 }
4057
4058 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
4060 where
4061 I: IntoIterator<Item = S>,
4062 S: Into<String>,
4063 {
4064 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
4065 self
4066 }
4067
4068 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
4070 mut self,
4071 agents: I,
4072 ) -> Self {
4073 self.custom_agents = Some(agents.into_iter().collect());
4074 self
4075 }
4076
4077 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
4079 self.default_agent = Some(agent);
4080 self
4081 }
4082
4083 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
4085 self.agent = Some(name.into());
4086 self
4087 }
4088
4089 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
4091 self.infinite_sessions = Some(config);
4092 self
4093 }
4094
4095 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
4097 self.provider = Some(provider);
4098 self
4099 }
4100
4101 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
4103 self.capi = Some(capi);
4104 self
4105 }
4106
4107 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
4113 self.providers = Some(providers);
4114 self
4115 }
4116
4117 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
4123 self.models = Some(models);
4124 self
4125 }
4126
4127 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
4131 self.enable_session_telemetry = Some(enable);
4132 self
4133 }
4134
4135 pub fn with_enable_citations(mut self, enable: bool) -> Self {
4137 self.enable_citations = Some(enable);
4138 self
4139 }
4140
4141 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
4143 self.session_limits = Some(limits);
4144 self
4145 }
4146
4147 pub fn with_model_capabilities(
4149 mut self,
4150 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
4151 ) -> Self {
4152 self.model_capabilities = Some(capabilities);
4153 self
4154 }
4155
4156 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
4158 self.memory = Some(memory);
4159 self
4160 }
4161
4162 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4164 self.config_directory = Some(dir.into());
4165 self
4166 }
4167
4168 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4170 self.working_directory = Some(dir.into());
4171 self
4172 }
4173
4174 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
4176 where
4177 I: IntoIterator<Item = P>,
4178 P: Into<PathBuf>,
4179 {
4180 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
4181 self
4182 }
4183
4184 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
4188 self.github_token = Some(token.into());
4189 self
4190 }
4191
4192 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4194 self.include_sub_agent_streaming_events = Some(include);
4195 self
4196 }
4197
4198 pub fn with_remote_session(
4200 mut self,
4201 mode: crate::generated::api_types::RemoteSessionMode,
4202 ) -> Self {
4203 self.remote_session = Some(mode);
4204 self
4205 }
4206
4207 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4210 self.suppress_resume_event = Some(suppress);
4211 self
4212 }
4213
4214 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4220 self.continue_pending_work = Some(continue_pending);
4221 self
4222 }
4223
4224 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4226 self.skip_custom_instructions = Some(value);
4227 self
4228 }
4229
4230 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4232 self.custom_agents_local_only = Some(value);
4233 self
4234 }
4235
4236 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
4238 self.enable_experimental_mode = Some(enable_experimental_mode);
4239 self
4240 }
4241
4242 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4244 self.coauthor_enabled = Some(value);
4245 self
4246 }
4247
4248 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4250 self.manage_schedule_enabled = Some(value);
4251 self
4252 }
4253
4254 #[doc(hidden)]
4258 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4259 self.exp_assignments = Some(assignments);
4260 self
4261 }
4262
4263 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4266 self.enable_managed_settings = Some(enabled);
4267 self
4268 }
4269}
4270
4271#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4277#[serde(rename_all = "camelCase")]
4278#[non_exhaustive]
4279pub struct SystemMessageConfig {
4280 #[serde(skip_serializing_if = "Option::is_none")]
4282 pub mode: Option<String>,
4283 #[serde(skip_serializing_if = "Option::is_none")]
4285 pub content: Option<String>,
4286 #[serde(skip_serializing_if = "Option::is_none")]
4288 pub sections: Option<HashMap<String, SectionOverride>>,
4289}
4290
4291impl SystemMessageConfig {
4292 pub fn new() -> Self {
4295 Self::default()
4296 }
4297
4298 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4301 self.mode = Some(mode.into());
4302 self
4303 }
4304
4305 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4308 self.content = Some(content.into());
4309 self
4310 }
4311
4312 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4314 self.sections = Some(sections);
4315 self
4316 }
4317}
4318
4319#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4325#[serde(rename_all = "camelCase")]
4326pub struct SectionOverride {
4327 #[serde(skip_serializing_if = "Option::is_none")]
4330 pub action: Option<String>,
4331 #[serde(skip_serializing_if = "Option::is_none")]
4333 pub content: Option<String>,
4334}
4335
4336#[derive(Debug, Clone, Serialize, Deserialize)]
4338#[serde(rename_all = "camelCase")]
4339pub struct CreateSessionResult {
4340 pub session_id: SessionId,
4342 #[serde(skip_serializing_if = "Option::is_none")]
4344 pub workspace_path: Option<PathBuf>,
4345 #[serde(default, alias = "remote_url")]
4347 pub remote_url: Option<String>,
4348 #[serde(skip_serializing_if = "Option::is_none")]
4350 pub capabilities: Option<SessionCapabilities>,
4351}
4352
4353#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4355#[serde(rename_all = "camelCase")]
4356pub(crate) struct ResumeSessionResult {
4357 #[serde(default)]
4359 pub session_id: Option<SessionId>,
4360 #[serde(default, skip_serializing_if = "Option::is_none")]
4362 pub workspace_path: Option<PathBuf>,
4363 #[serde(default, alias = "remote_url")]
4365 pub remote_url: Option<String>,
4366 #[serde(default, skip_serializing_if = "Option::is_none")]
4368 pub capabilities: Option<SessionCapabilities>,
4369 #[serde(
4371 default,
4372 alias = "openCanvasInstances",
4373 skip_serializing_if = "Option::is_none"
4374 )]
4375 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4376}
4377
4378#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4380#[serde(rename_all = "lowercase")]
4381pub enum LogLevel {
4382 #[default]
4384 Info,
4385 Warning,
4387 Error,
4389}
4390
4391#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4396#[serde(rename_all = "camelCase")]
4397pub struct LogOptions {
4398 #[serde(skip_serializing_if = "Option::is_none")]
4400 pub level: Option<LogLevel>,
4401 #[serde(skip_serializing_if = "Option::is_none")]
4404 pub ephemeral: Option<bool>,
4405}
4406
4407impl LogOptions {
4408 pub fn with_level(mut self, level: LogLevel) -> Self {
4410 self.level = Some(level);
4411 self
4412 }
4413
4414 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4416 self.ephemeral = Some(ephemeral);
4417 self
4418 }
4419}
4420
4421#[derive(Debug, Clone, Default)]
4425pub struct SetModelOptions {
4426 pub reasoning_effort: Option<String>,
4429 pub reasoning_summary: Option<ReasoningSummary>,
4433 pub context_tier: Option<ContextTier>,
4436 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4440}
4441
4442impl SetModelOptions {
4443 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4445 self.reasoning_effort = Some(effort.into());
4446 self
4447 }
4448
4449 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4451 self.reasoning_summary = Some(summary);
4452 self
4453 }
4454
4455 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4457 self.context_tier = Some(tier);
4458 self
4459 }
4460
4461 pub fn with_model_capabilities(
4463 mut self,
4464 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4465 ) -> Self {
4466 self.model_capabilities = Some(caps);
4467 self
4468 }
4469}
4470
4471#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4478#[serde(rename_all = "camelCase")]
4479pub struct PingResponse {
4480 #[serde(default)]
4482 pub message: String,
4483 #[serde(default)]
4485 pub timestamp: String,
4486 #[serde(skip_serializing_if = "Option::is_none")]
4488 pub protocol_version: Option<u32>,
4489}
4490
4491#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4493#[serde(rename_all = "camelCase")]
4494pub struct AttachmentLineRange {
4495 pub start: u32,
4497 pub end: u32,
4499}
4500
4501#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4503#[serde(rename_all = "camelCase")]
4504pub struct AttachmentSelectionPosition {
4505 pub line: u32,
4507 pub character: u32,
4509}
4510
4511#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4513#[serde(rename_all = "camelCase")]
4514pub struct AttachmentSelectionRange {
4515 pub start: AttachmentSelectionPosition,
4517 pub end: AttachmentSelectionPosition,
4519}
4520
4521#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4523#[serde(rename_all = "snake_case")]
4524#[non_exhaustive]
4525pub enum GitHubReferenceType {
4526 Issue,
4528 Pr,
4530 Discussion,
4532}
4533
4534#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4540#[serde(rename_all = "camelCase")]
4541pub struct GitHubRepoPointer {
4542 #[serde(skip_serializing_if = "Option::is_none")]
4544 pub id: Option<i64>,
4545 pub name: String,
4547 pub owner: String,
4549}
4550
4551#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4553#[serde(rename_all = "camelCase")]
4554pub struct GitHubFileDiffSide {
4555 pub path: String,
4557 pub r#ref: String,
4559 pub repo: GitHubRepoPointer,
4561}
4562
4563#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4565#[serde(rename_all = "camelCase")]
4566pub struct GitHubTreeComparisonSide {
4567 pub repo: GitHubRepoPointer,
4569 pub revision: String,
4571}
4572
4573#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4575#[serde(rename_all = "camelCase")]
4576pub struct GitHubSnippetLineRange {
4577 pub start: i64,
4579 pub end: i64,
4581}
4582
4583#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4585#[serde(
4586 tag = "type",
4587 rename_all = "camelCase",
4588 rename_all_fields = "camelCase"
4589)]
4590#[non_exhaustive]
4591pub enum Attachment {
4592 File {
4594 path: PathBuf,
4596 #[serde(skip_serializing_if = "Option::is_none")]
4598 display_name: Option<String>,
4599 #[serde(skip_serializing_if = "Option::is_none")]
4601 line_range: Option<AttachmentLineRange>,
4602 },
4603 Directory {
4605 path: PathBuf,
4607 #[serde(skip_serializing_if = "Option::is_none")]
4609 display_name: Option<String>,
4610 },
4611 Selection {
4613 file_path: PathBuf,
4615 text: String,
4617 #[serde(skip_serializing_if = "Option::is_none")]
4619 display_name: Option<String>,
4620 selection: AttachmentSelectionRange,
4622 },
4623 Blob {
4625 data: String,
4627 mime_type: String,
4629 #[serde(skip_serializing_if = "Option::is_none")]
4631 display_name: Option<String>,
4632 },
4633 #[serde(rename = "github_reference")]
4635 GitHubReference {
4636 number: u64,
4638 title: String,
4640 reference_type: GitHubReferenceType,
4642 state: String,
4644 url: String,
4646 },
4647 #[serde(rename = "github_commit")]
4649 GitHubCommit {
4650 message: String,
4652 oid: String,
4654 repo: GitHubRepoPointer,
4656 url: String,
4658 },
4659 #[serde(rename = "github_release")]
4661 GitHubRelease {
4662 name: String,
4664 repo: GitHubRepoPointer,
4666 tag_name: String,
4668 url: String,
4670 },
4671 #[serde(rename = "github_actions_job")]
4673 GitHubActionsJob {
4674 #[serde(skip_serializing_if = "Option::is_none")]
4677 conclusion: Option<String>,
4678 job_id: i64,
4680 job_name: String,
4682 repo: GitHubRepoPointer,
4684 url: String,
4686 workflow_name: String,
4688 },
4689 #[serde(rename = "github_repository")]
4691 GitHubRepository {
4692 #[serde(skip_serializing_if = "Option::is_none")]
4694 description: Option<String>,
4695 #[serde(skip_serializing_if = "Option::is_none")]
4698 r#ref: Option<String>,
4699 repo: GitHubRepoPointer,
4701 url: String,
4703 },
4704 #[serde(rename = "github_file_diff")]
4706 GitHubFileDiff {
4707 #[serde(skip_serializing_if = "Option::is_none")]
4709 base: Option<GitHubFileDiffSide>,
4710 #[serde(skip_serializing_if = "Option::is_none")]
4712 head: Option<GitHubFileDiffSide>,
4713 url: String,
4715 },
4716 #[serde(rename = "github_tree_comparison")]
4718 GitHubTreeComparison {
4719 base: GitHubTreeComparisonSide,
4721 head: GitHubTreeComparisonSide,
4723 url: String,
4725 },
4726 #[serde(rename = "github_url")]
4728 GitHubUrl {
4729 url: String,
4731 },
4732 #[serde(rename = "github_file")]
4734 GitHubFile {
4735 path: String,
4737 r#ref: String,
4739 repo: GitHubRepoPointer,
4741 url: String,
4743 },
4744 #[serde(rename = "github_snippet")]
4746 GitHubSnippet {
4747 line_range: GitHubSnippetLineRange,
4749 path: String,
4751 r#ref: String,
4753 repo: GitHubRepoPointer,
4755 url: String,
4757 },
4758}
4759
4760impl Attachment {
4761 pub fn display_name(&self) -> Option<&str> {
4763 match self {
4764 Self::File { display_name, .. }
4765 | Self::Directory { display_name, .. }
4766 | Self::Selection { display_name, .. }
4767 | Self::Blob { display_name, .. } => display_name.as_deref(),
4768 Self::GitHubReference { .. }
4769 | Self::GitHubCommit { .. }
4770 | Self::GitHubRelease { .. }
4771 | Self::GitHubActionsJob { .. }
4772 | Self::GitHubRepository { .. }
4773 | Self::GitHubFileDiff { .. }
4774 | Self::GitHubTreeComparison { .. }
4775 | Self::GitHubUrl { .. }
4776 | Self::GitHubFile { .. }
4777 | Self::GitHubSnippet { .. } => None,
4778 }
4779 }
4780
4781 pub fn label(&self) -> Option<String> {
4783 if let Some(display_name) = self
4784 .display_name()
4785 .map(str::trim)
4786 .filter(|name| !name.is_empty())
4787 {
4788 return Some(display_name.to_string());
4789 }
4790
4791 match self {
4792 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4793 format!("#{}", number)
4794 } else {
4795 title.trim().to_string()
4796 }),
4797 _ => self.derived_display_name(),
4798 }
4799 }
4800
4801 pub fn ensure_display_name(&mut self) {
4803 if self
4804 .display_name()
4805 .map(str::trim)
4806 .is_some_and(|name| !name.is_empty())
4807 {
4808 return;
4809 }
4810
4811 let Some(derived_display_name) = self.derived_display_name() else {
4812 return;
4813 };
4814
4815 match self {
4816 Self::File { display_name, .. }
4817 | Self::Directory { display_name, .. }
4818 | Self::Selection { display_name, .. }
4819 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4820 Self::GitHubReference { .. }
4821 | Self::GitHubCommit { .. }
4822 | Self::GitHubRelease { .. }
4823 | Self::GitHubActionsJob { .. }
4824 | Self::GitHubRepository { .. }
4825 | Self::GitHubFileDiff { .. }
4826 | Self::GitHubTreeComparison { .. }
4827 | Self::GitHubUrl { .. }
4828 | Self::GitHubFile { .. }
4829 | Self::GitHubSnippet { .. } => {}
4830 }
4831 }
4832
4833 fn derived_display_name(&self) -> Option<String> {
4834 match self {
4835 Self::File { path, .. } | Self::Directory { path, .. } => {
4836 Some(attachment_name_from_path(path))
4837 }
4838 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4839 Self::Blob { .. } => Some("attachment".to_string()),
4840 Self::GitHubReference { .. }
4841 | Self::GitHubCommit { .. }
4842 | Self::GitHubRelease { .. }
4843 | Self::GitHubActionsJob { .. }
4844 | Self::GitHubRepository { .. }
4845 | Self::GitHubFileDiff { .. }
4846 | Self::GitHubTreeComparison { .. }
4847 | Self::GitHubUrl { .. }
4848 | Self::GitHubFile { .. }
4849 | Self::GitHubSnippet { .. } => None,
4850 }
4851 }
4852}
4853
4854fn attachment_name_from_path(path: &Path) -> String {
4855 path.file_name()
4856 .map(|name| name.to_string_lossy().into_owned())
4857 .filter(|name| !name.is_empty())
4858 .unwrap_or_else(|| {
4859 let full = path.to_string_lossy();
4860 if full.is_empty() {
4861 "attachment".to_string()
4862 } else {
4863 full.into_owned()
4864 }
4865 })
4866}
4867
4868pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
4870 for attachment in attachments {
4871 attachment.ensure_display_name();
4872 }
4873}
4874
4875#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4880#[serde(rename_all = "lowercase")]
4881#[non_exhaustive]
4882pub enum DeliveryMode {
4883 Enqueue,
4885 Immediate,
4887}
4888
4889#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4894#[serde(rename_all = "lowercase")]
4895#[non_exhaustive]
4896pub enum AgentMode {
4897 Interactive,
4899 Plan,
4901 Autopilot,
4903 Shell,
4905}
4906
4907#[derive(Debug, Clone)]
4936#[non_exhaustive]
4937pub struct MessageOptions {
4938 pub prompt: String,
4940 pub mode: Option<DeliveryMode>,
4946 pub agent_mode: Option<AgentMode>,
4950 pub attachments: Option<Vec<Attachment>>,
4952 pub wait_timeout: Option<Duration>,
4955 pub request_headers: Option<HashMap<String, String>>,
4959 pub traceparent: Option<String>,
4966 pub tracestate: Option<String>,
4970 pub display_prompt: Option<String>,
4972}
4973
4974impl MessageOptions {
4975 pub fn new(prompt: impl Into<String>) -> Self {
4977 Self {
4978 prompt: prompt.into(),
4979 mode: None,
4980 agent_mode: None,
4981 attachments: None,
4982 wait_timeout: None,
4983 request_headers: None,
4984 traceparent: None,
4985 tracestate: None,
4986 display_prompt: None,
4987 }
4988 }
4989
4990 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
4996 self.mode = Some(mode);
4997 self
4998 }
4999
5000 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
5004 self.agent_mode = Some(agent_mode);
5005 self
5006 }
5007
5008 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
5010 self.attachments = Some(attachments);
5011 self
5012 }
5013
5014 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
5016 self.wait_timeout = Some(timeout);
5017 self
5018 }
5019
5020 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
5022 self.request_headers = Some(headers);
5023 self
5024 }
5025
5026 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
5031 self.traceparent = ctx.traceparent;
5032 self.tracestate = ctx.tracestate;
5033 self
5034 }
5035
5036 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5038 self.traceparent = Some(traceparent.into());
5039 self
5040 }
5041
5042 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5044 self.tracestate = Some(tracestate.into());
5045 self
5046 }
5047
5048 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5050 self.display_prompt = Some(display_prompt.into());
5051 self
5052 }
5053}
5054
5055impl From<&str> for MessageOptions {
5056 fn from(prompt: &str) -> Self {
5057 Self::new(prompt)
5058 }
5059}
5060
5061impl From<String> for MessageOptions {
5062 fn from(prompt: String) -> Self {
5063 Self::new(prompt)
5064 }
5065}
5066
5067impl From<&String> for MessageOptions {
5068 fn from(prompt: &String) -> Self {
5069 Self::new(prompt.clone())
5070 }
5071}
5072
5073#[derive(Debug, Clone, Serialize, Deserialize)]
5075#[serde(rename_all = "camelCase")]
5076#[non_exhaustive]
5077pub struct GetStatusResponse {
5078 pub version: String,
5080 pub protocol_version: u32,
5082}
5083
5084#[derive(Debug, Clone, Serialize, Deserialize)]
5086#[serde(rename_all = "camelCase")]
5087#[non_exhaustive]
5088pub struct GetAuthStatusResponse {
5089 pub is_authenticated: bool,
5091 #[serde(skip_serializing_if = "Option::is_none")]
5094 pub auth_type: Option<String>,
5095 #[serde(skip_serializing_if = "Option::is_none")]
5097 pub host: Option<String>,
5098 #[serde(skip_serializing_if = "Option::is_none")]
5100 pub login: Option<String>,
5101 #[serde(skip_serializing_if = "Option::is_none")]
5103 pub status_message: Option<String>,
5104}
5105
5106#[derive(Debug, Clone, Serialize, Deserialize)]
5110#[serde(rename_all = "camelCase")]
5111pub struct SessionEventNotification {
5112 pub session_id: SessionId,
5114 pub event: SessionEvent,
5116}
5117
5118#[derive(Debug, Clone, Serialize, Deserialize)]
5125#[serde(rename_all = "camelCase")]
5126pub struct SessionEvent {
5127 pub id: String,
5129 pub timestamp: String,
5131 pub parent_id: Option<String>,
5133 #[serde(skip_serializing_if = "Option::is_none")]
5135 pub ephemeral: Option<bool>,
5136 #[serde(skip_serializing_if = "Option::is_none")]
5139 pub agent_id: Option<String>,
5140 #[serde(skip_serializing_if = "Option::is_none")]
5142 pub debug_cli_received_at_ms: Option<i64>,
5143 #[serde(skip_serializing_if = "Option::is_none")]
5145 pub debug_ws_forwarded_at_ms: Option<i64>,
5146 #[serde(rename = "type")]
5148 pub event_type: String,
5149 pub data: Value,
5151}
5152
5153impl SessionEvent {
5154 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5159 use serde::de::IntoDeserializer;
5160 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5161 self.event_type.as_str().into_deserializer();
5162 crate::generated::SessionEventType::deserialize(deserializer)
5163 .unwrap_or(crate::generated::SessionEventType::Unknown)
5164 }
5165
5166 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5172 serde_json::from_value(self.data.clone()).ok()
5173 }
5174
5175 pub fn is_transient_error(&self) -> bool {
5179 self.event_type == "session.error"
5180 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5181 }
5182}
5183
5184#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5189#[serde(rename_all = "camelCase")]
5190#[non_exhaustive]
5191pub struct ToolInvocation {
5192 pub session_id: SessionId,
5194 pub tool_call_id: String,
5196 pub tool_name: String,
5198 pub arguments: Value,
5200 #[serde(skip)]
5208 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5209 #[serde(default, skip_serializing_if = "Option::is_none")]
5214 pub traceparent: Option<String>,
5215 #[serde(default, skip_serializing_if = "Option::is_none")]
5218 pub tracestate: Option<String>,
5219}
5220
5221impl ToolInvocation {
5222 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5243 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5244 }
5245
5246 pub fn trace_context(&self) -> TraceContext {
5249 TraceContext {
5250 traceparent: self.traceparent.clone(),
5251 tracestate: self.tracestate.clone(),
5252 }
5253 }
5254}
5255
5256#[derive(Debug, Clone, Serialize, Deserialize)]
5258#[serde(rename_all = "camelCase")]
5259pub struct ToolBinaryResult {
5260 pub data: String,
5262 pub mime_type: String,
5264 pub r#type: String,
5266 #[serde(default, skip_serializing_if = "Option::is_none")]
5268 pub description: Option<String>,
5269}
5270
5271#[derive(Debug, Clone, Serialize, Deserialize)]
5278#[serde(rename_all = "camelCase")]
5279#[non_exhaustive]
5280pub struct ToolResultExpanded {
5281 pub text_result_for_llm: String,
5283 pub result_type: String,
5285 #[serde(default, skip_serializing_if = "Option::is_none")]
5287 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5288 #[serde(skip_serializing_if = "Option::is_none")]
5290 pub session_log: Option<String>,
5291 #[serde(skip_serializing_if = "Option::is_none")]
5293 pub error: Option<String>,
5294 #[serde(default, skip_serializing_if = "Option::is_none")]
5296 pub tool_telemetry: Option<HashMap<String, Value>>,
5297 #[serde(default, skip_serializing_if = "Option::is_none")]
5299 pub tool_references: Option<Vec<String>>,
5300}
5301
5302impl ToolResultExpanded {
5303 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5307 Self {
5308 text_result_for_llm: text_result_for_llm.into(),
5309 result_type: result_type.into(),
5310 binary_results_for_llm: None,
5311 session_log: None,
5312 error: None,
5313 tool_telemetry: None,
5314 tool_references: None,
5315 }
5316 }
5317
5318 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5320 self.binary_results_for_llm = Some(results);
5321 self
5322 }
5323
5324 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5326 self.session_log = Some(session_log.into());
5327 self
5328 }
5329
5330 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5332 self.error = Some(error.into());
5333 self
5334 }
5335
5336 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5338 self.tool_telemetry = Some(telemetry);
5339 self
5340 }
5341
5342 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5344 where
5345 I: IntoIterator<Item = S>,
5346 S: Into<String>,
5347 {
5348 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5349 self
5350 }
5351}
5352
5353#[derive(Debug, Clone, Serialize, Deserialize)]
5355#[serde(untagged)]
5356#[non_exhaustive]
5357pub enum ToolResult {
5358 Text(String),
5360 Expanded(ToolResultExpanded),
5362}
5363
5364#[derive(Debug, Clone, Serialize, Deserialize)]
5366#[serde(rename_all = "camelCase")]
5367pub struct ToolResultResponse {
5368 pub result: ToolResult,
5370}
5371
5372#[derive(Debug, Clone, Serialize, Deserialize)]
5374#[serde(rename_all = "camelCase")]
5375pub struct SessionMetadata {
5376 pub session_id: SessionId,
5378 pub start_time: String,
5380 pub modified_time: String,
5382 #[serde(skip_serializing_if = "Option::is_none")]
5384 pub summary: Option<String>,
5385 pub is_remote: bool,
5387}
5388
5389#[derive(Debug, Clone, Serialize, Deserialize)]
5391#[serde(rename_all = "camelCase")]
5392pub struct ListSessionsResponse {
5393 pub sessions: Vec<SessionMetadata>,
5395}
5396
5397#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5401#[serde(rename_all = "camelCase")]
5402pub struct SessionListFilter {
5403 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5405 pub working_directory: Option<String>,
5406 #[serde(default, skip_serializing_if = "Option::is_none")]
5408 pub git_root: Option<String>,
5409 #[serde(default, skip_serializing_if = "Option::is_none")]
5411 pub repository: Option<String>,
5412 #[serde(default, skip_serializing_if = "Option::is_none")]
5414 pub branch: Option<String>,
5415}
5416
5417#[derive(Debug, Clone, Serialize, Deserialize)]
5419#[serde(rename_all = "camelCase")]
5420pub struct GetSessionMetadataResponse {
5421 #[serde(skip_serializing_if = "Option::is_none")]
5423 pub session: Option<SessionMetadata>,
5424}
5425
5426#[derive(Debug, Clone, Serialize, Deserialize)]
5428#[serde(rename_all = "camelCase")]
5429pub struct GetLastSessionIdResponse {
5430 #[serde(skip_serializing_if = "Option::is_none")]
5432 pub session_id: Option<SessionId>,
5433}
5434
5435#[derive(Debug, Clone, Serialize, Deserialize)]
5437#[serde(rename_all = "camelCase")]
5438pub struct GetForegroundSessionResponse {
5439 #[serde(skip_serializing_if = "Option::is_none")]
5441 pub session_id: Option<SessionId>,
5442}
5443
5444#[derive(Debug, Clone, Serialize, Deserialize)]
5446#[serde(rename_all = "camelCase")]
5447pub struct GetMessagesResponse {
5448 pub events: Vec<SessionEvent>,
5450}
5451
5452#[derive(Debug, Clone, Serialize, Deserialize)]
5454#[serde(rename_all = "camelCase")]
5455pub struct ElicitationResult {
5456 pub action: String,
5458 #[serde(skip_serializing_if = "Option::is_none")]
5460 pub content: Option<Value>,
5461}
5462
5463#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5469#[serde(rename_all = "camelCase")]
5470#[non_exhaustive]
5471pub enum ElicitationMode {
5472 Form,
5474 Url,
5476 #[serde(other)]
5478 Unknown,
5479}
5480
5481#[derive(Debug, Clone, Serialize, Deserialize)]
5488#[serde(rename_all = "camelCase")]
5489pub struct ElicitationRequest {
5490 pub message: String,
5492 #[serde(skip_serializing_if = "Option::is_none")]
5494 pub requested_schema: Option<Value>,
5495 #[serde(skip_serializing_if = "Option::is_none")]
5497 pub mode: Option<ElicitationMode>,
5498 #[serde(skip_serializing_if = "Option::is_none")]
5500 pub elicitation_source: Option<String>,
5501 #[serde(skip_serializing_if = "Option::is_none")]
5503 pub url: Option<String>,
5504}
5505
5506#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5511#[serde(rename_all = "camelCase")]
5512pub struct SessionCapabilities {
5513 #[serde(skip_serializing_if = "Option::is_none")]
5515 pub ui: Option<UiCapabilities>,
5516}
5517
5518#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5520#[serde(rename_all = "camelCase")]
5521pub struct UiCapabilities {
5522 #[serde(skip_serializing_if = "Option::is_none")]
5524 pub elicitation: Option<bool>,
5525 #[serde(skip_serializing_if = "Option::is_none")]
5536 pub mcp_apps: Option<bool>,
5537 #[serde(skip_serializing_if = "Option::is_none")]
5539 pub canvases: Option<bool>,
5540}
5541
5542#[derive(Debug, Clone, Default)]
5544pub struct UiInputOptions<'a> {
5545 pub title: Option<&'a str>,
5547 pub description: Option<&'a str>,
5549 pub min_length: Option<u64>,
5551 pub max_length: Option<u64>,
5553 pub format: Option<InputFormat>,
5555 pub default: Option<&'a str>,
5557}
5558
5559#[derive(Debug, Clone, Copy)]
5561#[non_exhaustive]
5562pub enum InputFormat {
5563 Email,
5565 Uri,
5567 Date,
5569 DateTime,
5571}
5572
5573impl InputFormat {
5574 pub fn as_str(&self) -> &'static str {
5576 match self {
5577 Self::Email => "email",
5578 Self::Uri => "uri",
5579 Self::Date => "date",
5580 Self::DateTime => "date-time",
5581 }
5582 }
5583}
5584
5585pub use crate::generated::api_types::{
5590 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5591 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5592 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5593 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
5594};
5595
5596#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5602#[serde(rename_all = "kebab-case")]
5603#[non_exhaustive]
5604pub enum PermissionRequestKind {
5605 Shell,
5607 Write,
5609 Read,
5611 Url,
5613 Mcp,
5615 CustomTool,
5617 Memory,
5619 Hook,
5621 #[serde(other)]
5624 Unknown,
5625}
5626
5627#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5633#[serde(rename_all = "camelCase")]
5634pub struct PermissionRequestData {
5635 #[serde(default, skip_serializing_if = "Option::is_none")]
5639 pub kind: Option<PermissionRequestKind>,
5640 #[serde(default, skip_serializing_if = "Option::is_none")]
5643 pub tool_call_id: Option<String>,
5644 #[serde(default, skip_serializing_if = "Option::is_none")]
5646 pub managed_approval_required: Option<bool>,
5647 #[serde(default, skip_serializing_if = "is_false")]
5649 pub managed_settings_enabled: bool,
5650 #[serde(flatten)]
5654 pub extra: Value,
5655}
5656
5657#[derive(Debug, Clone, Serialize, Deserialize)]
5659#[serde(rename_all = "camelCase")]
5660pub struct ExitPlanModeData {
5661 #[serde(default)]
5663 pub summary: String,
5664 #[serde(default, skip_serializing_if = "Option::is_none")]
5666 pub plan_content: Option<String>,
5667 #[serde(default)]
5669 pub actions: Vec<String>,
5670 #[serde(default = "default_recommended_action")]
5672 pub recommended_action: String,
5673}
5674
5675fn default_recommended_action() -> String {
5676 "autopilot".to_string()
5677}
5678
5679impl Default for ExitPlanModeData {
5680 fn default() -> Self {
5681 Self {
5682 summary: String::new(),
5683 plan_content: None,
5684 actions: Vec::new(),
5685 recommended_action: default_recommended_action(),
5686 }
5687 }
5688}
5689
5690#[cfg(test)]
5691mod tests {
5692 use std::collections::HashMap;
5693 use std::path::PathBuf;
5694
5695 use serde_json::json;
5696
5697 use super::{
5698 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5699 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5700 CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry,
5701 ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
5702 InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
5703 MemoryConfiguration, NamedProviderConfig, ProviderConfig, ProviderModelConfig,
5704 ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, SessionId,
5705 SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
5706 ToolResultResponse, ensure_attachment_display_names,
5707 };
5708 use crate::generated::session_events::TypedSessionEvent;
5709
5710 #[test]
5711 fn tool_builder_composes() {
5712 let tool = Tool::new("greet")
5713 .with_description("Say hello")
5714 .with_namespaced_name("hello/greet")
5715 .with_instructions("Pass the user's name")
5716 .with_parameters(json!({
5717 "type": "object",
5718 "properties": { "name": { "type": "string" } },
5719 "required": ["name"]
5720 }))
5721 .with_overrides_built_in_tool(true)
5722 .with_skip_permission(true);
5723 assert_eq!(tool.name, "greet");
5724 assert_eq!(tool.description, "Say hello");
5725 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5726 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5727 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5728 assert!(tool.overrides_built_in_tool);
5729 assert!(tool.skip_permission);
5730 }
5731
5732 #[test]
5733 fn tool_defer_serialization() {
5734 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5735 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5736 let value = serde_json::to_value(&tool).unwrap();
5737 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5738
5739 let plain = Tool::new("plain");
5740 let value = serde_json::to_value(&plain).unwrap();
5741 assert!(value.get("defer").is_none());
5742 }
5743
5744 #[test]
5745 fn tool_metadata_serialization() {
5746 use indexmap::IndexMap;
5747
5748 let mut metadata = IndexMap::new();
5749 metadata.insert(
5750 "github.com/copilot:safeForTelemetry".to_string(),
5751 json!({ "name": true, "inputsNames": false }),
5752 );
5753 let tool = Tool::new("lookup").with_metadata(metadata);
5754 let value = serde_json::to_value(&tool).unwrap();
5755 assert_eq!(
5756 value
5757 .get("metadata")
5758 .unwrap()
5759 .get("github.com/copilot:safeForTelemetry")
5760 .unwrap(),
5761 &json!({ "name": true, "inputsNames": false })
5762 );
5763
5764 let plain = Tool::new("plain");
5766 let value = serde_json::to_value(&plain).unwrap();
5767 assert!(value.get("metadata").is_none());
5768 }
5769
5770 #[test]
5771 fn custom_agent_config_builder_with_model() {
5772 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5773 .with_model("claude-haiku-4.5")
5774 .with_display_name("My Agent");
5775 assert_eq!(agent.name, "my-agent");
5776 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5777 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5778 }
5779
5780 #[test]
5781 fn custom_agent_config_serializes_model() {
5782 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5783 let wire = serde_json::to_value(&agent).unwrap();
5784 assert_eq!(wire["model"], "claude-haiku-4.5");
5785 assert_eq!(wire["name"], "model-agent");
5786 }
5787
5788 #[test]
5789 fn custom_agent_config_omits_model_when_none() {
5790 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5791 let wire = serde_json::to_value(&agent).unwrap();
5792 assert!(wire.get("model").is_none());
5793 }
5794
5795 #[test]
5796 fn custom_agent_config_builder_with_reasoning_effort() {
5797 let agent =
5798 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5799 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
5800 }
5801
5802 #[test]
5803 fn custom_agent_config_serializes_reasoning_effort() {
5804 let agent =
5805 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5806 let wire = serde_json::to_value(&agent).unwrap();
5807 assert_eq!(wire["reasoningEffort"], "high");
5808 }
5809
5810 #[test]
5811 fn custom_agent_config_omits_reasoning_effort_when_none() {
5812 let agent = CustomAgentConfig::new("default-agent", "prompt");
5813 let wire = serde_json::to_value(&agent).unwrap();
5814 assert!(wire.get("reasoningEffort").is_none());
5815 }
5816
5817 #[test]
5818 #[should_panic(expected = "tool parameter schema must be a JSON object")]
5819 fn tool_with_parameters_panics_on_non_object_value() {
5820 let _ = Tool::new("noop").with_parameters(json!(null));
5821 }
5822
5823 #[test]
5824 fn tool_result_expanded_serializes_binary_results_for_llm() {
5825 let response = ToolResultResponse {
5826 result: ToolResult::Expanded(ToolResultExpanded {
5827 text_result_for_llm: "rendered chart".to_string(),
5828 result_type: "success".to_string(),
5829 binary_results_for_llm: Some(vec![ToolBinaryResult {
5830 data: "aW1n".to_string(),
5831 mime_type: "image/png".to_string(),
5832 r#type: "image".to_string(),
5833 description: Some("chart preview".to_string()),
5834 }]),
5835 session_log: None,
5836 error: None,
5837 tool_telemetry: None,
5838 tool_references: None,
5839 }),
5840 };
5841
5842 let wire = serde_json::to_value(&response).unwrap();
5843
5844 assert_eq!(
5845 wire,
5846 json!({
5847 "result": {
5848 "textResultForLlm": "rendered chart",
5849 "resultType": "success",
5850 "binaryResultsForLlm": [
5851 {
5852 "data": "aW1n",
5853 "mimeType": "image/png",
5854 "type": "image",
5855 "description": "chart preview"
5856 }
5857 ]
5858 }
5859 })
5860 );
5861 }
5862
5863 #[test]
5864 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
5865 let response = ToolResultResponse {
5866 result: ToolResult::Expanded(ToolResultExpanded {
5867 text_result_for_llm: "ok".to_string(),
5868 result_type: "success".to_string(),
5869 binary_results_for_llm: None,
5870 session_log: None,
5871 error: None,
5872 tool_telemetry: None,
5873 tool_references: None,
5874 }),
5875 };
5876
5877 let wire = serde_json::to_value(&response).unwrap();
5878
5879 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5880 assert!(wire["result"].get("binaryResultsForLlm").is_none());
5881 }
5882
5883 #[test]
5884 fn tool_result_expanded_serializes_tool_references() {
5885 let response = ToolResultResponse {
5886 result: ToolResult::Expanded(
5887 ToolResultExpanded::new("found 2 tools", "success")
5888 .with_tool_references(["get_weather", "check_status"]),
5889 ),
5890 };
5891
5892 let wire = serde_json::to_value(&response).unwrap();
5893
5894 assert_eq!(
5895 wire,
5896 json!({
5897 "result": {
5898 "textResultForLlm": "found 2 tools",
5899 "resultType": "success",
5900 "toolReferences": ["get_weather", "check_status"]
5901 }
5902 })
5903 );
5904 }
5905
5906 #[test]
5907 fn tool_result_expanded_omits_tool_references_when_none() {
5908 let response = ToolResultResponse {
5909 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
5910 };
5911
5912 let wire = serde_json::to_value(&response).unwrap();
5913
5914 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5915 assert!(wire["result"].get("toolReferences").is_none());
5916 }
5917
5918 #[test]
5919 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
5920 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
5923 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
5924
5925 assert_eq!(
5926 expanded.tool_references.as_deref(),
5927 Some(["alpha".to_string(), "beta".to_string()].as_slice())
5928 );
5929 }
5930
5931 #[test]
5932 fn tool_result_expanded_deserializes_tool_references() {
5933 let wire = json!({
5934 "textResultForLlm": "found tools",
5935 "resultType": "success",
5936 "toolReferences": ["alpha", "beta"]
5937 });
5938
5939 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
5940
5941 assert_eq!(
5942 expanded.tool_references.as_deref(),
5943 Some(["alpha".to_string(), "beta".to_string()].as_slice())
5944 );
5945 }
5946
5947 #[test]
5948 fn session_config_default_wire_flags_off_without_handlers() {
5949 let cfg = SessionConfig::default();
5950 assert_eq!(cfg.mcp_oauth_token_storage, None);
5951 let (wire, _runtime) = cfg
5955 .into_wire(Some(SessionId::from("default-flags")))
5956 .expect("default config has no duplicate handlers");
5957 assert!(!wire.request_user_input);
5958 assert!(!wire.request_permission);
5959 assert!(!wire.request_elicitation);
5960 assert!(!wire.request_exit_plan_mode);
5961 assert!(!wire.request_auto_mode_switch);
5962 assert!(!wire.hooks);
5963 assert!(!wire.request_mcp_apps);
5964 }
5965
5966 #[test]
5967 fn resume_session_config_new_wire_flags_off_without_handlers() {
5968 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
5969 assert_eq!(cfg.mcp_oauth_token_storage, None);
5970 let (wire, _runtime) = cfg
5971 .into_wire()
5972 .expect("default resume config has no duplicate handlers");
5973 assert!(!wire.request_user_input);
5974 assert!(!wire.request_permission);
5975 assert!(!wire.request_elicitation);
5976 assert!(!wire.request_exit_plan_mode);
5977 assert!(!wire.request_auto_mode_switch);
5978 assert!(!wire.hooks);
5979 assert!(!wire.request_mcp_apps);
5980 }
5981
5982 #[test]
5983 fn custom_agents_local_only_serializes_on_create_and_resume() {
5984 let (create_wire, _) = SessionConfig::default()
5985 .with_custom_agents_local_only(false)
5986 .into_wire(Some(SessionId::from("create-locality")))
5987 .expect("create config has no duplicate handlers");
5988 let create_json = serde_json::to_value(&create_wire).unwrap();
5989 assert_eq!(create_json["customAgentsLocalOnly"], false);
5990
5991 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
5992 .with_custom_agents_local_only(false)
5993 .into_wire()
5994 .expect("resume config has no duplicate handlers");
5995 let resume_json = serde_json::to_value(&resume_wire).unwrap();
5996 assert_eq!(resume_json["customAgentsLocalOnly"], false);
5997
5998 let (unset_create_wire, _) = SessionConfig::default()
5999 .into_wire(Some(SessionId::from("create-unset")))
6000 .expect("create config has no duplicate handlers");
6001 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6002 assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
6003
6004 let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
6005 .into_wire()
6006 .expect("resume config has no duplicate handlers");
6007 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6008 assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
6009 }
6010
6011 #[test]
6012 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6013 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
6014 assert_eq!(cfg.enable_mcp_apps, Some(true));
6015
6016 let (wire, _runtime) = cfg
6017 .into_wire(Some(SessionId::from("enable-mcp-apps")))
6018 .expect("enable_mcp_apps config has no duplicate handlers");
6019 assert!(wire.request_mcp_apps);
6020
6021 let json = serde_json::to_value(&wire).unwrap();
6022 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6023 }
6024
6025 #[test]
6026 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6027 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
6028 .with_enable_mcp_apps(true);
6029 assert_eq!(cfg.enable_mcp_apps, Some(true));
6030
6031 let (wire, _runtime) = cfg
6032 .into_wire()
6033 .expect("resume enable_mcp_apps config has no duplicate handlers");
6034 assert!(wire.request_mcp_apps);
6035
6036 let json = serde_json::to_value(&wire).unwrap();
6037 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6038 }
6039
6040 #[test]
6041 fn github_mcp_tool_config_serializes_for_create_and_resume() {
6042 let github_config = GitHubMcpToolConfig::new()
6043 .with_enable_all_tools(true)
6044 .with_additional_toolsets(["repos"])
6045 .with_additional_tools(["get_issue"])
6046 .with_enable_insiders_mode(true)
6047 .with_disable_form_deferral(true);
6048
6049 let (create_wire, _) = SessionConfig::default()
6050 .with_github_mcp_tool_config(github_config.clone())
6051 .into_wire(Some(SessionId::from("github-mcp")))
6052 .expect("create config has no duplicate handlers");
6053 assert_eq!(
6054 serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6055 serde_json::json!({
6056 "enableAllTools": true,
6057 "additionalToolsets": ["repos"],
6058 "additionalTools": ["get_issue"],
6059 "enableInsidersMode": true,
6060 "disableFormDeferral": true,
6061 })
6062 );
6063
6064 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6065 .with_github_mcp_tool_config(github_config)
6066 .into_wire()
6067 .expect("resume config has no duplicate handlers");
6068 assert!(resume_wire.github_mcp_tool_config.is_some());
6069
6070 let (unset_wire, _) = SessionConfig::default()
6071 .into_wire(Some(SessionId::from("github-mcp-unset")))
6072 .expect("default config has no duplicate handlers");
6073 assert!(
6074 serde_json::to_value(&unset_wire)
6075 .unwrap()
6076 .get("githubMcpToolConfig")
6077 .is_none()
6078 );
6079 }
6080
6081 #[test]
6082 fn memory_configuration_constructors_and_serde() {
6083 assert!(MemoryConfiguration::enabled().enabled);
6084 assert!(!MemoryConfiguration::disabled().enabled);
6085 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6086
6087 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6088 assert_eq!(json, serde_json::json!({ "enabled": true }));
6089 }
6090
6091 #[test]
6092 fn session_config_with_memory_serializes() {
6093 let (wire, _runtime) = SessionConfig::default()
6094 .with_memory(MemoryConfiguration::enabled())
6095 .into_wire(Some(SessionId::from("memory-on")))
6096 .expect("no duplicate handlers");
6097 let json = serde_json::to_value(&wire).unwrap();
6098 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6099
6100 let (wire_off, _) = SessionConfig::default()
6101 .with_memory(MemoryConfiguration::disabled())
6102 .into_wire(Some(SessionId::from("memory-off")))
6103 .expect("no duplicate handlers");
6104 let json_off = serde_json::to_value(&wire_off).unwrap();
6105 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6106
6107 let (empty_wire, _) = SessionConfig::default()
6109 .into_wire(Some(SessionId::from("memory-unset")))
6110 .expect("no duplicate handlers");
6111 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6112 assert!(empty_json.get("memory").is_none());
6113 }
6114
6115 #[test]
6116 fn resume_session_config_with_memory_serializes() {
6117 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6118 .with_memory(MemoryConfiguration::enabled())
6119 .into_wire()
6120 .expect("no duplicate handlers");
6121 let json = serde_json::to_value(&wire).unwrap();
6122 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6123
6124 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6126 .into_wire()
6127 .expect("no duplicate handlers");
6128 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6129 assert!(empty_json.get("memory").is_none());
6130 }
6131
6132 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6133 CopilotExpAssignmentResponse {
6134 features: vec!["copilot_exp_flag".to_string()],
6135 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6136 configs: vec![ExpConfigEntry {
6137 id: "cfg-1".to_string(),
6138 parameters: HashMap::from([
6139 ("threshold".to_string(), ExpFlagValue::Integer(5)),
6140 ("enabled".to_string(), ExpFlagValue::Bool(true)),
6141 ]),
6142 }],
6143 assignment_context: context.to_string(),
6144 ..Default::default()
6145 }
6146 }
6147
6148 #[test]
6149 fn exp_flag_value_round_trips_all_variants() {
6150 let values = serde_json::json!({
6151 "s": "text",
6152 "i": 7,
6153 "f": 1.5,
6154 "b": true,
6155 "n": null,
6156 });
6157 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6158 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6159 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6160 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6161 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6162 assert_eq!(parsed["n"], ExpFlagValue::Null);
6163 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6164 }
6165
6166 #[test]
6167 fn session_config_with_exp_assignments_serializes() {
6168 let assignments = sample_exp_assignments("ctx-123");
6169 let expected = serde_json::to_value(&assignments).unwrap();
6170 let (wire, _runtime) = SessionConfig::default()
6171 .with_exp_assignments(assignments)
6172 .into_wire(Some(SessionId::from("exp-on")))
6173 .expect("no duplicate handlers");
6174 let json = serde_json::to_value(&wire).unwrap();
6175 assert_eq!(json["expAssignments"], expected);
6176 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6177 assert_eq!(
6178 json["expAssignments"]["Flights"]["copilot_exp_flag"],
6179 "treatment"
6180 );
6181
6182 let (empty_wire, _) = SessionConfig::default()
6184 .into_wire(Some(SessionId::from("exp-unset")))
6185 .expect("no duplicate handlers");
6186 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6187 assert!(empty_json.get("expAssignments").is_none());
6188 }
6189
6190 #[test]
6191 fn resume_session_config_with_exp_assignments_serializes() {
6192 let assignments = sample_exp_assignments("ctx-456");
6193 let expected = serde_json::to_value(&assignments).unwrap();
6194 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6195 .with_exp_assignments(assignments)
6196 .into_wire()
6197 .expect("no duplicate handlers");
6198 let json = serde_json::to_value(&wire).unwrap();
6199 assert_eq!(json["expAssignments"], expected);
6200
6201 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6203 .into_wire()
6204 .expect("no duplicate handlers");
6205 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6206 assert!(empty_json.get("expAssignments").is_none());
6207 }
6208
6209 #[test]
6210 fn session_config_clone_preserves_exp_assignments() {
6211 let assignments = sample_exp_assignments("ctx-clone");
6212 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6213 let cloned = config.clone();
6214
6215 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6216
6217 let (wire, _runtime) = cloned
6218 .into_wire(Some(SessionId::from("exp-clone")))
6219 .expect("no duplicate handlers");
6220 let json = serde_json::to_value(&wire).unwrap();
6221 assert_eq!(
6222 json["expAssignments"],
6223 serde_json::to_value(&assignments).unwrap()
6224 );
6225 }
6226
6227 #[test]
6228 fn resume_session_config_clone_preserves_exp_assignments() {
6229 let assignments = sample_exp_assignments("ctx-clone-resume");
6230 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6231 .with_exp_assignments(assignments.clone());
6232 let cloned = config.clone();
6233
6234 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6235
6236 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6237 let json = serde_json::to_value(&wire).unwrap();
6238 assert_eq!(
6239 json["expAssignments"],
6240 serde_json::to_value(&assignments).unwrap()
6241 );
6242 }
6243
6244 #[test]
6245 #[allow(clippy::field_reassign_with_default)]
6246 fn session_config_into_wire_serializes_bucket_b_fields() {
6247 use std::path::PathBuf;
6248
6249 use super::{CloudSessionOptions, CloudSessionRepository};
6250
6251 let mut cfg = SessionConfig::default();
6252 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6253 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6254 cfg.github_token = Some("ghs_secret".to_string());
6255 cfg.include_sub_agent_streaming_events = Some(false);
6256 cfg.enable_session_telemetry = Some(false);
6257 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
6258 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
6259 cfg.enable_on_demand_instruction_discovery = Some(false);
6260 cfg.cloud = Some(CloudSessionOptions::with_repository(
6261 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
6262 ));
6263
6264 let (wire, _runtime) = cfg
6265 .into_wire(Some(SessionId::from("custom-id")))
6266 .expect("no duplicate handlers");
6267 let wire_json = serde_json::to_value(&wire).unwrap();
6268 assert_eq!(wire_json["sessionId"], "custom-id");
6269 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6270 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6271 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6272 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6273 assert_eq!(wire_json["enableSessionTelemetry"], false);
6274 assert_eq!(wire_json["reasoningSummary"], "concise");
6275 assert_eq!(wire_json["remoteSession"], "export");
6276 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6277 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6278 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6279 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6280
6281 let (empty_wire, _) = SessionConfig::default()
6283 .into_wire(Some(SessionId::from("empty")))
6284 .expect("default has no duplicate handlers");
6285 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6286 assert!(empty_json.get("gitHubToken").is_none());
6287 assert!(empty_json.get("enableSessionTelemetry").is_none());
6288 assert!(empty_json.get("reasoningSummary").is_none());
6289 assert!(empty_json.get("remoteSession").is_none());
6290 assert!(
6291 empty_json
6292 .get("enableOnDemandInstructionDiscovery")
6293 .is_none()
6294 );
6295 assert!(empty_json.get("cloud").is_none());
6296 }
6297
6298 #[test]
6299 fn session_config_into_wire_serializes_named_providers_and_models() {
6300 let cfg = SessionConfig::default()
6301 .with_providers(vec![
6302 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6303 .with_provider_type("openai")
6304 .with_wire_api("responses")
6305 .with_api_key("sk-test"),
6306 ])
6307 .with_models(vec![
6308 ProviderModelConfig::new("gpt-x", "my-openai")
6309 .with_wire_model("gpt-x-2025")
6310 .with_max_output_tokens(2048),
6311 ]);
6312
6313 let (wire, _) = cfg
6314 .into_wire(Some(SessionId::from("sess-providers")))
6315 .expect("no duplicate handlers");
6316 let wire_json = serde_json::to_value(&wire).unwrap();
6317 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6318 assert_eq!(
6319 wire_json["providers"][0]["baseUrl"],
6320 "https://api.example.com/v1"
6321 );
6322 assert_eq!(wire_json["providers"][0]["type"], "openai");
6323 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6324 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6325 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
6326 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
6327 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
6328 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
6329
6330 let (empty_wire, _) = SessionConfig::default()
6331 .into_wire(Some(SessionId::from("empty")))
6332 .expect("default has no duplicate handlers");
6333 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6334 assert!(empty_json.get("providers").is_none());
6335 assert!(empty_json.get("models").is_none());
6336 }
6337
6338 #[test]
6339 fn resume_config_into_wire_serializes_named_providers_and_models() {
6340 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
6341 .with_providers(vec![
6342 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
6343 .with_provider_type("azure")
6344 .with_azure(AzureProviderOptions {
6345 api_version: Some("2024-10-21".to_string()),
6346 }),
6347 ])
6348 .with_models(vec![
6349 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
6350 ]);
6351
6352 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6353 let wire_json = serde_json::to_value(&wire).unwrap();
6354 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
6355 assert_eq!(wire_json["providers"][0]["type"], "azure");
6356 assert_eq!(
6357 wire_json["providers"][0]["azure"]["apiVersion"],
6358 "2024-10-21"
6359 );
6360 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
6361 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
6362 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
6363
6364 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
6365 .into_wire()
6366 .expect("default has no duplicate handlers");
6367 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6368 assert!(empty_json.get("providers").is_none());
6369 assert!(empty_json.get("models").is_none());
6370 }
6371
6372 #[test]
6373 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
6374 use std::path::PathBuf;
6375
6376 let cfg = SessionConfig {
6377 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
6378 disabled_mcp_servers: Some(vec![
6379 "local-files".to_string(),
6380 "remote-github".to_string(),
6381 ]),
6382 large_output: Some(
6383 LargeToolOutputConfig::new()
6384 .with_enabled(true)
6385 .with_max_size_bytes(1024)
6386 .with_output_directory(PathBuf::from("/tmp/large-output")),
6387 ),
6388 ..Default::default()
6389 };
6390
6391 let (wire, _) = cfg
6392 .into_wire(Some(SessionId::from("sess-1")))
6393 .expect("no duplicate handlers");
6394 let wire_json = serde_json::to_value(&wire).unwrap();
6395 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
6396 assert_eq!(
6397 wire_json["disabledMcpServers"],
6398 serde_json::json!(["local-files", "remote-github"])
6399 );
6400 assert_eq!(wire_json["largeOutput"]["enabled"], true);
6401 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
6402 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
6403
6404 let (empty_wire, _) = SessionConfig::default()
6405 .into_wire(Some(SessionId::from("empty")))
6406 .expect("default has no duplicate handlers");
6407 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6408 assert!(empty_json.get("pluginDirectories").is_none());
6409 assert!(empty_json.get("disabledMcpServers").is_none());
6410 assert!(empty_json.get("largeOutput").is_none());
6411 }
6412
6413 #[test]
6414 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
6415 use std::path::PathBuf;
6416
6417 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6418 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6419 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6420 cfg.github_token = Some("ghs_secret".to_string());
6421 cfg.include_sub_agent_streaming_events = Some(true);
6422 cfg.enable_session_telemetry = Some(false);
6423 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
6424 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
6425 cfg.enable_on_demand_instruction_discovery = Some(false);
6426
6427 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6428 let wire_json = serde_json::to_value(&wire).unwrap();
6429 assert_eq!(wire_json["sessionId"], "sess-1");
6430 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6431 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6432 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6433 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
6434 assert_eq!(wire_json["enableSessionTelemetry"], false);
6435 assert_eq!(wire_json["reasoningSummary"], "detailed");
6436 assert_eq!(wire_json["remoteSession"], "on");
6437 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6438
6439 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6441 .into_wire()
6442 .expect("default resume has no duplicate handlers");
6443 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6444 assert!(empty_json.get("reasoningSummary").is_none());
6445 assert!(empty_json.get("remoteSession").is_none());
6446 assert!(
6447 empty_json
6448 .get("enableOnDemandInstructionDiscovery")
6449 .is_none()
6450 );
6451 }
6452
6453 #[test]
6454 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
6455 use std::path::PathBuf;
6456
6457 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6458 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6459 cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]);
6460 cfg.large_output = Some(
6461 LargeToolOutputConfig::new()
6462 .with_enabled(false)
6463 .with_max_size_bytes(2048)
6464 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6465 );
6466
6467 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6468 let wire_json = serde_json::to_value(&wire).unwrap();
6469 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6470 assert_eq!(
6471 wire_json["disabledMcpServers"],
6472 serde_json::json!(["local-files-r"])
6473 );
6474 assert_eq!(wire_json["largeOutput"]["enabled"], false);
6475 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6476 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6477
6478 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6479 .into_wire()
6480 .expect("default resume has no duplicate handlers");
6481 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6482 assert!(empty_json.get("pluginDirectories").is_none());
6483 assert!(empty_json.get("disabledMcpServers").is_none());
6484 assert!(empty_json.get("largeOutput").is_none());
6485 }
6486
6487 #[test]
6488 fn session_config_clones_disabled_mcp_servers() {
6489 let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]);
6490 let mut create_clone = create.clone();
6491 create_clone
6492 .disabled_mcp_servers
6493 .as_mut()
6494 .expect("configured disabled MCP servers")
6495 .push("remote-github".to_string());
6496 assert_eq!(
6497 create.disabled_mcp_servers.as_deref(),
6498 Some(&["local-files".to_string()][..])
6499 );
6500
6501 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6502 .with_disabled_mcp_servers(["local-files"]);
6503 let mut resume_clone = resume.clone();
6504 resume_clone
6505 .disabled_mcp_servers
6506 .as_mut()
6507 .expect("configured disabled MCP servers")
6508 .push("remote-github".to_string());
6509 assert_eq!(
6510 resume.disabled_mcp_servers.as_deref(),
6511 Some(&["local-files".to_string()][..])
6512 );
6513 }
6514
6515 #[test]
6516 fn session_config_builder_composes() {
6517 use indexmap::IndexMap;
6518
6519 let cfg = SessionConfig::default()
6520 .with_session_id(SessionId::from("sess-1"))
6521 .with_model("claude-sonnet-4")
6522 .with_client_name("test-app")
6523 .with_reasoning_effort("medium")
6524 .with_reasoning_summary(ReasoningSummary::Concise)
6525 .with_context_tier("long_context")
6526 .with_streaming(true)
6527 .with_tools([Tool::new("greet")])
6528 .with_available_tools(["bash", "view"])
6529 .with_excluded_tools(["dangerous"])
6530 .with_mcp_servers(IndexMap::new())
6531 .with_mcp_oauth_token_storage("persistent")
6532 .with_enable_config_discovery(true)
6533 .with_enable_on_demand_instruction_discovery(true)
6534 .with_skill_directories([PathBuf::from("/tmp/skills")])
6535 .with_disabled_skills(["broken-skill"])
6536 .with_disabled_mcp_servers(["local-files"])
6537 .with_agent("researcher")
6538 .with_config_directory(PathBuf::from("/tmp/config"))
6539 .with_working_directory(PathBuf::from("/tmp/work"))
6540 .with_additional_directories([PathBuf::from("/tmp/shared")])
6541 .with_github_token("ghp_test")
6542 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6543 .with_enable_session_telemetry(false)
6544 .with_include_sub_agent_streaming_events(false)
6545 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6546
6547 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
6548 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
6549 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6550 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
6551 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
6552 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
6553 assert_eq!(cfg.streaming, Some(true));
6554 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6555 assert_eq!(
6556 cfg.available_tools.as_deref(),
6557 Some(&["bash".to_string(), "view".to_string()][..])
6558 );
6559 assert_eq!(
6560 cfg.excluded_tools.as_deref(),
6561 Some(&["dangerous".to_string()][..])
6562 );
6563 assert!(cfg.mcp_servers.is_some());
6564 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6565 assert_eq!(cfg.enable_config_discovery, Some(true));
6566 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
6567 assert_eq!(
6568 cfg.skill_directories.as_deref(),
6569 Some(&[PathBuf::from("/tmp/skills")][..])
6570 );
6571 assert_eq!(
6572 cfg.disabled_skills.as_deref(),
6573 Some(&["broken-skill".to_string()][..])
6574 );
6575 assert_eq!(
6576 cfg.disabled_mcp_servers.as_deref(),
6577 Some(&["local-files".to_string()][..])
6578 );
6579 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6580 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6581 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6582 assert_eq!(
6583 cfg.additional_directories.as_deref(),
6584 Some(&[PathBuf::from("/tmp/shared")][..])
6585 );
6586 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6587 assert_eq!(
6588 cfg.capi,
6589 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6590 );
6591 assert_eq!(cfg.enable_session_telemetry, Some(false));
6592 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
6593 assert_eq!(
6594 cfg.extension_info,
6595 Some(ExtensionInfo::new("github-app", "counter"))
6596 );
6597 }
6598
6599 #[test]
6600 fn resume_session_config_builder_composes() {
6601 use indexmap::IndexMap;
6602
6603 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
6604 .with_client_name("test-app")
6605 .with_reasoning_summary(ReasoningSummary::None)
6606 .with_context_tier("default")
6607 .with_streaming(true)
6608 .with_tools([Tool::new("greet")])
6609 .with_available_tools(["bash", "view"])
6610 .with_excluded_tools(["dangerous"])
6611 .with_mcp_servers(IndexMap::new())
6612 .with_mcp_oauth_token_storage("persistent")
6613 .with_enable_config_discovery(true)
6614 .with_enable_on_demand_instruction_discovery(false)
6615 .with_skill_directories([PathBuf::from("/tmp/skills")])
6616 .with_disabled_skills(["broken-skill"])
6617 .with_disabled_mcp_servers(["local-files"])
6618 .with_agent("researcher")
6619 .with_config_directory(PathBuf::from("/tmp/config"))
6620 .with_working_directory(PathBuf::from("/tmp/work"))
6621 .with_additional_directories([PathBuf::from("/tmp/shared")])
6622 .with_github_token("ghp_test")
6623 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6624 .with_enable_session_telemetry(false)
6625 .with_include_sub_agent_streaming_events(true)
6626 .with_suppress_resume_event(true)
6627 .with_continue_pending_work(true)
6628 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6629
6630 assert_eq!(cfg.session_id.as_str(), "sess-2");
6631 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6632 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
6633 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
6634 assert_eq!(cfg.streaming, Some(true));
6635 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6636 assert_eq!(
6637 cfg.available_tools.as_deref(),
6638 Some(&["bash".to_string(), "view".to_string()][..])
6639 );
6640 assert_eq!(
6641 cfg.excluded_tools.as_deref(),
6642 Some(&["dangerous".to_string()][..])
6643 );
6644 assert!(cfg.mcp_servers.is_some());
6645 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6646 assert_eq!(cfg.enable_config_discovery, Some(true));
6647 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
6648 assert_eq!(
6649 cfg.skill_directories.as_deref(),
6650 Some(&[PathBuf::from("/tmp/skills")][..])
6651 );
6652 assert_eq!(
6653 cfg.disabled_skills.as_deref(),
6654 Some(&["broken-skill".to_string()][..])
6655 );
6656 assert_eq!(
6657 cfg.disabled_mcp_servers.as_deref(),
6658 Some(&["local-files".to_string()][..])
6659 );
6660 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6661 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6662 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6663 assert_eq!(
6664 cfg.additional_directories.as_deref(),
6665 Some(&[PathBuf::from("/tmp/shared")][..])
6666 );
6667 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6668 assert_eq!(
6669 cfg.capi,
6670 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6671 );
6672 assert_eq!(cfg.enable_session_telemetry, Some(false));
6673 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
6674 assert_eq!(cfg.suppress_resume_event, Some(true));
6675 assert_eq!(cfg.continue_pending_work, Some(true));
6676 assert_eq!(
6677 cfg.extension_info,
6678 Some(ExtensionInfo::new("github-app", "counter"))
6679 );
6680 }
6681
6682 #[test]
6686 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
6687 let cfg =
6688 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
6689 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6690 let json = serde_json::to_value(&wire).unwrap();
6691 assert_eq!(json["continuePendingWork"], true);
6692
6693 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6695 .into_wire()
6696 .expect("no duplicate handlers");
6697 let json = serde_json::to_value(&wire).unwrap();
6698 assert!(json.get("continuePendingWork").is_none());
6699 }
6700
6701 #[test]
6702 fn session_configs_serialize_additional_directories() {
6703 let create = SessionConfig::default().with_additional_directories([
6704 PathBuf::from("/tmp/shared"),
6705 PathBuf::from("/tmp/generated"),
6706 ]);
6707 let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
6708 let create_json = serde_json::to_value(&create_wire).unwrap();
6709 assert_eq!(
6710 create_json["additionalDirectories"],
6711 serde_json::json!(["/tmp/shared", "/tmp/generated"])
6712 );
6713
6714 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6715 .with_additional_directories([PathBuf::from("/tmp/resumed")]);
6716 let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
6717 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6718 assert_eq!(
6719 resume_json["additionalDirectories"],
6720 serde_json::json!(["/tmp/resumed"])
6721 );
6722 }
6723
6724 #[test]
6728 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
6729 let cfg =
6730 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
6731 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6732 let json = serde_json::to_value(&wire).unwrap();
6733 assert_eq!(json["disableResume"], true);
6734 assert!(json.get("suppressResumeEvent").is_none());
6735 }
6736
6737 #[test]
6740 fn session_config_serializes_instruction_directories_to_camel_case() {
6741 let cfg =
6742 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
6743 let (wire, _) = cfg
6744 .into_wire(Some(SessionId::from("instr-on")))
6745 .expect("no duplicate handlers");
6746 let json = serde_json::to_value(&wire).unwrap();
6747 assert_eq!(
6748 json["instructionDirectories"],
6749 serde_json::json!(["/tmp/instr"])
6750 );
6751
6752 let (wire, _) = SessionConfig::default()
6754 .into_wire(Some(SessionId::from("instr-off")))
6755 .expect("no duplicate handlers");
6756 let json = serde_json::to_value(&wire).unwrap();
6757 assert!(json.get("instructionDirectories").is_none());
6758 }
6759
6760 #[test]
6763 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
6764 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
6765 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
6766 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6767 let json = serde_json::to_value(&wire).unwrap();
6768 assert_eq!(
6769 json["instructionDirectories"],
6770 serde_json::json!(["/tmp/instr"])
6771 );
6772
6773 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6774 .into_wire()
6775 .expect("no duplicate handlers");
6776 let json = serde_json::to_value(&wire).unwrap();
6777 assert!(json.get("instructionDirectories").is_none());
6778 }
6779
6780 #[test]
6781 fn custom_agent_config_builder_composes() {
6782 use indexmap::IndexMap;
6783
6784 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
6785 .with_display_name("Research Assistant")
6786 .with_description("Investigates technical questions.")
6787 .with_tools(["bash", "view"])
6788 .with_mcp_servers(IndexMap::new())
6789 .with_infer(true)
6790 .with_skills(["rust-coding-skill"]);
6791
6792 assert_eq!(cfg.name, "researcher");
6793 assert_eq!(cfg.prompt, "You are a research assistant.");
6794 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
6795 assert_eq!(
6796 cfg.description.as_deref(),
6797 Some("Investigates technical questions.")
6798 );
6799 assert_eq!(
6800 cfg.tools.as_deref(),
6801 Some(&["bash".to_string(), "view".to_string()][..])
6802 );
6803 assert!(cfg.mcp_servers.is_some());
6804 assert_eq!(cfg.infer, Some(true));
6805 assert_eq!(
6806 cfg.skills.as_deref(),
6807 Some(&["rust-coding-skill".to_string()][..])
6808 );
6809 }
6810
6811 #[test]
6812 fn mcp_servers_serialize_in_insertion_order() {
6813 use indexmap::IndexMap;
6814
6815 let order = [
6821 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
6822 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
6823 ];
6824 let mut servers = IndexMap::new();
6825 for name in order {
6826 servers.insert(
6827 name.to_string(),
6828 McpServerConfig::Stdio(McpStdioServerConfig {
6829 command: "run".to_string(),
6830 ..Default::default()
6831 }),
6832 );
6833 }
6834
6835 let (wire, _runtime) = SessionConfig::default()
6836 .with_mcp_servers(servers)
6837 .into_wire(None)
6838 .expect("into_wire should succeed");
6839 let json = serde_json::to_string(&wire).expect("serialize wire");
6840
6841 let positions: Vec<usize> = order
6842 .iter()
6843 .map(|name| {
6844 json.find(&format!("\"{name}\""))
6845 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
6846 })
6847 .collect();
6848 let mut ascending = positions.clone();
6849 ascending.sort_unstable();
6850 assert_eq!(
6851 positions, ascending,
6852 "mcp server keys must serialize in insertion order: {json}"
6853 );
6854 }
6855
6856 #[test]
6857 fn infinite_session_config_builder_composes() {
6858 let cfg = InfiniteSessionConfig::new()
6859 .with_enabled(true)
6860 .with_background_compaction_threshold(0.75)
6861 .with_buffer_exhaustion_threshold(0.92);
6862
6863 assert_eq!(cfg.enabled, Some(true));
6864 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
6865 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
6866 }
6867
6868 #[test]
6869 fn provider_config_builder_composes() {
6870 use std::collections::HashMap;
6871
6872 let mut headers = HashMap::new();
6873 headers.insert("X-Custom".to_string(), "value".to_string());
6874
6875 let cfg = ProviderConfig::new("https://api.example.com")
6876 .with_provider_type("openai")
6877 .with_wire_api("completions")
6878 .with_transport("websockets")
6879 .with_api_key("sk-test")
6880 .with_bearer_token("bearer-test")
6881 .with_headers(headers)
6882 .with_model_id("gpt-4")
6883 .with_wire_model("azure-gpt-4-deployment")
6884 .with_max_prompt_tokens(8192)
6885 .with_max_output_tokens(2048);
6886
6887 assert_eq!(cfg.base_url, "https://api.example.com");
6888 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
6889 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
6890 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
6891 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
6892 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
6893 assert_eq!(
6894 cfg.headers
6895 .as_ref()
6896 .and_then(|h| h.get("X-Custom"))
6897 .map(String::as_str),
6898 Some("value"),
6899 );
6900 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
6901 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
6902 assert_eq!(cfg.max_prompt_tokens, Some(8192));
6903 assert_eq!(cfg.max_output_tokens, Some(2048));
6904
6905 let wire = serde_json::to_value(&cfg).unwrap();
6907 assert_eq!(wire["modelId"], "gpt-4");
6908 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
6909 assert_eq!(wire["maxPromptTokens"], 8192);
6910 assert_eq!(wire["maxOutputTokens"], 2048);
6911
6912 let unset = ProviderConfig::new("https://api.example.com");
6913 let wire_unset = serde_json::to_value(&unset).unwrap();
6914 assert!(wire_unset.get("modelId").is_none());
6915 assert!(wire_unset.get("wireModel").is_none());
6916 assert!(wire_unset.get("maxPromptTokens").is_none());
6917 assert!(wire_unset.get("maxOutputTokens").is_none());
6918 }
6919
6920 #[test]
6921 fn capi_session_options_builder_composes_and_serializes() {
6922 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
6923
6924 assert_eq!(cfg.enable_web_socket_responses, Some(false));
6925
6926 let wire = serde_json::to_value(&cfg).unwrap();
6927 assert_eq!(
6928 wire,
6929 serde_json::json!({ "enableWebSocketResponses": false })
6930 );
6931
6932 let unset = CapiSessionOptions::new();
6933 let wire_unset = serde_json::to_value(&unset).unwrap();
6934 assert!(wire_unset.get("enableWebSocketResponses").is_none());
6935 }
6936
6937 #[test]
6938 fn session_config_with_capi_serializes() {
6939 let (wire, _) = SessionConfig::default()
6940 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6941 .into_wire(Some(SessionId::from("capi-create")))
6942 .expect("no duplicate handlers");
6943 let json = serde_json::to_value(&wire).unwrap();
6944 assert_eq!(
6945 json["capi"],
6946 serde_json::json!({ "enableWebSocketResponses": false })
6947 );
6948
6949 let (empty_wire, _) = SessionConfig::default()
6950 .into_wire(Some(SessionId::from("capi-create-unset")))
6951 .expect("no duplicate handlers");
6952 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6953 assert!(empty_json.get("capi").is_none());
6954 }
6955
6956 #[test]
6957 fn resume_session_config_with_capi_serializes() {
6958 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
6959 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6960 .into_wire()
6961 .expect("no duplicate handlers");
6962 let json = serde_json::to_value(&wire).unwrap();
6963 assert_eq!(
6964 json["capi"],
6965 serde_json::json!({ "enableWebSocketResponses": false })
6966 );
6967
6968 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
6969 .into_wire()
6970 .expect("no duplicate handlers");
6971 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6972 assert!(empty_json.get("capi").is_none());
6973 }
6974
6975 #[test]
6976 fn system_message_config_builder_composes() {
6977 use std::collections::HashMap;
6978
6979 let cfg = SystemMessageConfig::new()
6980 .with_mode("replace")
6981 .with_content("Custom system message.")
6982 .with_sections(HashMap::new());
6983
6984 assert_eq!(cfg.mode.as_deref(), Some("replace"));
6985 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
6986 assert!(cfg.sections.is_some());
6987 }
6988
6989 #[test]
6990 fn delivery_mode_serializes_to_kebab_case_strings() {
6991 assert_eq!(
6992 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
6993 "\"enqueue\""
6994 );
6995 assert_eq!(
6996 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
6997 "\"immediate\""
6998 );
6999 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
7000 assert_eq!(parsed, DeliveryMode::Immediate);
7001 }
7002
7003 #[test]
7004 fn agent_mode_serializes_to_kebab_case_strings() {
7005 assert_eq!(
7006 serde_json::to_string(&AgentMode::Interactive).unwrap(),
7007 "\"interactive\""
7008 );
7009 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
7010 assert_eq!(
7011 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
7012 "\"autopilot\""
7013 );
7014 assert_eq!(
7015 serde_json::to_string(&AgentMode::Shell).unwrap(),
7016 "\"shell\""
7017 );
7018 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
7019 assert_eq!(parsed, AgentMode::Plan);
7020 }
7021
7022 #[test]
7023 fn connection_state_distinguishes_variants() {
7024 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
7027 }
7028
7029 #[test]
7035 fn session_event_round_trips_agent_id_on_envelope() {
7036 let wire = json!({
7037 "id": "evt-1",
7038 "timestamp": "2026-04-30T12:00:00Z",
7039 "parentId": null,
7040 "agentId": "sub-agent-42",
7041 "type": "assistant.message",
7042 "data": { "message": "hi" }
7043 });
7044
7045 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
7046 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7047
7048 let roundtripped = serde_json::to_value(&event).unwrap();
7050 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7051
7052 let main_agent_event: SessionEvent = serde_json::from_value(json!({
7054 "id": "evt-2",
7055 "timestamp": "2026-04-30T12:00:01Z",
7056 "parentId": null,
7057 "type": "session.idle",
7058 "data": {}
7059 }))
7060 .unwrap();
7061 assert!(main_agent_event.agent_id.is_none());
7062 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
7063 assert!(roundtripped.get("agentId").is_none());
7064 }
7065
7066 #[test]
7068 fn typed_session_event_round_trips_agent_id_on_envelope() {
7069 let wire = json!({
7070 "id": "evt-1",
7071 "timestamp": "2026-04-30T12:00:00Z",
7072 "parentId": null,
7073 "agentId": "sub-agent-42",
7074 "type": "session.idle",
7075 "data": {}
7076 });
7077
7078 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
7079 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7080
7081 let roundtripped = serde_json::to_value(&event).unwrap();
7082 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7083 }
7084
7085 #[test]
7086 fn connection_state_variants_compile() {
7087 let _ = ConnectionState::Disconnected;
7091 let _ = ConnectionState::Connecting;
7092 let _ = ConnectionState::Connected;
7093 let _ = ConnectionState::Error;
7094 }
7095
7096 #[test]
7097 fn deserializes_runtime_attachment_variants() {
7098 let attachments: Vec<Attachment> = serde_json::from_value(json!([
7099 {
7100 "type": "file",
7101 "path": "/tmp/file.rs",
7102 "displayName": "file.rs",
7103 "lineRange": { "start": 7, "end": 12 }
7104 },
7105 {
7106 "type": "directory",
7107 "path": "/tmp/project",
7108 "displayName": "project"
7109 },
7110 {
7111 "type": "selection",
7112 "filePath": "/tmp/lib.rs",
7113 "displayName": "lib.rs",
7114 "text": "fn main() {}",
7115 "selection": {
7116 "start": { "line": 1, "character": 2 },
7117 "end": { "line": 3, "character": 4 }
7118 }
7119 },
7120 {
7121 "type": "blob",
7122 "data": "Zm9v",
7123 "mimeType": "image/png",
7124 "displayName": "image.png"
7125 },
7126 {
7127 "type": "github_reference",
7128 "number": 42,
7129 "title": "Fix rendering",
7130 "referenceType": "issue",
7131 "state": "open",
7132 "url": "https://github.com/example/repo/issues/42"
7133 }
7134 ]))
7135 .expect("attachments should deserialize");
7136
7137 assert_eq!(attachments.len(), 5);
7138 assert!(matches!(
7139 &attachments[0],
7140 Attachment::File {
7141 path,
7142 display_name,
7143 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7144 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7145 ));
7146 assert!(matches!(
7147 &attachments[1],
7148 Attachment::Directory { path, display_name }
7149 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7150 ));
7151 assert!(matches!(
7152 &attachments[2],
7153 Attachment::Selection {
7154 file_path,
7155 display_name,
7156 selection:
7157 AttachmentSelectionRange {
7158 start: AttachmentSelectionPosition { line: 1, character: 2 },
7159 end: AttachmentSelectionPosition { line: 3, character: 4 },
7160 },
7161 ..
7162 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
7163 ));
7164 assert!(matches!(
7165 &attachments[3],
7166 Attachment::Blob {
7167 data,
7168 mime_type,
7169 display_name,
7170 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
7171 ));
7172 assert!(matches!(
7173 &attachments[4],
7174 Attachment::GitHubReference {
7175 number: 42,
7176 title,
7177 reference_type: GitHubReferenceType::Issue,
7178 state,
7179 url,
7180 } if title == "Fix rendering"
7181 && state == "open"
7182 && url == "https://github.com/example/repo/issues/42"
7183 ));
7184 }
7185
7186 #[test]
7187 fn ensures_display_names_for_variants_that_support_them() {
7188 let mut attachments = vec![
7189 Attachment::File {
7190 path: PathBuf::from("/tmp/file.rs"),
7191 display_name: None,
7192 line_range: None,
7193 },
7194 Attachment::Selection {
7195 file_path: PathBuf::from("/tmp/src/lib.rs"),
7196 display_name: None,
7197 text: "fn main() {}".to_string(),
7198 selection: AttachmentSelectionRange {
7199 start: AttachmentSelectionPosition {
7200 line: 0,
7201 character: 0,
7202 },
7203 end: AttachmentSelectionPosition {
7204 line: 0,
7205 character: 10,
7206 },
7207 },
7208 },
7209 Attachment::Blob {
7210 data: "Zm9v".to_string(),
7211 mime_type: "image/png".to_string(),
7212 display_name: None,
7213 },
7214 Attachment::GitHubReference {
7215 number: 7,
7216 title: "Track regressions".to_string(),
7217 reference_type: GitHubReferenceType::Issue,
7218 state: "open".to_string(),
7219 url: "https://example.com/issues/7".to_string(),
7220 },
7221 ];
7222
7223 ensure_attachment_display_names(&mut attachments);
7224
7225 assert_eq!(attachments[0].display_name(), Some("file.rs"));
7226 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
7227 assert_eq!(attachments[2].display_name(), Some("attachment"));
7228 assert_eq!(attachments[3].display_name(), None);
7229 assert_eq!(
7230 attachments[3].label(),
7231 Some("Track regressions".to_string())
7232 );
7233 }
7234
7235 #[test]
7236 fn github_anchored_attachment_variants_round_trip() {
7237 let cases = vec![
7238 (
7239 "github_commit",
7240 json!({
7241 "type": "github_commit",
7242 "message": "Fix the thing",
7243 "oid": "abc123",
7244 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
7245 "url": "https://github.com/octocat/repo/commit/abc123"
7246 }),
7247 ),
7248 (
7249 "github_release",
7250 json!({
7251 "type": "github_release",
7252 "name": "v1.2.3",
7253 "repo": { "name": "repo", "owner": "octocat" },
7254 "tagName": "v1.2.3",
7255 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
7256 }),
7257 ),
7258 (
7259 "github_actions_job",
7260 json!({
7261 "type": "github_actions_job",
7262 "conclusion": "failure",
7263 "jobId": 99,
7264 "jobName": "build",
7265 "repo": { "name": "repo", "owner": "octocat" },
7266 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
7267 "workflowName": "CI"
7268 }),
7269 ),
7270 (
7271 "github_repository",
7272 json!({
7273 "type": "github_repository",
7274 "description": "An example repository",
7275 "ref": "main",
7276 "repo": { "name": "repo", "owner": "octocat" },
7277 "url": "https://github.com/octocat/repo"
7278 }),
7279 ),
7280 (
7281 "github_file_diff",
7282 json!({
7283 "type": "github_file_diff",
7284 "base": {
7285 "path": "src/lib.rs",
7286 "ref": "main",
7287 "repo": { "name": "repo", "owner": "octocat" }
7288 },
7289 "head": {
7290 "path": "src/lib.rs",
7291 "ref": "feature",
7292 "repo": { "name": "repo", "owner": "octocat" }
7293 },
7294 "url": "https://github.com/octocat/repo/compare/main...feature"
7295 }),
7296 ),
7297 (
7298 "github_tree_comparison",
7299 json!({
7300 "type": "github_tree_comparison",
7301 "base": {
7302 "repo": { "name": "repo", "owner": "octocat" },
7303 "revision": "main"
7304 },
7305 "head": {
7306 "repo": { "name": "repo", "owner": "octocat" },
7307 "revision": "feature"
7308 },
7309 "url": "https://github.com/octocat/repo/compare/main...feature"
7310 }),
7311 ),
7312 (
7313 "github_url",
7314 json!({
7315 "type": "github_url",
7316 "url": "https://github.com/octocat/repo/wiki"
7317 }),
7318 ),
7319 (
7320 "github_file",
7321 json!({
7322 "type": "github_file",
7323 "path": "src/main.rs",
7324 "ref": "main",
7325 "repo": { "name": "repo", "owner": "octocat" },
7326 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
7327 }),
7328 ),
7329 (
7330 "github_snippet",
7331 json!({
7332 "type": "github_snippet",
7333 "lineRange": { "start": 10, "end": 20 },
7334 "path": "src/main.rs",
7335 "ref": "main",
7336 "repo": { "name": "repo", "owner": "octocat" },
7337 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
7338 }),
7339 ),
7340 ];
7341
7342 for (expected_type, input) in cases {
7343 let attachment: Attachment = serde_json::from_value(input.clone())
7344 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
7345
7346 let serialized_string = serde_json::to_string(&attachment)
7351 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
7352
7353 assert_eq!(
7355 serialized_string.matches("\"type\":").count(),
7356 1,
7357 "{expected_type} must serialize a single `type` key"
7358 );
7359
7360 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
7361 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
7362 assert_eq!(
7363 serialized.get("type").and_then(|value| value.as_str()),
7364 Some(expected_type),
7365 "{expected_type} must serialize the correct discriminator"
7366 );
7367
7368 assert_eq!(
7370 serialized, input,
7371 "{expected_type} should round-trip without data loss"
7372 );
7373 let reparsed: Attachment = serde_json::from_value(serialized)
7374 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
7375 assert_eq!(
7376 reparsed, attachment,
7377 "{expected_type} should re-deserialize to the same value"
7378 );
7379 }
7380 }
7381}
7382
7383#[cfg(test)]
7384mod permission_builder_tests {
7385 use std::sync::Arc;
7386
7387 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
7388 use crate::permission;
7389 use crate::types::{
7390 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
7391 SessionId,
7392 };
7393
7394 fn data() -> PermissionRequestData {
7395 PermissionRequestData {
7396 extra: serde_json::json!({"tool": "shell"}),
7397 ..Default::default()
7398 }
7399 }
7400
7401 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7404 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7405 }
7406
7407 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7408 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7409 }
7410
7411 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
7412 handler
7413 .handle(SessionId::from("s1"), RequestId::new("1"), data())
7414 .await
7415 }
7416
7417 #[tokio::test]
7418 async fn approve_all_with_handler_present_approves() {
7419 let cfg = SessionConfig::default()
7420 .with_permission_handler(Arc::new(ApproveAllHandler))
7421 .approve_all_permissions();
7422 let h = resolve_create(cfg).expect("policy + handler yields handler");
7423 assert!(matches!(
7424 dispatch(&h).await,
7425 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7426 ));
7427 }
7428
7429 #[tokio::test]
7430 async fn approve_all_standalone_produces_handler() {
7431 let cfg = SessionConfig::default().approve_all_permissions();
7432 let h = resolve_create(cfg).expect("policy alone yields handler");
7433 assert!(matches!(
7434 dispatch(&h).await,
7435 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7436 ));
7437 }
7438
7439 #[tokio::test]
7442 async fn approve_all_is_order_independent() {
7443 let a = SessionConfig::default()
7444 .with_permission_handler(Arc::new(ApproveAllHandler))
7445 .approve_all_permissions();
7446 let b = SessionConfig::default()
7447 .approve_all_permissions()
7448 .with_permission_handler(Arc::new(ApproveAllHandler));
7449 let ha = resolve_create(a).unwrap();
7450 let hb = resolve_create(b).unwrap();
7451 assert!(matches!(
7452 dispatch(&ha).await,
7453 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7454 ));
7455 assert!(matches!(
7456 dispatch(&hb).await,
7457 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7458 ));
7459 }
7460
7461 #[tokio::test]
7462 async fn deny_all_is_order_independent() {
7463 let a = SessionConfig::default()
7464 .with_permission_handler(Arc::new(ApproveAllHandler))
7465 .deny_all_permissions();
7466 let b = SessionConfig::default()
7467 .deny_all_permissions()
7468 .with_permission_handler(Arc::new(ApproveAllHandler));
7469 let ha = resolve_create(a).unwrap();
7470 let hb = resolve_create(b).unwrap();
7471 assert!(matches!(
7472 dispatch(&ha).await,
7473 PermissionResult::Decision(PermissionDecision::Reject(_))
7474 ));
7475 assert!(matches!(
7476 dispatch(&hb).await,
7477 PermissionResult::Decision(PermissionDecision::Reject(_))
7478 ));
7479 }
7480
7481 #[tokio::test]
7482 async fn approve_permissions_if_consults_predicate() {
7483 let cfg = SessionConfig::default().approve_permissions_if(|d| {
7484 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7485 });
7486 let h = resolve_create(cfg).unwrap();
7487 assert!(matches!(
7488 dispatch(&h).await,
7489 PermissionResult::Decision(PermissionDecision::Reject(_))
7490 ));
7491 }
7492
7493 #[tokio::test]
7494 async fn approve_permissions_if_is_order_independent() {
7495 let predicate = |d: &PermissionRequestData| {
7496 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7497 };
7498 let a = SessionConfig::default()
7499 .with_permission_handler(Arc::new(ApproveAllHandler))
7500 .approve_permissions_if(predicate);
7501 let b = SessionConfig::default()
7502 .approve_permissions_if(predicate)
7503 .with_permission_handler(Arc::new(ApproveAllHandler));
7504 let ha = resolve_create(a).unwrap();
7505 let hb = resolve_create(b).unwrap();
7506 assert!(matches!(
7507 dispatch(&ha).await,
7508 PermissionResult::Decision(PermissionDecision::Reject(_))
7509 ));
7510 assert!(matches!(
7511 dispatch(&hb).await,
7512 PermissionResult::Decision(PermissionDecision::Reject(_))
7513 ));
7514 }
7515
7516 #[tokio::test]
7517 async fn resume_session_config_approve_all_works() {
7518 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
7519 .with_permission_handler(Arc::new(ApproveAllHandler))
7520 .approve_all_permissions();
7521 let h = resolve_resume(cfg).unwrap();
7522 assert!(matches!(
7523 dispatch(&h).await,
7524 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7525 ));
7526 }
7527
7528 #[tokio::test]
7529 async fn resume_session_config_approve_all_is_order_independent() {
7530 let a = ResumeSessionConfig::new(SessionId::from("s1"))
7531 .with_permission_handler(Arc::new(ApproveAllHandler))
7532 .approve_all_permissions();
7533 let b = ResumeSessionConfig::new(SessionId::from("s1"))
7534 .approve_all_permissions()
7535 .with_permission_handler(Arc::new(ApproveAllHandler));
7536 let ha = resolve_resume(a).unwrap();
7537 let hb = resolve_resume(b).unwrap();
7538 assert!(matches!(
7539 dispatch(&ha).await,
7540 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7541 ));
7542 assert!(matches!(
7543 dispatch(&hb).await,
7544 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7545 ));
7546 }
7547
7548 #[test]
7549 fn session_config_enable_experimental_mode_serializes_when_set() {
7550 let cfg = SessionConfig::default().with_enable_experimental_mode(false);
7551 assert_eq!(cfg.enable_experimental_mode, Some(false));
7552
7553 let (wire, _runtime) = cfg
7554 .into_wire(Some(SessionId::from("experimental-mode")))
7555 .expect("enable_experimental_mode config has no duplicate handlers");
7556 assert_eq!(wire.is_experimental_mode, Some(false));
7557
7558 let json = serde_json::to_value(&wire).unwrap();
7559 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7560 }
7561
7562 #[test]
7563 fn session_config_enable_experimental_mode_omitted_when_none() {
7564 let cfg = SessionConfig::default();
7565 assert_eq!(cfg.enable_experimental_mode, None);
7566
7567 let (wire, _runtime) = cfg
7568 .into_wire(Some(SessionId::from("no-experimental-mode")))
7569 .expect("default config has no duplicate handlers");
7570 assert_eq!(wire.is_experimental_mode, None);
7571
7572 let json = serde_json::to_value(&wire).unwrap();
7573 assert!(json.get("isExperimentalMode").is_none());
7574 }
7575
7576 #[test]
7577 fn resume_session_config_enable_experimental_mode_serializes_when_set() {
7578 let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
7579 .with_enable_experimental_mode(false);
7580 assert_eq!(cfg.enable_experimental_mode, Some(false));
7581
7582 let (wire, _runtime) = cfg
7583 .into_wire()
7584 .expect("resume enable_experimental_mode config has no duplicate handlers");
7585 assert_eq!(wire.is_experimental_mode, Some(false));
7586
7587 let json = serde_json::to_value(&wire).unwrap();
7588 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7589 }
7590
7591 #[test]
7592 fn resume_session_config_enable_experimental_mode_omitted_when_none() {
7593 let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
7594 assert_eq!(cfg.enable_experimental_mode, None);
7595
7596 let (wire, _runtime) = cfg
7597 .into_wire()
7598 .expect("default resume config has no duplicate handlers");
7599 assert_eq!(wire.is_experimental_mode, None);
7600
7601 let json = serde_json::to_value(&wire).unwrap();
7602 assert!(json.get("isExperimentalMode").is_none());
7603 }
7604}