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 hooks: Option<bool>,
1935 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1937 pub default_agent: Option<DefaultAgentConfig>,
1941 pub agent: Option<String>,
1944 pub infinite_sessions: Option<InfiniteSessionConfig>,
1947 pub provider: Option<ProviderConfig>,
1951 pub capi: Option<CapiSessionOptions>,
1957 pub providers: Option<Vec<NamedProviderConfig>>,
1964 pub models: Option<Vec<ProviderModelConfig>>,
1970 pub enable_session_telemetry: Option<bool>,
1978 pub enable_citations: Option<bool>,
1980 pub session_limits: Option<SessionLimitsConfig>,
1982 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1985 pub memory: Option<MemoryConfiguration>,
1987 pub config_directory: Option<PathBuf>,
1990 pub working_directory: Option<PathBuf>,
1993 pub additional_directories: Option<Vec<PathBuf>>,
1997 pub github_token: Option<String>,
2003 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2009 pub cloud: Option<CloudSessionOptions>,
2012 pub include_sub_agent_streaming_events: Option<bool>,
2016 pub commands: Option<Vec<CommandDefinition>>,
2020 #[doc(hidden)]
2027 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
2028 pub enable_managed_settings: Option<bool>,
2035 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2040 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2044 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2047 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2050 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2054 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2057 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2060 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2064 pub(crate) permission_policy: Option<crate::permission::Policy>,
2068 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2073 pub skip_custom_instructions: Option<bool>,
2077 pub custom_agents_local_only: Option<bool>,
2081 pub enable_experimental_mode: Option<bool>,
2086 pub coauthor_enabled: Option<bool>,
2090 pub manage_schedule_enabled: Option<bool>,
2094}
2095
2096impl std::fmt::Debug for SessionConfig {
2097 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2098 f.debug_struct("SessionConfig")
2099 .field("session_id", &self.session_id)
2100 .field("model", &self.model)
2101 .field("client_name", &self.client_name)
2102 .field("reasoning_effort", &self.reasoning_effort)
2103 .field("reasoning_summary", &self.reasoning_summary)
2104 .field("context_tier", &self.context_tier)
2105 .field("streaming", &self.streaming)
2106 .field("system_message", &self.system_message)
2107 .field("tools", &self.tools)
2108 .field("canvases", &self.canvases)
2109 .field(
2110 "canvas_handler",
2111 &self.canvas_handler.as_ref().map(|_| "<set>"),
2112 )
2113 .field("request_canvas_renderer", &self.request_canvas_renderer)
2114 .field("request_extensions", &self.request_extensions)
2115 .field("extension_sdk_path", &self.extension_sdk_path)
2116 .field("extension_info", &self.extension_info)
2117 .field("canvas_provider", &self.canvas_provider)
2118 .field("available_tools", &self.available_tools)
2119 .field("excluded_tools", &self.excluded_tools)
2120 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2121 .field("mcp_servers", &self.mcp_servers)
2122 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2123 .field("embedding_cache_storage", &self.embedding_cache_storage)
2124 .field("enable_config_discovery", &self.enable_config_discovery)
2125 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2126 .field(
2127 "organization_custom_instructions",
2128 &self
2129 .organization_custom_instructions
2130 .as_ref()
2131 .map(|_| "<redacted>"),
2132 )
2133 .field(
2134 "enable_on_demand_instruction_discovery",
2135 &self.enable_on_demand_instruction_discovery,
2136 )
2137 .field("enable_file_hooks", &self.enable_file_hooks)
2138 .field(
2139 "enable_host_git_operations",
2140 &self.enable_host_git_operations,
2141 )
2142 .field("enable_session_store", &self.enable_session_store)
2143 .field("enable_skills", &self.enable_skills)
2144 .field("enable_mcp_apps", &self.enable_mcp_apps)
2145 .field("skill_directories", &self.skill_directories)
2146 .field("instruction_directories", &self.instruction_directories)
2147 .field("plugin_directories", &self.plugin_directories)
2148 .field("large_output", &self.large_output)
2149 .field("tool_search", &self.tool_search)
2150 .field("disabled_skills", &self.disabled_skills)
2151 .field("hooks", &self.hooks)
2152 .field("custom_agents", &self.custom_agents)
2153 .field("default_agent", &self.default_agent)
2154 .field("agent", &self.agent)
2155 .field("infinite_sessions", &self.infinite_sessions)
2156 .field("provider", &self.provider)
2157 .field("capi", &self.capi)
2158 .field("enable_session_telemetry", &self.enable_session_telemetry)
2159 .field("enable_citations", &self.enable_citations)
2160 .field("session_limits", &self.session_limits)
2161 .field("model_capabilities", &self.model_capabilities)
2162 .field("memory", &self.memory)
2163 .field("config_directory", &self.config_directory)
2164 .field("working_directory", &self.working_directory)
2165 .field("additional_directories", &self.additional_directories)
2166 .field(
2167 "github_token",
2168 &self.github_token.as_ref().map(|_| "<redacted>"),
2169 )
2170 .field("remote_session", &self.remote_session)
2171 .field("cloud", &self.cloud)
2172 .field(
2173 "include_sub_agent_streaming_events",
2174 &self.include_sub_agent_streaming_events,
2175 )
2176 .field("commands", &self.commands)
2177 .field("exp_assignments", &self.exp_assignments)
2178 .field("enable_managed_settings", &self.enable_managed_settings)
2179 .field("enable_experimental_mode", &self.enable_experimental_mode)
2180 .field(
2181 "session_fs_provider",
2182 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2183 )
2184 .field(
2185 "permission_handler",
2186 &self.permission_handler.as_ref().map(|_| "<set>"),
2187 )
2188 .field(
2189 "elicitation_handler",
2190 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2191 )
2192 .field(
2193 "mcp_auth_handler",
2194 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2195 )
2196 .field(
2197 "user_input_handler",
2198 &self.user_input_handler.as_ref().map(|_| "<set>"),
2199 )
2200 .field(
2201 "exit_plan_mode_handler",
2202 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2203 )
2204 .field(
2205 "auto_mode_switch_handler",
2206 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2207 )
2208 .field(
2209 "hooks_handler",
2210 &self.hooks_handler.as_ref().map(|_| "<set>"),
2211 )
2212 .field(
2213 "system_message_transform",
2214 &self.system_message_transform.as_ref().map(|_| "<set>"),
2215 )
2216 .finish()
2217 }
2218}
2219
2220impl Default for SessionConfig {
2221 fn default() -> Self {
2227 Self {
2228 session_id: None,
2229 model: None,
2230 client_name: None,
2231 reasoning_effort: None,
2232 reasoning_summary: None,
2233 context_tier: None,
2234 streaming: None,
2235 system_message: None,
2236 tools: None,
2237 canvases: None,
2238 canvas_handler: None,
2239 request_canvas_renderer: None,
2240 request_extensions: None,
2241 extension_sdk_path: None,
2242 extension_info: None,
2243 canvas_provider: None,
2244 available_tools: None,
2245 excluded_tools: None,
2246 excluded_builtin_agents: None,
2247 mcp_servers: None,
2248 mcp_oauth_token_storage: None,
2249 enable_config_discovery: None,
2250 skip_embedding_retrieval: None,
2251 organization_custom_instructions: None,
2252 enable_on_demand_instruction_discovery: None,
2253 enable_file_hooks: None,
2254 enable_host_git_operations: None,
2255 enable_session_store: None,
2256 enable_skills: None,
2257 embedding_cache_storage: None,
2258 enable_mcp_apps: None,
2259 github_mcp_tool_config: None,
2260 skill_directories: None,
2261 instruction_directories: None,
2262 plugin_directories: None,
2263 large_output: None,
2264 tool_search: None,
2265 disabled_skills: None,
2266 hooks: None,
2267 custom_agents: None,
2268 default_agent: None,
2269 agent: None,
2270 infinite_sessions: None,
2271 provider: None,
2272 capi: None,
2273 providers: None,
2274 models: None,
2275 enable_session_telemetry: None,
2276 enable_citations: None,
2277 session_limits: None,
2278 model_capabilities: None,
2279 memory: None,
2280 config_directory: None,
2281 working_directory: None,
2282 additional_directories: None,
2283 github_token: None,
2284 remote_session: None,
2285 cloud: None,
2286 include_sub_agent_streaming_events: None,
2287 commands: None,
2288 exp_assignments: None,
2289 enable_managed_settings: None,
2290 session_fs_provider: None,
2291 permission_handler: None,
2292 elicitation_handler: None,
2293 mcp_auth_handler: None,
2294 user_input_handler: None,
2295 exit_plan_mode_handler: None,
2296 auto_mode_switch_handler: None,
2297 hooks_handler: None,
2298 permission_policy: None,
2299 system_message_transform: None,
2300 skip_custom_instructions: None,
2301 custom_agents_local_only: None,
2302 enable_experimental_mode: None,
2303 coauthor_enabled: None,
2304 manage_schedule_enabled: None,
2305 }
2306 }
2307}
2308
2309pub(crate) struct SessionConfigRuntime {
2315 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2316 pub permission_policy: Option<crate::permission::Policy>,
2317 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2318 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2319 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2320 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2321 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2322 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2323 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2324 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2325 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2326 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2327 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2328 pub commands: Option<Vec<CommandDefinition>>,
2329}
2330
2331impl SessionConfig {
2332 pub(crate) fn into_wire(
2344 mut self,
2345 session_id: Option<SessionId>,
2346 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2347 let permission_active =
2348 self.permission_handler.is_some() || self.permission_policy.is_some();
2349 let request_user_input = self.user_input_handler.is_some();
2350 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2351 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2352 let request_elicitation = self.elicitation_handler.is_some();
2353 let hooks_flag = self.hooks_handler.is_some();
2354
2355 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2356 if let Some(tools) = self.tools.as_mut() {
2357 for tool in tools.iter_mut() {
2358 if let Some(handler) = tool.handler.take()
2359 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2360 {
2361 return Err(crate::Error::with_message(
2362 crate::ErrorKind::InvalidConfig,
2363 format!("duplicate tool handler registered for name {:?}", tool.name),
2364 ));
2365 }
2366 }
2367 }
2368
2369 let wire_commands = self.commands.as_ref().map(|cmds| {
2370 cmds.iter()
2371 .map(|c| crate::wire::CommandWireDefinition {
2372 name: c.name.clone(),
2373 description: c.description.clone(),
2374 })
2375 .collect()
2376 });
2377 let wire_canvases = self.canvases.clone();
2378 let canvas_handler = self.canvas_handler.clone();
2379 let bearer_token_providers =
2380 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2381
2382 let wire = crate::wire::SessionCreateWire {
2383 session_id,
2384 model: self.model,
2385 client_name: self.client_name,
2386 reasoning_effort: self.reasoning_effort,
2387 reasoning_summary: self.reasoning_summary,
2388 context_tier: self.context_tier,
2389 streaming: self.streaming,
2390 system_message: self.system_message,
2391 tools: self.tools,
2392 canvases: wire_canvases,
2393 request_canvas_renderer: self.request_canvas_renderer,
2394 request_extensions: self.request_extensions,
2395 extension_sdk_path: self.extension_sdk_path,
2396 extension_info: self.extension_info,
2397 canvas_provider: self.canvas_provider,
2398 available_tools: self.available_tools,
2399 excluded_tools: self.excluded_tools,
2400 excluded_builtin_agents: self.excluded_builtin_agents,
2401 tool_filter_precedence: "excluded",
2402 mcp_servers: self.mcp_servers,
2403 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2404 embedding_cache_storage: self.embedding_cache_storage,
2405 env_value_mode: "direct",
2406 enable_config_discovery: self.enable_config_discovery,
2407 skip_embedding_retrieval: self.skip_embedding_retrieval,
2408 organization_custom_instructions: self.organization_custom_instructions,
2409 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2410 enable_file_hooks: self.enable_file_hooks,
2411 enable_host_git_operations: self.enable_host_git_operations,
2412 enable_session_store: self.enable_session_store,
2413 enable_skills: self.enable_skills,
2414 request_user_input,
2415 request_permission: permission_active,
2416 request_exit_plan_mode,
2417 request_auto_mode_switch,
2418 request_elicitation,
2419 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2420 github_mcp_tool_config: self.github_mcp_tool_config,
2421 hooks: hooks_flag,
2422 skill_directories: self.skill_directories,
2423 instruction_directories: self.instruction_directories,
2424 plugin_directories: self.plugin_directories,
2425 large_output: self.large_output,
2426 tool_search: self.tool_search,
2427 disabled_skills: self.disabled_skills,
2428 custom_agents: self.custom_agents,
2429 custom_agents_local_only: self.custom_agents_local_only,
2430 default_agent: self.default_agent,
2431 agent: self.agent,
2432 infinite_sessions: self.infinite_sessions,
2433 provider: self.provider,
2434 capi: self.capi,
2435 providers: self.providers,
2436 models: self.models,
2437 enable_session_telemetry: self.enable_session_telemetry,
2438 enable_citations: self.enable_citations,
2439 session_limits: self.session_limits,
2440 model_capabilities: self.model_capabilities,
2441 memory: self.memory,
2442 config_dir: self.config_directory,
2443 working_directory: self.working_directory,
2444 additional_directories: self.additional_directories,
2445 github_token: self.github_token,
2446 remote_session: self.remote_session,
2447 cloud: self.cloud,
2448 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2449 enable_github_telemetry_forwarding: None,
2450 commands: wire_commands,
2451 exp_assignments: self.exp_assignments,
2452 enable_managed_settings: self.enable_managed_settings,
2453 is_experimental_mode: self.enable_experimental_mode,
2454 };
2455
2456 let runtime = SessionConfigRuntime {
2457 permission_handler: self.permission_handler,
2458 permission_policy: self.permission_policy,
2459 elicitation_handler: self.elicitation_handler,
2460 mcp_auth_handler: self.mcp_auth_handler,
2461 user_input_handler: self.user_input_handler,
2462 exit_plan_mode_handler: self.exit_plan_mode_handler,
2463 auto_mode_switch_handler: self.auto_mode_switch_handler,
2464 hooks_handler: self.hooks_handler,
2465 system_message_transform: self.system_message_transform,
2466 tool_handlers,
2467 canvas_handler,
2468 session_fs_provider: self.session_fs_provider,
2469 bearer_token_providers,
2470 commands: self.commands,
2471 };
2472
2473 Ok((wire, runtime))
2474 }
2475
2476 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2480 self.permission_handler = Some(handler);
2481 self
2482 }
2483
2484 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2487 self.elicitation_handler = Some(handler);
2488 self
2489 }
2490
2491 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2493 self.mcp_auth_handler = Some(handler);
2494 self
2495 }
2496
2497 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2500 self.user_input_handler = Some(handler);
2501 self
2502 }
2503
2504 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2506 self.exit_plan_mode_handler = Some(handler);
2507 self
2508 }
2509
2510 pub fn with_auto_mode_switch_handler(
2512 mut self,
2513 handler: Arc<dyn AutoModeSwitchHandler>,
2514 ) -> Self {
2515 self.auto_mode_switch_handler = Some(handler);
2516 self
2517 }
2518
2519 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2524 self.commands = Some(commands);
2525 self
2526 }
2527
2528 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2532 self.session_fs_provider = Some(provider);
2533 self
2534 }
2535
2536 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2539 self.hooks_handler = Some(hooks);
2540 self
2541 }
2542
2543 pub fn with_system_message_transform(
2547 mut self,
2548 transform: Arc<dyn SystemMessageTransform>,
2549 ) -> Self {
2550 self.system_message_transform = Some(transform);
2551 self
2552 }
2553
2554 pub fn approve_all_permissions(mut self) -> Self {
2560 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2561 self
2562 }
2563
2564 pub fn deny_all_permissions(mut self) -> Self {
2567 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2568 self
2569 }
2570
2571 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2576 where
2577 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2578 {
2579 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2580 self
2581 }
2582
2583 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2585 self.session_id = Some(id.into());
2586 self
2587 }
2588
2589 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2591 self.model = Some(model.into());
2592 self
2593 }
2594
2595 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2597 self.client_name = Some(name.into());
2598 self
2599 }
2600
2601 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2603 self.reasoning_effort = Some(effort.into());
2604 self
2605 }
2606
2607 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2609 self.reasoning_summary = Some(summary);
2610 self
2611 }
2612
2613 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2615 self.context_tier = Some(tier.into());
2616 self
2617 }
2618
2619 pub fn with_streaming(mut self, streaming: bool) -> Self {
2621 self.streaming = Some(streaming);
2622 self
2623 }
2624
2625 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2627 self.system_message = Some(system_message);
2628 self
2629 }
2630
2631 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2633 self.tools = Some(tools.into_iter().collect());
2634 self
2635 }
2636
2637 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2642 self.canvases = Some(canvases.into_iter().collect());
2643 self
2644 }
2645
2646 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2648 self.canvas_handler = Some(handler);
2649 self
2650 }
2651
2652 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2654 self.request_canvas_renderer = Some(request);
2655 self
2656 }
2657
2658 pub fn with_request_extensions(mut self, request: bool) -> Self {
2660 self.request_extensions = Some(request);
2661 self
2662 }
2663
2664 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2668 self.extension_sdk_path = Some(path.into());
2669 self
2670 }
2671
2672 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2674 self.extension_info = Some(extension_info);
2675 self
2676 }
2677
2678 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2681 self.canvas_provider = Some(canvas_provider);
2682 self
2683 }
2684
2685 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2687 where
2688 I: IntoIterator<Item = S>,
2689 S: Into<String>,
2690 {
2691 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2692 self
2693 }
2694
2695 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2697 where
2698 I: IntoIterator<Item = S>,
2699 S: Into<String>,
2700 {
2701 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2702 self
2703 }
2704
2705 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2707 where
2708 I: IntoIterator<Item = S>,
2709 S: Into<String>,
2710 {
2711 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2712 self
2713 }
2714
2715 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2717 self.mcp_servers = Some(servers);
2718 self
2719 }
2720
2721 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2729 self.mcp_oauth_token_storage = Some(mode.into());
2730 self
2731 }
2732
2733 pub fn with_embedding_cache_storage(
2735 mut self,
2736 embedding_cache_storage: impl Into<String>,
2737 ) -> Self {
2738 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2739 self
2740 }
2741
2742 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2745 self.enable_config_discovery = Some(enable);
2746 self
2747 }
2748
2749 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2751 self.skip_embedding_retrieval = Some(value);
2752 self
2753 }
2754
2755 pub fn with_organization_custom_instructions(
2757 mut self,
2758 instructions: impl Into<String>,
2759 ) -> Self {
2760 self.organization_custom_instructions = Some(instructions.into());
2761 self
2762 }
2763
2764 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2766 self.enable_on_demand_instruction_discovery = Some(value);
2767 self
2768 }
2769
2770 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2772 self.enable_file_hooks = Some(value);
2773 self
2774 }
2775
2776 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2778 self.enable_host_git_operations = Some(value);
2779 self
2780 }
2781
2782 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2784 self.enable_session_store = Some(value);
2785 self
2786 }
2787
2788 pub fn with_enable_skills(mut self, value: bool) -> Self {
2790 self.enable_skills = Some(value);
2791 self
2792 }
2793
2794 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2800 self.enable_mcp_apps = Some(enable);
2801 self
2802 }
2803
2804 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
2806 self.github_mcp_tool_config = Some(config);
2807 self
2808 }
2809
2810 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2812 where
2813 I: IntoIterator<Item = P>,
2814 P: Into<PathBuf>,
2815 {
2816 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2817 self
2818 }
2819
2820 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2824 where
2825 I: IntoIterator<Item = P>,
2826 P: Into<PathBuf>,
2827 {
2828 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2829 self
2830 }
2831
2832 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2834 where
2835 I: IntoIterator<Item = P>,
2836 P: Into<PathBuf>,
2837 {
2838 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2839 self
2840 }
2841
2842 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2844 self.large_output = Some(config);
2845 self
2846 }
2847
2848 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
2851 self.tool_search = Some(config);
2852 self
2853 }
2854
2855 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2857 where
2858 I: IntoIterator<Item = S>,
2859 S: Into<String>,
2860 {
2861 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2862 self
2863 }
2864
2865 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2867 mut self,
2868 agents: I,
2869 ) -> Self {
2870 self.custom_agents = Some(agents.into_iter().collect());
2871 self
2872 }
2873
2874 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2876 self.default_agent = Some(agent);
2877 self
2878 }
2879
2880 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2883 self.agent = Some(name.into());
2884 self
2885 }
2886
2887 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2890 self.infinite_sessions = Some(config);
2891 self
2892 }
2893
2894 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2896 self.provider = Some(provider);
2897 self
2898 }
2899
2900 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
2902 self.capi = Some(capi);
2903 self
2904 }
2905
2906 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
2912 self.providers = Some(providers);
2913 self
2914 }
2915
2916 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
2922 self.models = Some(models);
2923 self
2924 }
2925
2926 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2930 self.enable_session_telemetry = Some(enable);
2931 self
2932 }
2933
2934 pub fn with_enable_citations(mut self, enable: bool) -> Self {
2936 self.enable_citations = Some(enable);
2937 self
2938 }
2939
2940 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
2942 self.session_limits = Some(limits);
2943 self
2944 }
2945
2946 pub fn with_model_capabilities(
2948 mut self,
2949 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2950 ) -> Self {
2951 self.model_capabilities = Some(capabilities);
2952 self
2953 }
2954
2955 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
2957 self.memory = Some(memory);
2958 self
2959 }
2960
2961 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2963 self.config_directory = Some(dir.into());
2964 self
2965 }
2966
2967 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2970 self.working_directory = Some(dir.into());
2971 self
2972 }
2973
2974 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
2976 where
2977 I: IntoIterator<Item = P>,
2978 P: Into<PathBuf>,
2979 {
2980 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
2981 self
2982 }
2983
2984 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2989 self.github_token = Some(token.into());
2990 self
2991 }
2992
2993 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2996 self.include_sub_agent_streaming_events = Some(include);
2997 self
2998 }
2999
3000 pub fn with_remote_session(
3002 mut self,
3003 mode: crate::generated::api_types::RemoteSessionMode,
3004 ) -> Self {
3005 self.remote_session = Some(mode);
3006 self
3007 }
3008
3009 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
3011 self.cloud = Some(cloud);
3012 self
3013 }
3014
3015 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3017 self.skip_custom_instructions = Some(value);
3018 self
3019 }
3020
3021 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3023 self.custom_agents_local_only = Some(value);
3024 self
3025 }
3026
3027 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
3029 self.enable_experimental_mode = Some(enable_experimental_mode);
3030 self
3031 }
3032
3033 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3035 self.coauthor_enabled = Some(value);
3036 self
3037 }
3038
3039 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3041 self.manage_schedule_enabled = Some(value);
3042 self
3043 }
3044
3045 #[doc(hidden)]
3053 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
3054 self.exp_assignments = Some(assignments);
3055 self
3056 }
3057
3058 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3064 self.enable_managed_settings = Some(enabled);
3065 self
3066 }
3067}
3068#[derive(Clone)]
3075#[non_exhaustive]
3076pub struct ResumeSessionConfig {
3077 pub session_id: SessionId,
3079 pub model: Option<String>,
3082 pub client_name: Option<String>,
3084 pub reasoning_effort: Option<String>,
3086 pub reasoning_summary: Option<ReasoningSummary>,
3090 pub context_tier: Option<String>,
3093 pub streaming: Option<bool>,
3095 pub system_message: Option<SystemMessageConfig>,
3098 pub tools: Option<Vec<Tool>>,
3100 pub canvases: Option<Vec<CanvasDeclaration>>,
3102 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
3105 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3107 pub request_canvas_renderer: Option<bool>,
3109 pub request_extensions: Option<bool>,
3111 pub extension_sdk_path: Option<String>,
3115 pub extension_info: Option<ExtensionInfo>,
3117 pub canvas_provider: Option<CanvasProviderIdentity>,
3120 pub available_tools: Option<Vec<String>>,
3122 pub excluded_tools: Option<Vec<String>>,
3124 pub excluded_builtin_agents: Option<Vec<String>>,
3130 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3132 pub mcp_oauth_token_storage: Option<String>,
3135 pub enable_config_discovery: Option<bool>,
3138 pub skip_embedding_retrieval: Option<bool>,
3140 pub embedding_cache_storage: Option<String>,
3142 pub organization_custom_instructions: Option<String>,
3144 pub enable_on_demand_instruction_discovery: Option<bool>,
3146 pub enable_file_hooks: Option<bool>,
3148 pub enable_host_git_operations: Option<bool>,
3150 pub enable_session_store: Option<bool>,
3152 pub enable_skills: Option<bool>,
3154 pub enable_mcp_apps: Option<bool>,
3160 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
3165 pub skill_directories: Option<Vec<PathBuf>>,
3167 pub instruction_directories: Option<Vec<PathBuf>>,
3170 pub plugin_directories: Option<Vec<PathBuf>>,
3172 pub large_output: Option<LargeToolOutputConfig>,
3174 pub tool_search: Option<ToolSearchConfig>,
3177 pub disabled_skills: Option<Vec<String>>,
3179 pub hooks: Option<bool>,
3181 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3183 pub default_agent: Option<DefaultAgentConfig>,
3185 pub agent: Option<String>,
3187 pub infinite_sessions: Option<InfiniteSessionConfig>,
3189 pub provider: Option<ProviderConfig>,
3191 pub capi: Option<CapiSessionOptions>,
3197 pub providers: Option<Vec<NamedProviderConfig>>,
3203 pub models: Option<Vec<ProviderModelConfig>>,
3209 pub enable_session_telemetry: Option<bool>,
3217 pub enable_citations: Option<bool>,
3219 pub session_limits: Option<SessionLimitsConfig>,
3221 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3223 pub memory: Option<MemoryConfiguration>,
3225 pub config_directory: Option<PathBuf>,
3227 pub working_directory: Option<PathBuf>,
3229 pub additional_directories: Option<Vec<PathBuf>>,
3232 pub github_token: Option<String>,
3235 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3238 pub include_sub_agent_streaming_events: Option<bool>,
3240 pub commands: Option<Vec<CommandDefinition>>,
3244 #[doc(hidden)]
3249 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3250 pub enable_managed_settings: Option<bool>,
3256 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3261 pub suppress_resume_event: Option<bool>,
3264 pub continue_pending_work: Option<bool>,
3272 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3275 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3278 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3280 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3283 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3286 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3289 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3291 pub(crate) permission_policy: Option<crate::permission::Policy>,
3293 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3295 pub skip_custom_instructions: Option<bool>,
3297 pub custom_agents_local_only: Option<bool>,
3299 pub enable_experimental_mode: Option<bool>,
3304 pub coauthor_enabled: Option<bool>,
3306 pub manage_schedule_enabled: Option<bool>,
3308}
3309
3310impl std::fmt::Debug for ResumeSessionConfig {
3311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3312 f.debug_struct("ResumeSessionConfig")
3313 .field("session_id", &self.session_id)
3314 .field("model", &self.model)
3315 .field("client_name", &self.client_name)
3316 .field("reasoning_effort", &self.reasoning_effort)
3317 .field("reasoning_summary", &self.reasoning_summary)
3318 .field("context_tier", &self.context_tier)
3319 .field("streaming", &self.streaming)
3320 .field("system_message", &self.system_message)
3321 .field("tools", &self.tools)
3322 .field("canvases", &self.canvases)
3323 .field(
3324 "canvas_handler",
3325 &self.canvas_handler.as_ref().map(|_| "<set>"),
3326 )
3327 .field("open_canvases", &self.open_canvases)
3328 .field("request_canvas_renderer", &self.request_canvas_renderer)
3329 .field("request_extensions", &self.request_extensions)
3330 .field("extension_sdk_path", &self.extension_sdk_path)
3331 .field("extension_info", &self.extension_info)
3332 .field("canvas_provider", &self.canvas_provider)
3333 .field("available_tools", &self.available_tools)
3334 .field("excluded_tools", &self.excluded_tools)
3335 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3336 .field("mcp_servers", &self.mcp_servers)
3337 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3338 .field("embedding_cache_storage", &self.embedding_cache_storage)
3339 .field("enable_config_discovery", &self.enable_config_discovery)
3340 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3341 .field(
3342 "organization_custom_instructions",
3343 &self
3344 .organization_custom_instructions
3345 .as_ref()
3346 .map(|_| "<redacted>"),
3347 )
3348 .field(
3349 "enable_on_demand_instruction_discovery",
3350 &self.enable_on_demand_instruction_discovery,
3351 )
3352 .field("enable_file_hooks", &self.enable_file_hooks)
3353 .field(
3354 "enable_host_git_operations",
3355 &self.enable_host_git_operations,
3356 )
3357 .field("enable_session_store", &self.enable_session_store)
3358 .field("enable_skills", &self.enable_skills)
3359 .field("enable_mcp_apps", &self.enable_mcp_apps)
3360 .field("skill_directories", &self.skill_directories)
3361 .field("instruction_directories", &self.instruction_directories)
3362 .field("plugin_directories", &self.plugin_directories)
3363 .field("large_output", &self.large_output)
3364 .field("tool_search", &self.tool_search)
3365 .field("disabled_skills", &self.disabled_skills)
3366 .field("hooks", &self.hooks)
3367 .field("custom_agents", &self.custom_agents)
3368 .field("default_agent", &self.default_agent)
3369 .field("agent", &self.agent)
3370 .field("infinite_sessions", &self.infinite_sessions)
3371 .field("provider", &self.provider)
3372 .field("capi", &self.capi)
3373 .field("enable_session_telemetry", &self.enable_session_telemetry)
3374 .field("enable_citations", &self.enable_citations)
3375 .field("session_limits", &self.session_limits)
3376 .field("model_capabilities", &self.model_capabilities)
3377 .field("memory", &self.memory)
3378 .field("config_directory", &self.config_directory)
3379 .field("working_directory", &self.working_directory)
3380 .field("additional_directories", &self.additional_directories)
3381 .field(
3382 "github_token",
3383 &self.github_token.as_ref().map(|_| "<redacted>"),
3384 )
3385 .field("remote_session", &self.remote_session)
3386 .field(
3387 "include_sub_agent_streaming_events",
3388 &self.include_sub_agent_streaming_events,
3389 )
3390 .field("commands", &self.commands)
3391 .field("exp_assignments", &self.exp_assignments)
3392 .field("enable_managed_settings", &self.enable_managed_settings)
3393 .field("enable_experimental_mode", &self.enable_experimental_mode)
3394 .field(
3395 "session_fs_provider",
3396 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3397 )
3398 .field(
3399 "permission_handler",
3400 &self.permission_handler.as_ref().map(|_| "<set>"),
3401 )
3402 .field(
3403 "elicitation_handler",
3404 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3405 )
3406 .field(
3407 "user_input_handler",
3408 &self.user_input_handler.as_ref().map(|_| "<set>"),
3409 )
3410 .field(
3411 "exit_plan_mode_handler",
3412 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3413 )
3414 .field(
3415 "auto_mode_switch_handler",
3416 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3417 )
3418 .field(
3419 "hooks_handler",
3420 &self.hooks_handler.as_ref().map(|_| "<set>"),
3421 )
3422 .field(
3423 "system_message_transform",
3424 &self.system_message_transform.as_ref().map(|_| "<set>"),
3425 )
3426 .field("suppress_resume_event", &self.suppress_resume_event)
3427 .field("continue_pending_work", &self.continue_pending_work)
3428 .finish()
3429 }
3430}
3431
3432impl ResumeSessionConfig {
3433 pub(crate) fn into_wire(
3441 mut self,
3442 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3443 let permission_active =
3444 self.permission_handler.is_some() || self.permission_policy.is_some();
3445 let request_user_input = self.user_input_handler.is_some();
3446 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3447 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3448 let request_elicitation = self.elicitation_handler.is_some();
3449 let hooks_flag = self.hooks_handler.is_some();
3450
3451 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3452 if let Some(tools) = self.tools.as_mut() {
3453 for tool in tools.iter_mut() {
3454 if let Some(handler) = tool.handler.take()
3455 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3456 {
3457 return Err(crate::Error::with_message(
3458 crate::ErrorKind::InvalidConfig,
3459 format!("duplicate tool handler registered for name {:?}", tool.name),
3460 ));
3461 }
3462 }
3463 }
3464
3465 let wire_commands = self.commands.as_ref().map(|cmds| {
3466 cmds.iter()
3467 .map(|c| crate::wire::CommandWireDefinition {
3468 name: c.name.clone(),
3469 description: c.description.clone(),
3470 })
3471 .collect()
3472 });
3473 let wire_canvases = self.canvases.clone();
3474 let canvas_handler = self.canvas_handler.clone();
3475 let bearer_token_providers =
3476 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3477
3478 let wire = crate::wire::SessionResumeWire {
3479 session_id: self.session_id,
3480 model: self.model,
3481 client_name: self.client_name,
3482 reasoning_effort: self.reasoning_effort,
3483 reasoning_summary: self.reasoning_summary,
3484 context_tier: self.context_tier,
3485 streaming: self.streaming,
3486 system_message: self.system_message,
3487 tools: self.tools,
3488 canvases: wire_canvases,
3489 open_canvases: self.open_canvases,
3490 request_canvas_renderer: self.request_canvas_renderer,
3491 request_extensions: self.request_extensions,
3492 extension_sdk_path: self.extension_sdk_path,
3493 extension_info: self.extension_info,
3494 canvas_provider: self.canvas_provider,
3495 available_tools: self.available_tools,
3496 excluded_tools: self.excluded_tools,
3497 excluded_builtin_agents: self.excluded_builtin_agents,
3498 tool_filter_precedence: "excluded",
3499 mcp_servers: self.mcp_servers,
3500 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3501 embedding_cache_storage: self.embedding_cache_storage,
3502 env_value_mode: "direct",
3503 enable_config_discovery: self.enable_config_discovery,
3504 skip_embedding_retrieval: self.skip_embedding_retrieval,
3505 organization_custom_instructions: self.organization_custom_instructions,
3506 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3507 enable_file_hooks: self.enable_file_hooks,
3508 enable_host_git_operations: self.enable_host_git_operations,
3509 enable_session_store: self.enable_session_store,
3510 enable_skills: self.enable_skills,
3511 request_user_input,
3512 request_permission: permission_active,
3513 request_exit_plan_mode,
3514 request_auto_mode_switch,
3515 request_elicitation,
3516 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3517 github_mcp_tool_config: self.github_mcp_tool_config,
3518 hooks: hooks_flag,
3519 skill_directories: self.skill_directories,
3520 instruction_directories: self.instruction_directories,
3521 plugin_directories: self.plugin_directories,
3522 large_output: self.large_output,
3523 tool_search: self.tool_search,
3524 disabled_skills: self.disabled_skills,
3525 custom_agents: self.custom_agents,
3526 custom_agents_local_only: self.custom_agents_local_only,
3527 default_agent: self.default_agent,
3528 agent: self.agent,
3529 infinite_sessions: self.infinite_sessions,
3530 provider: self.provider,
3531 capi: self.capi,
3532 providers: self.providers,
3533 models: self.models,
3534 enable_session_telemetry: self.enable_session_telemetry,
3535 enable_citations: self.enable_citations,
3536 session_limits: self.session_limits,
3537 model_capabilities: self.model_capabilities,
3538 memory: self.memory,
3539 config_dir: self.config_directory,
3540 working_directory: self.working_directory,
3541 additional_directories: self.additional_directories,
3542 github_token: self.github_token,
3543 remote_session: self.remote_session,
3544 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3545 enable_github_telemetry_forwarding: None,
3546 commands: wire_commands,
3547 exp_assignments: self.exp_assignments,
3548 enable_managed_settings: self.enable_managed_settings,
3549 is_experimental_mode: self.enable_experimental_mode,
3550 suppress_resume_event: self.suppress_resume_event,
3551 continue_pending_work: self.continue_pending_work,
3552 };
3553
3554 let runtime = SessionConfigRuntime {
3555 permission_handler: self.permission_handler,
3556 permission_policy: self.permission_policy,
3557 elicitation_handler: self.elicitation_handler,
3558 mcp_auth_handler: self.mcp_auth_handler,
3559 user_input_handler: self.user_input_handler,
3560 exit_plan_mode_handler: self.exit_plan_mode_handler,
3561 auto_mode_switch_handler: self.auto_mode_switch_handler,
3562 hooks_handler: self.hooks_handler,
3563 system_message_transform: self.system_message_transform,
3564 tool_handlers,
3565 canvas_handler,
3566 session_fs_provider: self.session_fs_provider,
3567 bearer_token_providers,
3568 commands: self.commands,
3569 };
3570
3571 Ok((wire, runtime))
3572 }
3573
3574 pub fn new(session_id: SessionId) -> Self {
3579 Self {
3580 session_id,
3581 model: None,
3582 client_name: None,
3583 reasoning_effort: None,
3584 reasoning_summary: None,
3585 context_tier: None,
3586 streaming: None,
3587 system_message: None,
3588 tools: None,
3589 canvases: None,
3590 canvas_handler: None,
3591 open_canvases: None,
3592 request_canvas_renderer: None,
3593 request_extensions: None,
3594 extension_sdk_path: None,
3595 extension_info: None,
3596 canvas_provider: None,
3597 available_tools: None,
3598 excluded_tools: None,
3599 excluded_builtin_agents: None,
3600 mcp_servers: None,
3601 mcp_oauth_token_storage: None,
3602 enable_config_discovery: None,
3603 skip_embedding_retrieval: None,
3604 organization_custom_instructions: None,
3605 enable_on_demand_instruction_discovery: None,
3606 enable_file_hooks: None,
3607 enable_host_git_operations: None,
3608 enable_session_store: None,
3609 enable_skills: None,
3610 embedding_cache_storage: None,
3611 enable_mcp_apps: None,
3612 github_mcp_tool_config: None,
3613 skill_directories: None,
3614 instruction_directories: None,
3615 plugin_directories: None,
3616 large_output: None,
3617 tool_search: None,
3618 disabled_skills: None,
3619 hooks: None,
3620 custom_agents: None,
3621 default_agent: None,
3622 agent: None,
3623 infinite_sessions: None,
3624 provider: None,
3625 capi: None,
3626 providers: None,
3627 models: None,
3628 enable_session_telemetry: None,
3629 enable_citations: None,
3630 session_limits: None,
3631 model_capabilities: None,
3632 memory: None,
3633 config_directory: None,
3634 working_directory: None,
3635 additional_directories: None,
3636 github_token: None,
3637 remote_session: None,
3638 include_sub_agent_streaming_events: None,
3639 commands: None,
3640 exp_assignments: None,
3641 enable_managed_settings: None,
3642 session_fs_provider: None,
3643 suppress_resume_event: None,
3644 continue_pending_work: None,
3645 permission_handler: None,
3646 elicitation_handler: None,
3647 mcp_auth_handler: None,
3648 user_input_handler: None,
3649 exit_plan_mode_handler: None,
3650 auto_mode_switch_handler: None,
3651 hooks_handler: None,
3652 permission_policy: None,
3653 system_message_transform: None,
3654 skip_custom_instructions: None,
3655 custom_agents_local_only: None,
3656 enable_experimental_mode: None,
3657 coauthor_enabled: None,
3658 manage_schedule_enabled: None,
3659 }
3660 }
3661
3662 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3664 self.permission_handler = Some(handler);
3665 self
3666 }
3667
3668 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3670 self.elicitation_handler = Some(handler);
3671 self
3672 }
3673
3674 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3676 self.mcp_auth_handler = Some(handler);
3677 self
3678 }
3679
3680 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3682 self.user_input_handler = Some(handler);
3683 self
3684 }
3685
3686 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3688 self.exit_plan_mode_handler = Some(handler);
3689 self
3690 }
3691
3692 pub fn with_auto_mode_switch_handler(
3694 mut self,
3695 handler: Arc<dyn AutoModeSwitchHandler>,
3696 ) -> Self {
3697 self.auto_mode_switch_handler = Some(handler);
3698 self
3699 }
3700
3701 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3704 self.hooks_handler = Some(hooks);
3705 self
3706 }
3707
3708 pub fn with_system_message_transform(
3710 mut self,
3711 transform: Arc<dyn SystemMessageTransform>,
3712 ) -> Self {
3713 self.system_message_transform = Some(transform);
3714 self
3715 }
3716
3717 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3721 self.commands = Some(commands);
3722 self
3723 }
3724
3725 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3728 self.session_fs_provider = Some(provider);
3729 self
3730 }
3731
3732 pub fn approve_all_permissions(mut self) -> Self {
3735 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3736 self
3737 }
3738
3739 pub fn deny_all_permissions(mut self) -> Self {
3742 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3743 self
3744 }
3745
3746 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3749 where
3750 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3751 {
3752 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3753 self
3754 }
3755
3756 pub fn with_model(mut self, model: impl Into<String>) -> Self {
3758 self.model = Some(model.into());
3759 self
3760 }
3761
3762 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3764 self.client_name = Some(name.into());
3765 self
3766 }
3767
3768 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3770 self.reasoning_effort = Some(effort.into());
3771 self
3772 }
3773
3774 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3776 self.reasoning_summary = Some(summary);
3777 self
3778 }
3779
3780 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3783 self.context_tier = Some(tier.into());
3784 self
3785 }
3786
3787 pub fn with_streaming(mut self, streaming: bool) -> Self {
3789 self.streaming = Some(streaming);
3790 self
3791 }
3792
3793 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3796 self.system_message = Some(system_message);
3797 self
3798 }
3799
3800 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3802 self.tools = Some(tools.into_iter().collect());
3803 self
3804 }
3805
3806 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3808 self.canvases = Some(canvases.into_iter().collect());
3809 self
3810 }
3811
3812 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
3814 self.canvas_handler = Some(handler);
3815 self
3816 }
3817
3818 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
3820 mut self,
3821 open_canvases: I,
3822 ) -> Self {
3823 self.open_canvases = Some(open_canvases.into_iter().collect());
3824 self
3825 }
3826
3827 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
3829 self.request_canvas_renderer = Some(request);
3830 self
3831 }
3832
3833 pub fn with_request_extensions(mut self, request: bool) -> Self {
3835 self.request_extensions = Some(request);
3836 self
3837 }
3838
3839 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
3843 self.extension_sdk_path = Some(path.into());
3844 self
3845 }
3846
3847 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
3849 self.extension_info = Some(extension_info);
3850 self
3851 }
3852
3853 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
3856 self.canvas_provider = Some(canvas_provider);
3857 self
3858 }
3859
3860 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
3862 where
3863 I: IntoIterator<Item = S>,
3864 S: Into<String>,
3865 {
3866 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
3867 self
3868 }
3869
3870 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
3872 where
3873 I: IntoIterator<Item = S>,
3874 S: Into<String>,
3875 {
3876 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
3877 self
3878 }
3879
3880 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
3882 where
3883 I: IntoIterator<Item = S>,
3884 S: Into<String>,
3885 {
3886 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
3887 self
3888 }
3889
3890 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
3892 self.mcp_servers = Some(servers);
3893 self
3894 }
3895
3896 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3899 self.mcp_oauth_token_storage = Some(mode.into());
3900 self
3901 }
3902
3903 pub fn with_embedding_cache_storage(
3905 mut self,
3906 embedding_cache_storage: impl Into<String>,
3907 ) -> Self {
3908 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3909 self
3910 }
3911
3912 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3915 self.enable_config_discovery = Some(enable);
3916 self
3917 }
3918
3919 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3921 self.skip_embedding_retrieval = Some(value);
3922 self
3923 }
3924
3925 pub fn with_organization_custom_instructions(
3927 mut self,
3928 instructions: impl Into<String>,
3929 ) -> Self {
3930 self.organization_custom_instructions = Some(instructions.into());
3931 self
3932 }
3933
3934 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3936 self.enable_on_demand_instruction_discovery = Some(value);
3937 self
3938 }
3939
3940 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3942 self.enable_file_hooks = Some(value);
3943 self
3944 }
3945
3946 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3948 self.enable_host_git_operations = Some(value);
3949 self
3950 }
3951
3952 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3954 self.enable_session_store = Some(value);
3955 self
3956 }
3957
3958 pub fn with_enable_skills(mut self, value: bool) -> Self {
3960 self.enable_skills = Some(value);
3961 self
3962 }
3963
3964 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3970 self.enable_mcp_apps = Some(enable);
3971 self
3972 }
3973
3974 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
3976 self.github_mcp_tool_config = Some(config);
3977 self
3978 }
3979
3980 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3982 where
3983 I: IntoIterator<Item = P>,
3984 P: Into<PathBuf>,
3985 {
3986 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3987 self
3988 }
3989
3990 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3994 where
3995 I: IntoIterator<Item = P>,
3996 P: Into<PathBuf>,
3997 {
3998 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3999 self
4000 }
4001
4002 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
4004 where
4005 I: IntoIterator<Item = P>,
4006 P: Into<PathBuf>,
4007 {
4008 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
4009 self
4010 }
4011
4012 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
4014 self.large_output = Some(config);
4015 self
4016 }
4017
4018 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
4021 self.tool_search = Some(config);
4022 self
4023 }
4024
4025 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
4027 where
4028 I: IntoIterator<Item = S>,
4029 S: Into<String>,
4030 {
4031 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
4032 self
4033 }
4034
4035 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
4037 mut self,
4038 agents: I,
4039 ) -> Self {
4040 self.custom_agents = Some(agents.into_iter().collect());
4041 self
4042 }
4043
4044 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
4046 self.default_agent = Some(agent);
4047 self
4048 }
4049
4050 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
4052 self.agent = Some(name.into());
4053 self
4054 }
4055
4056 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
4058 self.infinite_sessions = Some(config);
4059 self
4060 }
4061
4062 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
4064 self.provider = Some(provider);
4065 self
4066 }
4067
4068 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
4070 self.capi = Some(capi);
4071 self
4072 }
4073
4074 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
4080 self.providers = Some(providers);
4081 self
4082 }
4083
4084 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
4090 self.models = Some(models);
4091 self
4092 }
4093
4094 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
4098 self.enable_session_telemetry = Some(enable);
4099 self
4100 }
4101
4102 pub fn with_enable_citations(mut self, enable: bool) -> Self {
4104 self.enable_citations = Some(enable);
4105 self
4106 }
4107
4108 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
4110 self.session_limits = Some(limits);
4111 self
4112 }
4113
4114 pub fn with_model_capabilities(
4116 mut self,
4117 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
4118 ) -> Self {
4119 self.model_capabilities = Some(capabilities);
4120 self
4121 }
4122
4123 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
4125 self.memory = Some(memory);
4126 self
4127 }
4128
4129 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4131 self.config_directory = Some(dir.into());
4132 self
4133 }
4134
4135 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4137 self.working_directory = Some(dir.into());
4138 self
4139 }
4140
4141 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
4143 where
4144 I: IntoIterator<Item = P>,
4145 P: Into<PathBuf>,
4146 {
4147 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
4148 self
4149 }
4150
4151 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
4155 self.github_token = Some(token.into());
4156 self
4157 }
4158
4159 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4161 self.include_sub_agent_streaming_events = Some(include);
4162 self
4163 }
4164
4165 pub fn with_remote_session(
4167 mut self,
4168 mode: crate::generated::api_types::RemoteSessionMode,
4169 ) -> Self {
4170 self.remote_session = Some(mode);
4171 self
4172 }
4173
4174 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4177 self.suppress_resume_event = Some(suppress);
4178 self
4179 }
4180
4181 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4187 self.continue_pending_work = Some(continue_pending);
4188 self
4189 }
4190
4191 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4193 self.skip_custom_instructions = Some(value);
4194 self
4195 }
4196
4197 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4199 self.custom_agents_local_only = Some(value);
4200 self
4201 }
4202
4203 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
4205 self.enable_experimental_mode = Some(enable_experimental_mode);
4206 self
4207 }
4208
4209 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4211 self.coauthor_enabled = Some(value);
4212 self
4213 }
4214
4215 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4217 self.manage_schedule_enabled = Some(value);
4218 self
4219 }
4220
4221 #[doc(hidden)]
4225 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4226 self.exp_assignments = Some(assignments);
4227 self
4228 }
4229
4230 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4233 self.enable_managed_settings = Some(enabled);
4234 self
4235 }
4236}
4237
4238#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4244#[serde(rename_all = "camelCase")]
4245#[non_exhaustive]
4246pub struct SystemMessageConfig {
4247 #[serde(skip_serializing_if = "Option::is_none")]
4249 pub mode: Option<String>,
4250 #[serde(skip_serializing_if = "Option::is_none")]
4252 pub content: Option<String>,
4253 #[serde(skip_serializing_if = "Option::is_none")]
4255 pub sections: Option<HashMap<String, SectionOverride>>,
4256}
4257
4258impl SystemMessageConfig {
4259 pub fn new() -> Self {
4262 Self::default()
4263 }
4264
4265 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4268 self.mode = Some(mode.into());
4269 self
4270 }
4271
4272 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4275 self.content = Some(content.into());
4276 self
4277 }
4278
4279 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4281 self.sections = Some(sections);
4282 self
4283 }
4284}
4285
4286#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4292#[serde(rename_all = "camelCase")]
4293pub struct SectionOverride {
4294 #[serde(skip_serializing_if = "Option::is_none")]
4297 pub action: Option<String>,
4298 #[serde(skip_serializing_if = "Option::is_none")]
4300 pub content: Option<String>,
4301}
4302
4303#[derive(Debug, Clone, Serialize, Deserialize)]
4305#[serde(rename_all = "camelCase")]
4306pub struct CreateSessionResult {
4307 pub session_id: SessionId,
4309 #[serde(skip_serializing_if = "Option::is_none")]
4311 pub workspace_path: Option<PathBuf>,
4312 #[serde(default, alias = "remote_url")]
4314 pub remote_url: Option<String>,
4315 #[serde(skip_serializing_if = "Option::is_none")]
4317 pub capabilities: Option<SessionCapabilities>,
4318}
4319
4320#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4322#[serde(rename_all = "camelCase")]
4323pub(crate) struct ResumeSessionResult {
4324 #[serde(default)]
4326 pub session_id: Option<SessionId>,
4327 #[serde(default, skip_serializing_if = "Option::is_none")]
4329 pub workspace_path: Option<PathBuf>,
4330 #[serde(default, alias = "remote_url")]
4332 pub remote_url: Option<String>,
4333 #[serde(default, skip_serializing_if = "Option::is_none")]
4335 pub capabilities: Option<SessionCapabilities>,
4336 #[serde(
4338 default,
4339 alias = "openCanvasInstances",
4340 skip_serializing_if = "Option::is_none"
4341 )]
4342 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4343}
4344
4345#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4347#[serde(rename_all = "lowercase")]
4348pub enum LogLevel {
4349 #[default]
4351 Info,
4352 Warning,
4354 Error,
4356}
4357
4358#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4363#[serde(rename_all = "camelCase")]
4364pub struct LogOptions {
4365 #[serde(skip_serializing_if = "Option::is_none")]
4367 pub level: Option<LogLevel>,
4368 #[serde(skip_serializing_if = "Option::is_none")]
4371 pub ephemeral: Option<bool>,
4372}
4373
4374impl LogOptions {
4375 pub fn with_level(mut self, level: LogLevel) -> Self {
4377 self.level = Some(level);
4378 self
4379 }
4380
4381 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4383 self.ephemeral = Some(ephemeral);
4384 self
4385 }
4386}
4387
4388#[derive(Debug, Clone, Default)]
4392pub struct SetModelOptions {
4393 pub reasoning_effort: Option<String>,
4396 pub reasoning_summary: Option<ReasoningSummary>,
4400 pub context_tier: Option<ContextTier>,
4403 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4407}
4408
4409impl SetModelOptions {
4410 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4412 self.reasoning_effort = Some(effort.into());
4413 self
4414 }
4415
4416 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4418 self.reasoning_summary = Some(summary);
4419 self
4420 }
4421
4422 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4424 self.context_tier = Some(tier);
4425 self
4426 }
4427
4428 pub fn with_model_capabilities(
4430 mut self,
4431 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4432 ) -> Self {
4433 self.model_capabilities = Some(caps);
4434 self
4435 }
4436}
4437
4438#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4445#[serde(rename_all = "camelCase")]
4446pub struct PingResponse {
4447 #[serde(default)]
4449 pub message: String,
4450 #[serde(default)]
4452 pub timestamp: String,
4453 #[serde(skip_serializing_if = "Option::is_none")]
4455 pub protocol_version: Option<u32>,
4456}
4457
4458#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4460#[serde(rename_all = "camelCase")]
4461pub struct AttachmentLineRange {
4462 pub start: u32,
4464 pub end: u32,
4466}
4467
4468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4470#[serde(rename_all = "camelCase")]
4471pub struct AttachmentSelectionPosition {
4472 pub line: u32,
4474 pub character: u32,
4476}
4477
4478#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4480#[serde(rename_all = "camelCase")]
4481pub struct AttachmentSelectionRange {
4482 pub start: AttachmentSelectionPosition,
4484 pub end: AttachmentSelectionPosition,
4486}
4487
4488#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4490#[serde(rename_all = "snake_case")]
4491#[non_exhaustive]
4492pub enum GitHubReferenceType {
4493 Issue,
4495 Pr,
4497 Discussion,
4499}
4500
4501#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4507#[serde(rename_all = "camelCase")]
4508pub struct GitHubRepoPointer {
4509 #[serde(skip_serializing_if = "Option::is_none")]
4511 pub id: Option<i64>,
4512 pub name: String,
4514 pub owner: String,
4516}
4517
4518#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4520#[serde(rename_all = "camelCase")]
4521pub struct GitHubFileDiffSide {
4522 pub path: String,
4524 pub r#ref: String,
4526 pub repo: GitHubRepoPointer,
4528}
4529
4530#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4532#[serde(rename_all = "camelCase")]
4533pub struct GitHubTreeComparisonSide {
4534 pub repo: GitHubRepoPointer,
4536 pub revision: String,
4538}
4539
4540#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4542#[serde(rename_all = "camelCase")]
4543pub struct GitHubSnippetLineRange {
4544 pub start: i64,
4546 pub end: i64,
4548}
4549
4550#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4552#[serde(
4553 tag = "type",
4554 rename_all = "camelCase",
4555 rename_all_fields = "camelCase"
4556)]
4557#[non_exhaustive]
4558pub enum Attachment {
4559 File {
4561 path: PathBuf,
4563 #[serde(skip_serializing_if = "Option::is_none")]
4565 display_name: Option<String>,
4566 #[serde(skip_serializing_if = "Option::is_none")]
4568 line_range: Option<AttachmentLineRange>,
4569 },
4570 Directory {
4572 path: PathBuf,
4574 #[serde(skip_serializing_if = "Option::is_none")]
4576 display_name: Option<String>,
4577 },
4578 Selection {
4580 file_path: PathBuf,
4582 text: String,
4584 #[serde(skip_serializing_if = "Option::is_none")]
4586 display_name: Option<String>,
4587 selection: AttachmentSelectionRange,
4589 },
4590 Blob {
4592 data: String,
4594 mime_type: String,
4596 #[serde(skip_serializing_if = "Option::is_none")]
4598 display_name: Option<String>,
4599 },
4600 #[serde(rename = "github_reference")]
4602 GitHubReference {
4603 number: u64,
4605 title: String,
4607 reference_type: GitHubReferenceType,
4609 state: String,
4611 url: String,
4613 },
4614 #[serde(rename = "github_commit")]
4616 GitHubCommit {
4617 message: String,
4619 oid: String,
4621 repo: GitHubRepoPointer,
4623 url: String,
4625 },
4626 #[serde(rename = "github_release")]
4628 GitHubRelease {
4629 name: String,
4631 repo: GitHubRepoPointer,
4633 tag_name: String,
4635 url: String,
4637 },
4638 #[serde(rename = "github_actions_job")]
4640 GitHubActionsJob {
4641 #[serde(skip_serializing_if = "Option::is_none")]
4644 conclusion: Option<String>,
4645 job_id: i64,
4647 job_name: String,
4649 repo: GitHubRepoPointer,
4651 url: String,
4653 workflow_name: String,
4655 },
4656 #[serde(rename = "github_repository")]
4658 GitHubRepository {
4659 #[serde(skip_serializing_if = "Option::is_none")]
4661 description: Option<String>,
4662 #[serde(skip_serializing_if = "Option::is_none")]
4665 r#ref: Option<String>,
4666 repo: GitHubRepoPointer,
4668 url: String,
4670 },
4671 #[serde(rename = "github_file_diff")]
4673 GitHubFileDiff {
4674 #[serde(skip_serializing_if = "Option::is_none")]
4676 base: Option<GitHubFileDiffSide>,
4677 #[serde(skip_serializing_if = "Option::is_none")]
4679 head: Option<GitHubFileDiffSide>,
4680 url: String,
4682 },
4683 #[serde(rename = "github_tree_comparison")]
4685 GitHubTreeComparison {
4686 base: GitHubTreeComparisonSide,
4688 head: GitHubTreeComparisonSide,
4690 url: String,
4692 },
4693 #[serde(rename = "github_url")]
4695 GitHubUrl {
4696 url: String,
4698 },
4699 #[serde(rename = "github_file")]
4701 GitHubFile {
4702 path: String,
4704 r#ref: String,
4706 repo: GitHubRepoPointer,
4708 url: String,
4710 },
4711 #[serde(rename = "github_snippet")]
4713 GitHubSnippet {
4714 line_range: GitHubSnippetLineRange,
4716 path: String,
4718 r#ref: String,
4720 repo: GitHubRepoPointer,
4722 url: String,
4724 },
4725}
4726
4727impl Attachment {
4728 pub fn display_name(&self) -> Option<&str> {
4730 match self {
4731 Self::File { display_name, .. }
4732 | Self::Directory { display_name, .. }
4733 | Self::Selection { display_name, .. }
4734 | Self::Blob { display_name, .. } => display_name.as_deref(),
4735 Self::GitHubReference { .. }
4736 | Self::GitHubCommit { .. }
4737 | Self::GitHubRelease { .. }
4738 | Self::GitHubActionsJob { .. }
4739 | Self::GitHubRepository { .. }
4740 | Self::GitHubFileDiff { .. }
4741 | Self::GitHubTreeComparison { .. }
4742 | Self::GitHubUrl { .. }
4743 | Self::GitHubFile { .. }
4744 | Self::GitHubSnippet { .. } => None,
4745 }
4746 }
4747
4748 pub fn label(&self) -> Option<String> {
4750 if let Some(display_name) = self
4751 .display_name()
4752 .map(str::trim)
4753 .filter(|name| !name.is_empty())
4754 {
4755 return Some(display_name.to_string());
4756 }
4757
4758 match self {
4759 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4760 format!("#{}", number)
4761 } else {
4762 title.trim().to_string()
4763 }),
4764 _ => self.derived_display_name(),
4765 }
4766 }
4767
4768 pub fn ensure_display_name(&mut self) {
4770 if self
4771 .display_name()
4772 .map(str::trim)
4773 .is_some_and(|name| !name.is_empty())
4774 {
4775 return;
4776 }
4777
4778 let Some(derived_display_name) = self.derived_display_name() else {
4779 return;
4780 };
4781
4782 match self {
4783 Self::File { display_name, .. }
4784 | Self::Directory { display_name, .. }
4785 | Self::Selection { display_name, .. }
4786 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4787 Self::GitHubReference { .. }
4788 | Self::GitHubCommit { .. }
4789 | Self::GitHubRelease { .. }
4790 | Self::GitHubActionsJob { .. }
4791 | Self::GitHubRepository { .. }
4792 | Self::GitHubFileDiff { .. }
4793 | Self::GitHubTreeComparison { .. }
4794 | Self::GitHubUrl { .. }
4795 | Self::GitHubFile { .. }
4796 | Self::GitHubSnippet { .. } => {}
4797 }
4798 }
4799
4800 fn derived_display_name(&self) -> Option<String> {
4801 match self {
4802 Self::File { path, .. } | Self::Directory { path, .. } => {
4803 Some(attachment_name_from_path(path))
4804 }
4805 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4806 Self::Blob { .. } => Some("attachment".to_string()),
4807 Self::GitHubReference { .. }
4808 | Self::GitHubCommit { .. }
4809 | Self::GitHubRelease { .. }
4810 | Self::GitHubActionsJob { .. }
4811 | Self::GitHubRepository { .. }
4812 | Self::GitHubFileDiff { .. }
4813 | Self::GitHubTreeComparison { .. }
4814 | Self::GitHubUrl { .. }
4815 | Self::GitHubFile { .. }
4816 | Self::GitHubSnippet { .. } => None,
4817 }
4818 }
4819}
4820
4821fn attachment_name_from_path(path: &Path) -> String {
4822 path.file_name()
4823 .map(|name| name.to_string_lossy().into_owned())
4824 .filter(|name| !name.is_empty())
4825 .unwrap_or_else(|| {
4826 let full = path.to_string_lossy();
4827 if full.is_empty() {
4828 "attachment".to_string()
4829 } else {
4830 full.into_owned()
4831 }
4832 })
4833}
4834
4835pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
4837 for attachment in attachments {
4838 attachment.ensure_display_name();
4839 }
4840}
4841
4842#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4847#[serde(rename_all = "lowercase")]
4848#[non_exhaustive]
4849pub enum DeliveryMode {
4850 Enqueue,
4852 Immediate,
4854}
4855
4856#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4861#[serde(rename_all = "lowercase")]
4862#[non_exhaustive]
4863pub enum AgentMode {
4864 Interactive,
4866 Plan,
4868 Autopilot,
4870 Shell,
4872}
4873
4874#[derive(Debug, Clone)]
4903#[non_exhaustive]
4904pub struct MessageOptions {
4905 pub prompt: String,
4907 pub mode: Option<DeliveryMode>,
4913 pub agent_mode: Option<AgentMode>,
4917 pub attachments: Option<Vec<Attachment>>,
4919 pub wait_timeout: Option<Duration>,
4922 pub request_headers: Option<HashMap<String, String>>,
4926 pub traceparent: Option<String>,
4933 pub tracestate: Option<String>,
4937 pub display_prompt: Option<String>,
4939}
4940
4941impl MessageOptions {
4942 pub fn new(prompt: impl Into<String>) -> Self {
4944 Self {
4945 prompt: prompt.into(),
4946 mode: None,
4947 agent_mode: None,
4948 attachments: None,
4949 wait_timeout: None,
4950 request_headers: None,
4951 traceparent: None,
4952 tracestate: None,
4953 display_prompt: None,
4954 }
4955 }
4956
4957 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
4963 self.mode = Some(mode);
4964 self
4965 }
4966
4967 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
4971 self.agent_mode = Some(agent_mode);
4972 self
4973 }
4974
4975 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
4977 self.attachments = Some(attachments);
4978 self
4979 }
4980
4981 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
4983 self.wait_timeout = Some(timeout);
4984 self
4985 }
4986
4987 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
4989 self.request_headers = Some(headers);
4990 self
4991 }
4992
4993 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
4998 self.traceparent = ctx.traceparent;
4999 self.tracestate = ctx.tracestate;
5000 self
5001 }
5002
5003 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5005 self.traceparent = Some(traceparent.into());
5006 self
5007 }
5008
5009 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5011 self.tracestate = Some(tracestate.into());
5012 self
5013 }
5014
5015 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5017 self.display_prompt = Some(display_prompt.into());
5018 self
5019 }
5020}
5021
5022impl From<&str> for MessageOptions {
5023 fn from(prompt: &str) -> Self {
5024 Self::new(prompt)
5025 }
5026}
5027
5028impl From<String> for MessageOptions {
5029 fn from(prompt: String) -> Self {
5030 Self::new(prompt)
5031 }
5032}
5033
5034impl From<&String> for MessageOptions {
5035 fn from(prompt: &String) -> Self {
5036 Self::new(prompt.clone())
5037 }
5038}
5039
5040#[derive(Debug, Clone, Serialize, Deserialize)]
5042#[serde(rename_all = "camelCase")]
5043#[non_exhaustive]
5044pub struct GetStatusResponse {
5045 pub version: String,
5047 pub protocol_version: u32,
5049}
5050
5051#[derive(Debug, Clone, Serialize, Deserialize)]
5053#[serde(rename_all = "camelCase")]
5054#[non_exhaustive]
5055pub struct GetAuthStatusResponse {
5056 pub is_authenticated: bool,
5058 #[serde(skip_serializing_if = "Option::is_none")]
5061 pub auth_type: Option<String>,
5062 #[serde(skip_serializing_if = "Option::is_none")]
5064 pub host: Option<String>,
5065 #[serde(skip_serializing_if = "Option::is_none")]
5067 pub login: Option<String>,
5068 #[serde(skip_serializing_if = "Option::is_none")]
5070 pub status_message: Option<String>,
5071}
5072
5073#[derive(Debug, Clone, Serialize, Deserialize)]
5077#[serde(rename_all = "camelCase")]
5078pub struct SessionEventNotification {
5079 pub session_id: SessionId,
5081 pub event: SessionEvent,
5083}
5084
5085#[derive(Debug, Clone, Serialize, Deserialize)]
5092#[serde(rename_all = "camelCase")]
5093pub struct SessionEvent {
5094 pub id: String,
5096 pub timestamp: String,
5098 pub parent_id: Option<String>,
5100 #[serde(skip_serializing_if = "Option::is_none")]
5102 pub ephemeral: Option<bool>,
5103 #[serde(skip_serializing_if = "Option::is_none")]
5106 pub agent_id: Option<String>,
5107 #[serde(skip_serializing_if = "Option::is_none")]
5109 pub debug_cli_received_at_ms: Option<i64>,
5110 #[serde(skip_serializing_if = "Option::is_none")]
5112 pub debug_ws_forwarded_at_ms: Option<i64>,
5113 #[serde(rename = "type")]
5115 pub event_type: String,
5116 pub data: Value,
5118}
5119
5120impl SessionEvent {
5121 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5126 use serde::de::IntoDeserializer;
5127 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5128 self.event_type.as_str().into_deserializer();
5129 crate::generated::SessionEventType::deserialize(deserializer)
5130 .unwrap_or(crate::generated::SessionEventType::Unknown)
5131 }
5132
5133 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5139 serde_json::from_value(self.data.clone()).ok()
5140 }
5141
5142 pub fn is_transient_error(&self) -> bool {
5146 self.event_type == "session.error"
5147 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5148 }
5149}
5150
5151#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5156#[serde(rename_all = "camelCase")]
5157#[non_exhaustive]
5158pub struct ToolInvocation {
5159 pub session_id: SessionId,
5161 pub tool_call_id: String,
5163 pub tool_name: String,
5165 pub arguments: Value,
5167 #[serde(skip)]
5175 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5176 #[serde(default, skip_serializing_if = "Option::is_none")]
5181 pub traceparent: Option<String>,
5182 #[serde(default, skip_serializing_if = "Option::is_none")]
5185 pub tracestate: Option<String>,
5186}
5187
5188impl ToolInvocation {
5189 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5210 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5211 }
5212
5213 pub fn trace_context(&self) -> TraceContext {
5216 TraceContext {
5217 traceparent: self.traceparent.clone(),
5218 tracestate: self.tracestate.clone(),
5219 }
5220 }
5221}
5222
5223#[derive(Debug, Clone, Serialize, Deserialize)]
5225#[serde(rename_all = "camelCase")]
5226pub struct ToolBinaryResult {
5227 pub data: String,
5229 pub mime_type: String,
5231 pub r#type: String,
5233 #[serde(default, skip_serializing_if = "Option::is_none")]
5235 pub description: Option<String>,
5236}
5237
5238#[derive(Debug, Clone, Serialize, Deserialize)]
5245#[serde(rename_all = "camelCase")]
5246#[non_exhaustive]
5247pub struct ToolResultExpanded {
5248 pub text_result_for_llm: String,
5250 pub result_type: String,
5252 #[serde(default, skip_serializing_if = "Option::is_none")]
5254 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5255 #[serde(skip_serializing_if = "Option::is_none")]
5257 pub session_log: Option<String>,
5258 #[serde(skip_serializing_if = "Option::is_none")]
5260 pub error: Option<String>,
5261 #[serde(default, skip_serializing_if = "Option::is_none")]
5263 pub tool_telemetry: Option<HashMap<String, Value>>,
5264 #[serde(default, skip_serializing_if = "Option::is_none")]
5266 pub tool_references: Option<Vec<String>>,
5267}
5268
5269impl ToolResultExpanded {
5270 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5274 Self {
5275 text_result_for_llm: text_result_for_llm.into(),
5276 result_type: result_type.into(),
5277 binary_results_for_llm: None,
5278 session_log: None,
5279 error: None,
5280 tool_telemetry: None,
5281 tool_references: None,
5282 }
5283 }
5284
5285 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5287 self.binary_results_for_llm = Some(results);
5288 self
5289 }
5290
5291 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5293 self.session_log = Some(session_log.into());
5294 self
5295 }
5296
5297 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5299 self.error = Some(error.into());
5300 self
5301 }
5302
5303 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5305 self.tool_telemetry = Some(telemetry);
5306 self
5307 }
5308
5309 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5311 where
5312 I: IntoIterator<Item = S>,
5313 S: Into<String>,
5314 {
5315 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5316 self
5317 }
5318}
5319
5320#[derive(Debug, Clone, Serialize, Deserialize)]
5322#[serde(untagged)]
5323#[non_exhaustive]
5324pub enum ToolResult {
5325 Text(String),
5327 Expanded(ToolResultExpanded),
5329}
5330
5331#[derive(Debug, Clone, Serialize, Deserialize)]
5333#[serde(rename_all = "camelCase")]
5334pub struct ToolResultResponse {
5335 pub result: ToolResult,
5337}
5338
5339#[derive(Debug, Clone, Serialize, Deserialize)]
5341#[serde(rename_all = "camelCase")]
5342pub struct SessionMetadata {
5343 pub session_id: SessionId,
5345 pub start_time: String,
5347 pub modified_time: String,
5349 #[serde(skip_serializing_if = "Option::is_none")]
5351 pub summary: Option<String>,
5352 pub is_remote: bool,
5354}
5355
5356#[derive(Debug, Clone, Serialize, Deserialize)]
5358#[serde(rename_all = "camelCase")]
5359pub struct ListSessionsResponse {
5360 pub sessions: Vec<SessionMetadata>,
5362}
5363
5364#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5368#[serde(rename_all = "camelCase")]
5369pub struct SessionListFilter {
5370 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5372 pub working_directory: Option<String>,
5373 #[serde(default, skip_serializing_if = "Option::is_none")]
5375 pub git_root: Option<String>,
5376 #[serde(default, skip_serializing_if = "Option::is_none")]
5378 pub repository: Option<String>,
5379 #[serde(default, skip_serializing_if = "Option::is_none")]
5381 pub branch: Option<String>,
5382}
5383
5384#[derive(Debug, Clone, Serialize, Deserialize)]
5386#[serde(rename_all = "camelCase")]
5387pub struct GetSessionMetadataResponse {
5388 #[serde(skip_serializing_if = "Option::is_none")]
5390 pub session: Option<SessionMetadata>,
5391}
5392
5393#[derive(Debug, Clone, Serialize, Deserialize)]
5395#[serde(rename_all = "camelCase")]
5396pub struct GetLastSessionIdResponse {
5397 #[serde(skip_serializing_if = "Option::is_none")]
5399 pub session_id: Option<SessionId>,
5400}
5401
5402#[derive(Debug, Clone, Serialize, Deserialize)]
5404#[serde(rename_all = "camelCase")]
5405pub struct GetForegroundSessionResponse {
5406 #[serde(skip_serializing_if = "Option::is_none")]
5408 pub session_id: Option<SessionId>,
5409}
5410
5411#[derive(Debug, Clone, Serialize, Deserialize)]
5413#[serde(rename_all = "camelCase")]
5414pub struct GetMessagesResponse {
5415 pub events: Vec<SessionEvent>,
5417}
5418
5419#[derive(Debug, Clone, Serialize, Deserialize)]
5421#[serde(rename_all = "camelCase")]
5422pub struct ElicitationResult {
5423 pub action: String,
5425 #[serde(skip_serializing_if = "Option::is_none")]
5427 pub content: Option<Value>,
5428}
5429
5430#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5436#[serde(rename_all = "camelCase")]
5437#[non_exhaustive]
5438pub enum ElicitationMode {
5439 Form,
5441 Url,
5443 #[serde(other)]
5445 Unknown,
5446}
5447
5448#[derive(Debug, Clone, Serialize, Deserialize)]
5455#[serde(rename_all = "camelCase")]
5456pub struct ElicitationRequest {
5457 pub message: String,
5459 #[serde(skip_serializing_if = "Option::is_none")]
5461 pub requested_schema: Option<Value>,
5462 #[serde(skip_serializing_if = "Option::is_none")]
5464 pub mode: Option<ElicitationMode>,
5465 #[serde(skip_serializing_if = "Option::is_none")]
5467 pub elicitation_source: Option<String>,
5468 #[serde(skip_serializing_if = "Option::is_none")]
5470 pub url: Option<String>,
5471}
5472
5473#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5478#[serde(rename_all = "camelCase")]
5479pub struct SessionCapabilities {
5480 #[serde(skip_serializing_if = "Option::is_none")]
5482 pub ui: Option<UiCapabilities>,
5483}
5484
5485#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5487#[serde(rename_all = "camelCase")]
5488pub struct UiCapabilities {
5489 #[serde(skip_serializing_if = "Option::is_none")]
5491 pub elicitation: Option<bool>,
5492 #[serde(skip_serializing_if = "Option::is_none")]
5503 pub mcp_apps: Option<bool>,
5504 #[serde(skip_serializing_if = "Option::is_none")]
5506 pub canvases: Option<bool>,
5507}
5508
5509#[derive(Debug, Clone, Default)]
5511pub struct UiInputOptions<'a> {
5512 pub title: Option<&'a str>,
5514 pub description: Option<&'a str>,
5516 pub min_length: Option<u64>,
5518 pub max_length: Option<u64>,
5520 pub format: Option<InputFormat>,
5522 pub default: Option<&'a str>,
5524}
5525
5526#[derive(Debug, Clone, Copy)]
5528#[non_exhaustive]
5529pub enum InputFormat {
5530 Email,
5532 Uri,
5534 Date,
5536 DateTime,
5538}
5539
5540impl InputFormat {
5541 pub fn as_str(&self) -> &'static str {
5543 match self {
5544 Self::Email => "email",
5545 Self::Uri => "uri",
5546 Self::Date => "date",
5547 Self::DateTime => "date-time",
5548 }
5549 }
5550}
5551
5552pub use crate::generated::api_types::{
5557 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5558 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5559 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5560 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
5561};
5562
5563#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5569#[serde(rename_all = "kebab-case")]
5570#[non_exhaustive]
5571pub enum PermissionRequestKind {
5572 Shell,
5574 Write,
5576 Read,
5578 Url,
5580 Mcp,
5582 CustomTool,
5584 Memory,
5586 Hook,
5588 #[serde(other)]
5591 Unknown,
5592}
5593
5594#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5600#[serde(rename_all = "camelCase")]
5601pub struct PermissionRequestData {
5602 #[serde(default, skip_serializing_if = "Option::is_none")]
5606 pub kind: Option<PermissionRequestKind>,
5607 #[serde(default, skip_serializing_if = "Option::is_none")]
5610 pub tool_call_id: Option<String>,
5611 #[serde(default, skip_serializing_if = "Option::is_none")]
5613 pub managed_approval_required: Option<bool>,
5614 #[serde(default, skip_serializing_if = "is_false")]
5616 pub managed_settings_enabled: bool,
5617 #[serde(flatten)]
5621 pub extra: Value,
5622}
5623
5624#[derive(Debug, Clone, Serialize, Deserialize)]
5626#[serde(rename_all = "camelCase")]
5627pub struct ExitPlanModeData {
5628 #[serde(default)]
5630 pub summary: String,
5631 #[serde(default, skip_serializing_if = "Option::is_none")]
5633 pub plan_content: Option<String>,
5634 #[serde(default)]
5636 pub actions: Vec<String>,
5637 #[serde(default = "default_recommended_action")]
5639 pub recommended_action: String,
5640}
5641
5642fn default_recommended_action() -> String {
5643 "autopilot".to_string()
5644}
5645
5646impl Default for ExitPlanModeData {
5647 fn default() -> Self {
5648 Self {
5649 summary: String::new(),
5650 plan_content: None,
5651 actions: Vec::new(),
5652 recommended_action: default_recommended_action(),
5653 }
5654 }
5655}
5656
5657#[cfg(test)]
5658mod tests {
5659 use std::collections::HashMap;
5660 use std::path::PathBuf;
5661
5662 use serde_json::json;
5663
5664 use super::{
5665 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5666 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5667 CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry,
5668 ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
5669 InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
5670 MemoryConfiguration, NamedProviderConfig, ProviderConfig, ProviderModelConfig,
5671 ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, SessionId,
5672 SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
5673 ToolResultResponse, ensure_attachment_display_names,
5674 };
5675 use crate::generated::session_events::TypedSessionEvent;
5676
5677 #[test]
5678 fn tool_builder_composes() {
5679 let tool = Tool::new("greet")
5680 .with_description("Say hello")
5681 .with_namespaced_name("hello/greet")
5682 .with_instructions("Pass the user's name")
5683 .with_parameters(json!({
5684 "type": "object",
5685 "properties": { "name": { "type": "string" } },
5686 "required": ["name"]
5687 }))
5688 .with_overrides_built_in_tool(true)
5689 .with_skip_permission(true);
5690 assert_eq!(tool.name, "greet");
5691 assert_eq!(tool.description, "Say hello");
5692 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5693 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5694 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5695 assert!(tool.overrides_built_in_tool);
5696 assert!(tool.skip_permission);
5697 }
5698
5699 #[test]
5700 fn tool_defer_serialization() {
5701 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5702 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5703 let value = serde_json::to_value(&tool).unwrap();
5704 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5705
5706 let plain = Tool::new("plain");
5707 let value = serde_json::to_value(&plain).unwrap();
5708 assert!(value.get("defer").is_none());
5709 }
5710
5711 #[test]
5712 fn tool_metadata_serialization() {
5713 use indexmap::IndexMap;
5714
5715 let mut metadata = IndexMap::new();
5716 metadata.insert(
5717 "github.com/copilot:safeForTelemetry".to_string(),
5718 json!({ "name": true, "inputsNames": false }),
5719 );
5720 let tool = Tool::new("lookup").with_metadata(metadata);
5721 let value = serde_json::to_value(&tool).unwrap();
5722 assert_eq!(
5723 value
5724 .get("metadata")
5725 .unwrap()
5726 .get("github.com/copilot:safeForTelemetry")
5727 .unwrap(),
5728 &json!({ "name": true, "inputsNames": false })
5729 );
5730
5731 let plain = Tool::new("plain");
5733 let value = serde_json::to_value(&plain).unwrap();
5734 assert!(value.get("metadata").is_none());
5735 }
5736
5737 #[test]
5738 fn custom_agent_config_builder_with_model() {
5739 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5740 .with_model("claude-haiku-4.5")
5741 .with_display_name("My Agent");
5742 assert_eq!(agent.name, "my-agent");
5743 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5744 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5745 }
5746
5747 #[test]
5748 fn custom_agent_config_serializes_model() {
5749 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5750 let wire = serde_json::to_value(&agent).unwrap();
5751 assert_eq!(wire["model"], "claude-haiku-4.5");
5752 assert_eq!(wire["name"], "model-agent");
5753 }
5754
5755 #[test]
5756 fn custom_agent_config_omits_model_when_none() {
5757 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5758 let wire = serde_json::to_value(&agent).unwrap();
5759 assert!(wire.get("model").is_none());
5760 }
5761
5762 #[test]
5763 fn custom_agent_config_builder_with_reasoning_effort() {
5764 let agent =
5765 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5766 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
5767 }
5768
5769 #[test]
5770 fn custom_agent_config_serializes_reasoning_effort() {
5771 let agent =
5772 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5773 let wire = serde_json::to_value(&agent).unwrap();
5774 assert_eq!(wire["reasoningEffort"], "high");
5775 }
5776
5777 #[test]
5778 fn custom_agent_config_omits_reasoning_effort_when_none() {
5779 let agent = CustomAgentConfig::new("default-agent", "prompt");
5780 let wire = serde_json::to_value(&agent).unwrap();
5781 assert!(wire.get("reasoningEffort").is_none());
5782 }
5783
5784 #[test]
5785 #[should_panic(expected = "tool parameter schema must be a JSON object")]
5786 fn tool_with_parameters_panics_on_non_object_value() {
5787 let _ = Tool::new("noop").with_parameters(json!(null));
5788 }
5789
5790 #[test]
5791 fn tool_result_expanded_serializes_binary_results_for_llm() {
5792 let response = ToolResultResponse {
5793 result: ToolResult::Expanded(ToolResultExpanded {
5794 text_result_for_llm: "rendered chart".to_string(),
5795 result_type: "success".to_string(),
5796 binary_results_for_llm: Some(vec![ToolBinaryResult {
5797 data: "aW1n".to_string(),
5798 mime_type: "image/png".to_string(),
5799 r#type: "image".to_string(),
5800 description: Some("chart preview".to_string()),
5801 }]),
5802 session_log: None,
5803 error: None,
5804 tool_telemetry: None,
5805 tool_references: None,
5806 }),
5807 };
5808
5809 let wire = serde_json::to_value(&response).unwrap();
5810
5811 assert_eq!(
5812 wire,
5813 json!({
5814 "result": {
5815 "textResultForLlm": "rendered chart",
5816 "resultType": "success",
5817 "binaryResultsForLlm": [
5818 {
5819 "data": "aW1n",
5820 "mimeType": "image/png",
5821 "type": "image",
5822 "description": "chart preview"
5823 }
5824 ]
5825 }
5826 })
5827 );
5828 }
5829
5830 #[test]
5831 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
5832 let response = ToolResultResponse {
5833 result: ToolResult::Expanded(ToolResultExpanded {
5834 text_result_for_llm: "ok".to_string(),
5835 result_type: "success".to_string(),
5836 binary_results_for_llm: None,
5837 session_log: None,
5838 error: None,
5839 tool_telemetry: None,
5840 tool_references: None,
5841 }),
5842 };
5843
5844 let wire = serde_json::to_value(&response).unwrap();
5845
5846 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5847 assert!(wire["result"].get("binaryResultsForLlm").is_none());
5848 }
5849
5850 #[test]
5851 fn tool_result_expanded_serializes_tool_references() {
5852 let response = ToolResultResponse {
5853 result: ToolResult::Expanded(
5854 ToolResultExpanded::new("found 2 tools", "success")
5855 .with_tool_references(["get_weather", "check_status"]),
5856 ),
5857 };
5858
5859 let wire = serde_json::to_value(&response).unwrap();
5860
5861 assert_eq!(
5862 wire,
5863 json!({
5864 "result": {
5865 "textResultForLlm": "found 2 tools",
5866 "resultType": "success",
5867 "toolReferences": ["get_weather", "check_status"]
5868 }
5869 })
5870 );
5871 }
5872
5873 #[test]
5874 fn tool_result_expanded_omits_tool_references_when_none() {
5875 let response = ToolResultResponse {
5876 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
5877 };
5878
5879 let wire = serde_json::to_value(&response).unwrap();
5880
5881 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5882 assert!(wire["result"].get("toolReferences").is_none());
5883 }
5884
5885 #[test]
5886 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
5887 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
5890 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
5891
5892 assert_eq!(
5893 expanded.tool_references.as_deref(),
5894 Some(["alpha".to_string(), "beta".to_string()].as_slice())
5895 );
5896 }
5897
5898 #[test]
5899 fn tool_result_expanded_deserializes_tool_references() {
5900 let wire = json!({
5901 "textResultForLlm": "found tools",
5902 "resultType": "success",
5903 "toolReferences": ["alpha", "beta"]
5904 });
5905
5906 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
5907
5908 assert_eq!(
5909 expanded.tool_references.as_deref(),
5910 Some(["alpha".to_string(), "beta".to_string()].as_slice())
5911 );
5912 }
5913
5914 #[test]
5915 fn session_config_default_wire_flags_off_without_handlers() {
5916 let cfg = SessionConfig::default();
5917 assert_eq!(cfg.mcp_oauth_token_storage, None);
5918 let (wire, _runtime) = cfg
5922 .into_wire(Some(SessionId::from("default-flags")))
5923 .expect("default config has no duplicate handlers");
5924 assert!(!wire.request_user_input);
5925 assert!(!wire.request_permission);
5926 assert!(!wire.request_elicitation);
5927 assert!(!wire.request_exit_plan_mode);
5928 assert!(!wire.request_auto_mode_switch);
5929 assert!(!wire.hooks);
5930 assert!(!wire.request_mcp_apps);
5931 }
5932
5933 #[test]
5934 fn resume_session_config_new_wire_flags_off_without_handlers() {
5935 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
5936 assert_eq!(cfg.mcp_oauth_token_storage, None);
5937 let (wire, _runtime) = cfg
5938 .into_wire()
5939 .expect("default resume config has no duplicate handlers");
5940 assert!(!wire.request_user_input);
5941 assert!(!wire.request_permission);
5942 assert!(!wire.request_elicitation);
5943 assert!(!wire.request_exit_plan_mode);
5944 assert!(!wire.request_auto_mode_switch);
5945 assert!(!wire.hooks);
5946 assert!(!wire.request_mcp_apps);
5947 }
5948
5949 #[test]
5950 fn custom_agents_local_only_serializes_on_create_and_resume() {
5951 let (create_wire, _) = SessionConfig::default()
5952 .with_custom_agents_local_only(false)
5953 .into_wire(Some(SessionId::from("create-locality")))
5954 .expect("create config has no duplicate handlers");
5955 let create_json = serde_json::to_value(&create_wire).unwrap();
5956 assert_eq!(create_json["customAgentsLocalOnly"], false);
5957
5958 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
5959 .with_custom_agents_local_only(false)
5960 .into_wire()
5961 .expect("resume config has no duplicate handlers");
5962 let resume_json = serde_json::to_value(&resume_wire).unwrap();
5963 assert_eq!(resume_json["customAgentsLocalOnly"], false);
5964
5965 let (unset_create_wire, _) = SessionConfig::default()
5966 .into_wire(Some(SessionId::from("create-unset")))
5967 .expect("create config has no duplicate handlers");
5968 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
5969 assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
5970
5971 let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
5972 .into_wire()
5973 .expect("resume config has no duplicate handlers");
5974 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
5975 assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
5976 }
5977
5978 #[test]
5979 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5980 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
5981 assert_eq!(cfg.enable_mcp_apps, Some(true));
5982
5983 let (wire, _runtime) = cfg
5984 .into_wire(Some(SessionId::from("enable-mcp-apps")))
5985 .expect("enable_mcp_apps config has no duplicate handlers");
5986 assert!(wire.request_mcp_apps);
5987
5988 let json = serde_json::to_value(&wire).unwrap();
5989 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5990 }
5991
5992 #[test]
5993 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5994 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
5995 .with_enable_mcp_apps(true);
5996 assert_eq!(cfg.enable_mcp_apps, Some(true));
5997
5998 let (wire, _runtime) = cfg
5999 .into_wire()
6000 .expect("resume enable_mcp_apps config has no duplicate handlers");
6001 assert!(wire.request_mcp_apps);
6002
6003 let json = serde_json::to_value(&wire).unwrap();
6004 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6005 }
6006
6007 #[test]
6008 fn github_mcp_tool_config_serializes_for_create_and_resume() {
6009 let github_config = GitHubMcpToolConfig::new()
6010 .with_enable_all_tools(true)
6011 .with_additional_toolsets(["repos"])
6012 .with_additional_tools(["get_issue"])
6013 .with_enable_insiders_mode(true)
6014 .with_disable_form_deferral(true);
6015
6016 let (create_wire, _) = SessionConfig::default()
6017 .with_github_mcp_tool_config(github_config.clone())
6018 .into_wire(Some(SessionId::from("github-mcp")))
6019 .expect("create config has no duplicate handlers");
6020 assert_eq!(
6021 serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6022 serde_json::json!({
6023 "enableAllTools": true,
6024 "additionalToolsets": ["repos"],
6025 "additionalTools": ["get_issue"],
6026 "enableInsidersMode": true,
6027 "disableFormDeferral": true,
6028 })
6029 );
6030
6031 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6032 .with_github_mcp_tool_config(github_config)
6033 .into_wire()
6034 .expect("resume config has no duplicate handlers");
6035 assert!(resume_wire.github_mcp_tool_config.is_some());
6036
6037 let (unset_wire, _) = SessionConfig::default()
6038 .into_wire(Some(SessionId::from("github-mcp-unset")))
6039 .expect("default config has no duplicate handlers");
6040 assert!(
6041 serde_json::to_value(&unset_wire)
6042 .unwrap()
6043 .get("githubMcpToolConfig")
6044 .is_none()
6045 );
6046 }
6047
6048 #[test]
6049 fn memory_configuration_constructors_and_serde() {
6050 assert!(MemoryConfiguration::enabled().enabled);
6051 assert!(!MemoryConfiguration::disabled().enabled);
6052 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6053
6054 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6055 assert_eq!(json, serde_json::json!({ "enabled": true }));
6056 }
6057
6058 #[test]
6059 fn session_config_with_memory_serializes() {
6060 let (wire, _runtime) = SessionConfig::default()
6061 .with_memory(MemoryConfiguration::enabled())
6062 .into_wire(Some(SessionId::from("memory-on")))
6063 .expect("no duplicate handlers");
6064 let json = serde_json::to_value(&wire).unwrap();
6065 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6066
6067 let (wire_off, _) = SessionConfig::default()
6068 .with_memory(MemoryConfiguration::disabled())
6069 .into_wire(Some(SessionId::from("memory-off")))
6070 .expect("no duplicate handlers");
6071 let json_off = serde_json::to_value(&wire_off).unwrap();
6072 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6073
6074 let (empty_wire, _) = SessionConfig::default()
6076 .into_wire(Some(SessionId::from("memory-unset")))
6077 .expect("no duplicate handlers");
6078 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6079 assert!(empty_json.get("memory").is_none());
6080 }
6081
6082 #[test]
6083 fn resume_session_config_with_memory_serializes() {
6084 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6085 .with_memory(MemoryConfiguration::enabled())
6086 .into_wire()
6087 .expect("no duplicate handlers");
6088 let json = serde_json::to_value(&wire).unwrap();
6089 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6090
6091 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6093 .into_wire()
6094 .expect("no duplicate handlers");
6095 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6096 assert!(empty_json.get("memory").is_none());
6097 }
6098
6099 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6100 CopilotExpAssignmentResponse {
6101 features: vec!["copilot_exp_flag".to_string()],
6102 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6103 configs: vec![ExpConfigEntry {
6104 id: "cfg-1".to_string(),
6105 parameters: HashMap::from([
6106 ("threshold".to_string(), ExpFlagValue::Integer(5)),
6107 ("enabled".to_string(), ExpFlagValue::Bool(true)),
6108 ]),
6109 }],
6110 assignment_context: context.to_string(),
6111 ..Default::default()
6112 }
6113 }
6114
6115 #[test]
6116 fn exp_flag_value_round_trips_all_variants() {
6117 let values = serde_json::json!({
6118 "s": "text",
6119 "i": 7,
6120 "f": 1.5,
6121 "b": true,
6122 "n": null,
6123 });
6124 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6125 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6126 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6127 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6128 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6129 assert_eq!(parsed["n"], ExpFlagValue::Null);
6130 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6131 }
6132
6133 #[test]
6134 fn session_config_with_exp_assignments_serializes() {
6135 let assignments = sample_exp_assignments("ctx-123");
6136 let expected = serde_json::to_value(&assignments).unwrap();
6137 let (wire, _runtime) = SessionConfig::default()
6138 .with_exp_assignments(assignments)
6139 .into_wire(Some(SessionId::from("exp-on")))
6140 .expect("no duplicate handlers");
6141 let json = serde_json::to_value(&wire).unwrap();
6142 assert_eq!(json["expAssignments"], expected);
6143 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6144 assert_eq!(
6145 json["expAssignments"]["Flights"]["copilot_exp_flag"],
6146 "treatment"
6147 );
6148
6149 let (empty_wire, _) = SessionConfig::default()
6151 .into_wire(Some(SessionId::from("exp-unset")))
6152 .expect("no duplicate handlers");
6153 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6154 assert!(empty_json.get("expAssignments").is_none());
6155 }
6156
6157 #[test]
6158 fn resume_session_config_with_exp_assignments_serializes() {
6159 let assignments = sample_exp_assignments("ctx-456");
6160 let expected = serde_json::to_value(&assignments).unwrap();
6161 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6162 .with_exp_assignments(assignments)
6163 .into_wire()
6164 .expect("no duplicate handlers");
6165 let json = serde_json::to_value(&wire).unwrap();
6166 assert_eq!(json["expAssignments"], expected);
6167
6168 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6170 .into_wire()
6171 .expect("no duplicate handlers");
6172 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6173 assert!(empty_json.get("expAssignments").is_none());
6174 }
6175
6176 #[test]
6177 fn session_config_clone_preserves_exp_assignments() {
6178 let assignments = sample_exp_assignments("ctx-clone");
6179 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6180 let cloned = config.clone();
6181
6182 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6183
6184 let (wire, _runtime) = cloned
6185 .into_wire(Some(SessionId::from("exp-clone")))
6186 .expect("no duplicate handlers");
6187 let json = serde_json::to_value(&wire).unwrap();
6188 assert_eq!(
6189 json["expAssignments"],
6190 serde_json::to_value(&assignments).unwrap()
6191 );
6192 }
6193
6194 #[test]
6195 fn resume_session_config_clone_preserves_exp_assignments() {
6196 let assignments = sample_exp_assignments("ctx-clone-resume");
6197 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6198 .with_exp_assignments(assignments.clone());
6199 let cloned = config.clone();
6200
6201 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6202
6203 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6204 let json = serde_json::to_value(&wire).unwrap();
6205 assert_eq!(
6206 json["expAssignments"],
6207 serde_json::to_value(&assignments).unwrap()
6208 );
6209 }
6210
6211 #[test]
6212 #[allow(clippy::field_reassign_with_default)]
6213 fn session_config_into_wire_serializes_bucket_b_fields() {
6214 use std::path::PathBuf;
6215
6216 use super::{CloudSessionOptions, CloudSessionRepository};
6217
6218 let mut cfg = SessionConfig::default();
6219 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6220 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6221 cfg.github_token = Some("ghs_secret".to_string());
6222 cfg.include_sub_agent_streaming_events = Some(false);
6223 cfg.enable_session_telemetry = Some(false);
6224 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
6225 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
6226 cfg.enable_on_demand_instruction_discovery = Some(false);
6227 cfg.cloud = Some(CloudSessionOptions::with_repository(
6228 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
6229 ));
6230
6231 let (wire, _runtime) = cfg
6232 .into_wire(Some(SessionId::from("custom-id")))
6233 .expect("no duplicate handlers");
6234 let wire_json = serde_json::to_value(&wire).unwrap();
6235 assert_eq!(wire_json["sessionId"], "custom-id");
6236 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6237 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6238 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6239 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6240 assert_eq!(wire_json["enableSessionTelemetry"], false);
6241 assert_eq!(wire_json["reasoningSummary"], "concise");
6242 assert_eq!(wire_json["remoteSession"], "export");
6243 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6244 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6245 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6246 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6247
6248 let (empty_wire, _) = SessionConfig::default()
6250 .into_wire(Some(SessionId::from("empty")))
6251 .expect("default has no duplicate handlers");
6252 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6253 assert!(empty_json.get("gitHubToken").is_none());
6254 assert!(empty_json.get("enableSessionTelemetry").is_none());
6255 assert!(empty_json.get("reasoningSummary").is_none());
6256 assert!(empty_json.get("remoteSession").is_none());
6257 assert!(
6258 empty_json
6259 .get("enableOnDemandInstructionDiscovery")
6260 .is_none()
6261 );
6262 assert!(empty_json.get("cloud").is_none());
6263 }
6264
6265 #[test]
6266 fn session_config_into_wire_serializes_named_providers_and_models() {
6267 let cfg = SessionConfig::default()
6268 .with_providers(vec![
6269 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6270 .with_provider_type("openai")
6271 .with_wire_api("responses")
6272 .with_api_key("sk-test"),
6273 ])
6274 .with_models(vec![
6275 ProviderModelConfig::new("gpt-x", "my-openai")
6276 .with_wire_model("gpt-x-2025")
6277 .with_max_output_tokens(2048),
6278 ]);
6279
6280 let (wire, _) = cfg
6281 .into_wire(Some(SessionId::from("sess-providers")))
6282 .expect("no duplicate handlers");
6283 let wire_json = serde_json::to_value(&wire).unwrap();
6284 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6285 assert_eq!(
6286 wire_json["providers"][0]["baseUrl"],
6287 "https://api.example.com/v1"
6288 );
6289 assert_eq!(wire_json["providers"][0]["type"], "openai");
6290 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6291 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6292 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
6293 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
6294 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
6295 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
6296
6297 let (empty_wire, _) = SessionConfig::default()
6298 .into_wire(Some(SessionId::from("empty")))
6299 .expect("default has no duplicate handlers");
6300 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6301 assert!(empty_json.get("providers").is_none());
6302 assert!(empty_json.get("models").is_none());
6303 }
6304
6305 #[test]
6306 fn resume_config_into_wire_serializes_named_providers_and_models() {
6307 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
6308 .with_providers(vec![
6309 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
6310 .with_provider_type("azure")
6311 .with_azure(AzureProviderOptions {
6312 api_version: Some("2024-10-21".to_string()),
6313 }),
6314 ])
6315 .with_models(vec![
6316 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
6317 ]);
6318
6319 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6320 let wire_json = serde_json::to_value(&wire).unwrap();
6321 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
6322 assert_eq!(wire_json["providers"][0]["type"], "azure");
6323 assert_eq!(
6324 wire_json["providers"][0]["azure"]["apiVersion"],
6325 "2024-10-21"
6326 );
6327 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
6328 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
6329 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
6330
6331 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
6332 .into_wire()
6333 .expect("default has no duplicate handlers");
6334 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6335 assert!(empty_json.get("providers").is_none());
6336 assert!(empty_json.get("models").is_none());
6337 }
6338
6339 #[test]
6340 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
6341 use std::path::PathBuf;
6342
6343 let cfg = SessionConfig {
6344 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
6345 large_output: Some(
6346 LargeToolOutputConfig::new()
6347 .with_enabled(true)
6348 .with_max_size_bytes(1024)
6349 .with_output_directory(PathBuf::from("/tmp/large-output")),
6350 ),
6351 ..Default::default()
6352 };
6353
6354 let (wire, _) = cfg
6355 .into_wire(Some(SessionId::from("sess-1")))
6356 .expect("no duplicate handlers");
6357 let wire_json = serde_json::to_value(&wire).unwrap();
6358 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
6359 assert_eq!(wire_json["largeOutput"]["enabled"], true);
6360 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
6361 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
6362
6363 let (empty_wire, _) = SessionConfig::default()
6364 .into_wire(Some(SessionId::from("empty")))
6365 .expect("default has no duplicate handlers");
6366 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6367 assert!(empty_json.get("pluginDirectories").is_none());
6368 assert!(empty_json.get("largeOutput").is_none());
6369 }
6370
6371 #[test]
6372 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
6373 use std::path::PathBuf;
6374
6375 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6376 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6377 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6378 cfg.github_token = Some("ghs_secret".to_string());
6379 cfg.include_sub_agent_streaming_events = Some(true);
6380 cfg.enable_session_telemetry = Some(false);
6381 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
6382 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
6383 cfg.enable_on_demand_instruction_discovery = Some(false);
6384
6385 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6386 let wire_json = serde_json::to_value(&wire).unwrap();
6387 assert_eq!(wire_json["sessionId"], "sess-1");
6388 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6389 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6390 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6391 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
6392 assert_eq!(wire_json["enableSessionTelemetry"], false);
6393 assert_eq!(wire_json["reasoningSummary"], "detailed");
6394 assert_eq!(wire_json["remoteSession"], "on");
6395 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6396
6397 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6399 .into_wire()
6400 .expect("default resume has no duplicate handlers");
6401 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6402 assert!(empty_json.get("reasoningSummary").is_none());
6403 assert!(empty_json.get("remoteSession").is_none());
6404 assert!(
6405 empty_json
6406 .get("enableOnDemandInstructionDiscovery")
6407 .is_none()
6408 );
6409 }
6410
6411 #[test]
6412 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
6413 use std::path::PathBuf;
6414
6415 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6416 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6417 cfg.large_output = Some(
6418 LargeToolOutputConfig::new()
6419 .with_enabled(false)
6420 .with_max_size_bytes(2048)
6421 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6422 );
6423
6424 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6425 let wire_json = serde_json::to_value(&wire).unwrap();
6426 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6427 assert_eq!(wire_json["largeOutput"]["enabled"], false);
6428 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6429 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6430
6431 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6432 .into_wire()
6433 .expect("default resume has no duplicate handlers");
6434 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6435 assert!(empty_json.get("pluginDirectories").is_none());
6436 assert!(empty_json.get("largeOutput").is_none());
6437 }
6438
6439 #[test]
6440 fn session_config_builder_composes() {
6441 use indexmap::IndexMap;
6442
6443 let cfg = SessionConfig::default()
6444 .with_session_id(SessionId::from("sess-1"))
6445 .with_model("claude-sonnet-4")
6446 .with_client_name("test-app")
6447 .with_reasoning_effort("medium")
6448 .with_reasoning_summary(ReasoningSummary::Concise)
6449 .with_context_tier("long_context")
6450 .with_streaming(true)
6451 .with_tools([Tool::new("greet")])
6452 .with_available_tools(["bash", "view"])
6453 .with_excluded_tools(["dangerous"])
6454 .with_mcp_servers(IndexMap::new())
6455 .with_mcp_oauth_token_storage("persistent")
6456 .with_enable_config_discovery(true)
6457 .with_enable_on_demand_instruction_discovery(true)
6458 .with_skill_directories([PathBuf::from("/tmp/skills")])
6459 .with_disabled_skills(["broken-skill"])
6460 .with_agent("researcher")
6461 .with_config_directory(PathBuf::from("/tmp/config"))
6462 .with_working_directory(PathBuf::from("/tmp/work"))
6463 .with_additional_directories([PathBuf::from("/tmp/shared")])
6464 .with_github_token("ghp_test")
6465 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6466 .with_enable_session_telemetry(false)
6467 .with_include_sub_agent_streaming_events(false)
6468 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6469
6470 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
6471 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
6472 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6473 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
6474 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
6475 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
6476 assert_eq!(cfg.streaming, Some(true));
6477 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6478 assert_eq!(
6479 cfg.available_tools.as_deref(),
6480 Some(&["bash".to_string(), "view".to_string()][..])
6481 );
6482 assert_eq!(
6483 cfg.excluded_tools.as_deref(),
6484 Some(&["dangerous".to_string()][..])
6485 );
6486 assert!(cfg.mcp_servers.is_some());
6487 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6488 assert_eq!(cfg.enable_config_discovery, Some(true));
6489 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
6490 assert_eq!(
6491 cfg.skill_directories.as_deref(),
6492 Some(&[PathBuf::from("/tmp/skills")][..])
6493 );
6494 assert_eq!(
6495 cfg.disabled_skills.as_deref(),
6496 Some(&["broken-skill".to_string()][..])
6497 );
6498 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6499 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6500 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6501 assert_eq!(
6502 cfg.additional_directories.as_deref(),
6503 Some(&[PathBuf::from("/tmp/shared")][..])
6504 );
6505 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6506 assert_eq!(
6507 cfg.capi,
6508 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6509 );
6510 assert_eq!(cfg.enable_session_telemetry, Some(false));
6511 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
6512 assert_eq!(
6513 cfg.extension_info,
6514 Some(ExtensionInfo::new("github-app", "counter"))
6515 );
6516 }
6517
6518 #[test]
6519 fn resume_session_config_builder_composes() {
6520 use indexmap::IndexMap;
6521
6522 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
6523 .with_client_name("test-app")
6524 .with_reasoning_summary(ReasoningSummary::None)
6525 .with_context_tier("default")
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(false)
6534 .with_skill_directories([PathBuf::from("/tmp/skills")])
6535 .with_disabled_skills(["broken-skill"])
6536 .with_agent("researcher")
6537 .with_config_directory(PathBuf::from("/tmp/config"))
6538 .with_working_directory(PathBuf::from("/tmp/work"))
6539 .with_additional_directories([PathBuf::from("/tmp/shared")])
6540 .with_github_token("ghp_test")
6541 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6542 .with_enable_session_telemetry(false)
6543 .with_include_sub_agent_streaming_events(true)
6544 .with_suppress_resume_event(true)
6545 .with_continue_pending_work(true)
6546 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6547
6548 assert_eq!(cfg.session_id.as_str(), "sess-2");
6549 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6550 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
6551 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
6552 assert_eq!(cfg.streaming, Some(true));
6553 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6554 assert_eq!(
6555 cfg.available_tools.as_deref(),
6556 Some(&["bash".to_string(), "view".to_string()][..])
6557 );
6558 assert_eq!(
6559 cfg.excluded_tools.as_deref(),
6560 Some(&["dangerous".to_string()][..])
6561 );
6562 assert!(cfg.mcp_servers.is_some());
6563 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6564 assert_eq!(cfg.enable_config_discovery, Some(true));
6565 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
6566 assert_eq!(
6567 cfg.skill_directories.as_deref(),
6568 Some(&[PathBuf::from("/tmp/skills")][..])
6569 );
6570 assert_eq!(
6571 cfg.disabled_skills.as_deref(),
6572 Some(&["broken-skill".to_string()][..])
6573 );
6574 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6575 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6576 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6577 assert_eq!(
6578 cfg.additional_directories.as_deref(),
6579 Some(&[PathBuf::from("/tmp/shared")][..])
6580 );
6581 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6582 assert_eq!(
6583 cfg.capi,
6584 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6585 );
6586 assert_eq!(cfg.enable_session_telemetry, Some(false));
6587 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
6588 assert_eq!(cfg.suppress_resume_event, Some(true));
6589 assert_eq!(cfg.continue_pending_work, Some(true));
6590 assert_eq!(
6591 cfg.extension_info,
6592 Some(ExtensionInfo::new("github-app", "counter"))
6593 );
6594 }
6595
6596 #[test]
6600 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
6601 let cfg =
6602 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
6603 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6604 let json = serde_json::to_value(&wire).unwrap();
6605 assert_eq!(json["continuePendingWork"], true);
6606
6607 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6609 .into_wire()
6610 .expect("no duplicate handlers");
6611 let json = serde_json::to_value(&wire).unwrap();
6612 assert!(json.get("continuePendingWork").is_none());
6613 }
6614
6615 #[test]
6616 fn session_configs_serialize_additional_directories() {
6617 let create = SessionConfig::default().with_additional_directories([
6618 PathBuf::from("/tmp/shared"),
6619 PathBuf::from("/tmp/generated"),
6620 ]);
6621 let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
6622 let create_json = serde_json::to_value(&create_wire).unwrap();
6623 assert_eq!(
6624 create_json["additionalDirectories"],
6625 serde_json::json!(["/tmp/shared", "/tmp/generated"])
6626 );
6627
6628 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6629 .with_additional_directories([PathBuf::from("/tmp/resumed")]);
6630 let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
6631 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6632 assert_eq!(
6633 resume_json["additionalDirectories"],
6634 serde_json::json!(["/tmp/resumed"])
6635 );
6636 }
6637
6638 #[test]
6642 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
6643 let cfg =
6644 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
6645 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6646 let json = serde_json::to_value(&wire).unwrap();
6647 assert_eq!(json["disableResume"], true);
6648 assert!(json.get("suppressResumeEvent").is_none());
6649 }
6650
6651 #[test]
6654 fn session_config_serializes_instruction_directories_to_camel_case() {
6655 let cfg =
6656 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
6657 let (wire, _) = cfg
6658 .into_wire(Some(SessionId::from("instr-on")))
6659 .expect("no duplicate handlers");
6660 let json = serde_json::to_value(&wire).unwrap();
6661 assert_eq!(
6662 json["instructionDirectories"],
6663 serde_json::json!(["/tmp/instr"])
6664 );
6665
6666 let (wire, _) = SessionConfig::default()
6668 .into_wire(Some(SessionId::from("instr-off")))
6669 .expect("no duplicate handlers");
6670 let json = serde_json::to_value(&wire).unwrap();
6671 assert!(json.get("instructionDirectories").is_none());
6672 }
6673
6674 #[test]
6677 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
6678 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
6679 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
6680 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6681 let json = serde_json::to_value(&wire).unwrap();
6682 assert_eq!(
6683 json["instructionDirectories"],
6684 serde_json::json!(["/tmp/instr"])
6685 );
6686
6687 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6688 .into_wire()
6689 .expect("no duplicate handlers");
6690 let json = serde_json::to_value(&wire).unwrap();
6691 assert!(json.get("instructionDirectories").is_none());
6692 }
6693
6694 #[test]
6695 fn custom_agent_config_builder_composes() {
6696 use indexmap::IndexMap;
6697
6698 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
6699 .with_display_name("Research Assistant")
6700 .with_description("Investigates technical questions.")
6701 .with_tools(["bash", "view"])
6702 .with_mcp_servers(IndexMap::new())
6703 .with_infer(true)
6704 .with_skills(["rust-coding-skill"]);
6705
6706 assert_eq!(cfg.name, "researcher");
6707 assert_eq!(cfg.prompt, "You are a research assistant.");
6708 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
6709 assert_eq!(
6710 cfg.description.as_deref(),
6711 Some("Investigates technical questions.")
6712 );
6713 assert_eq!(
6714 cfg.tools.as_deref(),
6715 Some(&["bash".to_string(), "view".to_string()][..])
6716 );
6717 assert!(cfg.mcp_servers.is_some());
6718 assert_eq!(cfg.infer, Some(true));
6719 assert_eq!(
6720 cfg.skills.as_deref(),
6721 Some(&["rust-coding-skill".to_string()][..])
6722 );
6723 }
6724
6725 #[test]
6726 fn mcp_servers_serialize_in_insertion_order() {
6727 use indexmap::IndexMap;
6728
6729 let order = [
6735 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
6736 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
6737 ];
6738 let mut servers = IndexMap::new();
6739 for name in order {
6740 servers.insert(
6741 name.to_string(),
6742 McpServerConfig::Stdio(McpStdioServerConfig {
6743 command: "run".to_string(),
6744 ..Default::default()
6745 }),
6746 );
6747 }
6748
6749 let (wire, _runtime) = SessionConfig::default()
6750 .with_mcp_servers(servers)
6751 .into_wire(None)
6752 .expect("into_wire should succeed");
6753 let json = serde_json::to_string(&wire).expect("serialize wire");
6754
6755 let positions: Vec<usize> = order
6756 .iter()
6757 .map(|name| {
6758 json.find(&format!("\"{name}\""))
6759 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
6760 })
6761 .collect();
6762 let mut ascending = positions.clone();
6763 ascending.sort_unstable();
6764 assert_eq!(
6765 positions, ascending,
6766 "mcp server keys must serialize in insertion order: {json}"
6767 );
6768 }
6769
6770 #[test]
6771 fn infinite_session_config_builder_composes() {
6772 let cfg = InfiniteSessionConfig::new()
6773 .with_enabled(true)
6774 .with_background_compaction_threshold(0.75)
6775 .with_buffer_exhaustion_threshold(0.92);
6776
6777 assert_eq!(cfg.enabled, Some(true));
6778 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
6779 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
6780 }
6781
6782 #[test]
6783 fn provider_config_builder_composes() {
6784 use std::collections::HashMap;
6785
6786 let mut headers = HashMap::new();
6787 headers.insert("X-Custom".to_string(), "value".to_string());
6788
6789 let cfg = ProviderConfig::new("https://api.example.com")
6790 .with_provider_type("openai")
6791 .with_wire_api("completions")
6792 .with_transport("websockets")
6793 .with_api_key("sk-test")
6794 .with_bearer_token("bearer-test")
6795 .with_headers(headers)
6796 .with_model_id("gpt-4")
6797 .with_wire_model("azure-gpt-4-deployment")
6798 .with_max_prompt_tokens(8192)
6799 .with_max_output_tokens(2048);
6800
6801 assert_eq!(cfg.base_url, "https://api.example.com");
6802 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
6803 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
6804 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
6805 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
6806 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
6807 assert_eq!(
6808 cfg.headers
6809 .as_ref()
6810 .and_then(|h| h.get("X-Custom"))
6811 .map(String::as_str),
6812 Some("value"),
6813 );
6814 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
6815 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
6816 assert_eq!(cfg.max_prompt_tokens, Some(8192));
6817 assert_eq!(cfg.max_output_tokens, Some(2048));
6818
6819 let wire = serde_json::to_value(&cfg).unwrap();
6821 assert_eq!(wire["modelId"], "gpt-4");
6822 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
6823 assert_eq!(wire["maxPromptTokens"], 8192);
6824 assert_eq!(wire["maxOutputTokens"], 2048);
6825
6826 let unset = ProviderConfig::new("https://api.example.com");
6827 let wire_unset = serde_json::to_value(&unset).unwrap();
6828 assert!(wire_unset.get("modelId").is_none());
6829 assert!(wire_unset.get("wireModel").is_none());
6830 assert!(wire_unset.get("maxPromptTokens").is_none());
6831 assert!(wire_unset.get("maxOutputTokens").is_none());
6832 }
6833
6834 #[test]
6835 fn capi_session_options_builder_composes_and_serializes() {
6836 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
6837
6838 assert_eq!(cfg.enable_web_socket_responses, Some(false));
6839
6840 let wire = serde_json::to_value(&cfg).unwrap();
6841 assert_eq!(
6842 wire,
6843 serde_json::json!({ "enableWebSocketResponses": false })
6844 );
6845
6846 let unset = CapiSessionOptions::new();
6847 let wire_unset = serde_json::to_value(&unset).unwrap();
6848 assert!(wire_unset.get("enableWebSocketResponses").is_none());
6849 }
6850
6851 #[test]
6852 fn session_config_with_capi_serializes() {
6853 let (wire, _) = SessionConfig::default()
6854 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6855 .into_wire(Some(SessionId::from("capi-create")))
6856 .expect("no duplicate handlers");
6857 let json = serde_json::to_value(&wire).unwrap();
6858 assert_eq!(
6859 json["capi"],
6860 serde_json::json!({ "enableWebSocketResponses": false })
6861 );
6862
6863 let (empty_wire, _) = SessionConfig::default()
6864 .into_wire(Some(SessionId::from("capi-create-unset")))
6865 .expect("no duplicate handlers");
6866 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6867 assert!(empty_json.get("capi").is_none());
6868 }
6869
6870 #[test]
6871 fn resume_session_config_with_capi_serializes() {
6872 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
6873 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6874 .into_wire()
6875 .expect("no duplicate handlers");
6876 let json = serde_json::to_value(&wire).unwrap();
6877 assert_eq!(
6878 json["capi"],
6879 serde_json::json!({ "enableWebSocketResponses": false })
6880 );
6881
6882 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
6883 .into_wire()
6884 .expect("no duplicate handlers");
6885 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6886 assert!(empty_json.get("capi").is_none());
6887 }
6888
6889 #[test]
6890 fn system_message_config_builder_composes() {
6891 use std::collections::HashMap;
6892
6893 let cfg = SystemMessageConfig::new()
6894 .with_mode("replace")
6895 .with_content("Custom system message.")
6896 .with_sections(HashMap::new());
6897
6898 assert_eq!(cfg.mode.as_deref(), Some("replace"));
6899 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
6900 assert!(cfg.sections.is_some());
6901 }
6902
6903 #[test]
6904 fn delivery_mode_serializes_to_kebab_case_strings() {
6905 assert_eq!(
6906 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
6907 "\"enqueue\""
6908 );
6909 assert_eq!(
6910 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
6911 "\"immediate\""
6912 );
6913 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
6914 assert_eq!(parsed, DeliveryMode::Immediate);
6915 }
6916
6917 #[test]
6918 fn agent_mode_serializes_to_kebab_case_strings() {
6919 assert_eq!(
6920 serde_json::to_string(&AgentMode::Interactive).unwrap(),
6921 "\"interactive\""
6922 );
6923 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
6924 assert_eq!(
6925 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
6926 "\"autopilot\""
6927 );
6928 assert_eq!(
6929 serde_json::to_string(&AgentMode::Shell).unwrap(),
6930 "\"shell\""
6931 );
6932 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
6933 assert_eq!(parsed, AgentMode::Plan);
6934 }
6935
6936 #[test]
6937 fn connection_state_distinguishes_variants() {
6938 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
6941 }
6942
6943 #[test]
6949 fn session_event_round_trips_agent_id_on_envelope() {
6950 let wire = json!({
6951 "id": "evt-1",
6952 "timestamp": "2026-04-30T12:00:00Z",
6953 "parentId": null,
6954 "agentId": "sub-agent-42",
6955 "type": "assistant.message",
6956 "data": { "message": "hi" }
6957 });
6958
6959 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
6960 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6961
6962 let roundtripped = serde_json::to_value(&event).unwrap();
6964 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6965
6966 let main_agent_event: SessionEvent = serde_json::from_value(json!({
6968 "id": "evt-2",
6969 "timestamp": "2026-04-30T12:00:01Z",
6970 "parentId": null,
6971 "type": "session.idle",
6972 "data": {}
6973 }))
6974 .unwrap();
6975 assert!(main_agent_event.agent_id.is_none());
6976 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
6977 assert!(roundtripped.get("agentId").is_none());
6978 }
6979
6980 #[test]
6982 fn typed_session_event_round_trips_agent_id_on_envelope() {
6983 let wire = json!({
6984 "id": "evt-1",
6985 "timestamp": "2026-04-30T12:00:00Z",
6986 "parentId": null,
6987 "agentId": "sub-agent-42",
6988 "type": "session.idle",
6989 "data": {}
6990 });
6991
6992 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
6993 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6994
6995 let roundtripped = serde_json::to_value(&event).unwrap();
6996 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6997 }
6998
6999 #[test]
7000 fn connection_state_variants_compile() {
7001 let _ = ConnectionState::Disconnected;
7005 let _ = ConnectionState::Connecting;
7006 let _ = ConnectionState::Connected;
7007 let _ = ConnectionState::Error;
7008 }
7009
7010 #[test]
7011 fn deserializes_runtime_attachment_variants() {
7012 let attachments: Vec<Attachment> = serde_json::from_value(json!([
7013 {
7014 "type": "file",
7015 "path": "/tmp/file.rs",
7016 "displayName": "file.rs",
7017 "lineRange": { "start": 7, "end": 12 }
7018 },
7019 {
7020 "type": "directory",
7021 "path": "/tmp/project",
7022 "displayName": "project"
7023 },
7024 {
7025 "type": "selection",
7026 "filePath": "/tmp/lib.rs",
7027 "displayName": "lib.rs",
7028 "text": "fn main() {}",
7029 "selection": {
7030 "start": { "line": 1, "character": 2 },
7031 "end": { "line": 3, "character": 4 }
7032 }
7033 },
7034 {
7035 "type": "blob",
7036 "data": "Zm9v",
7037 "mimeType": "image/png",
7038 "displayName": "image.png"
7039 },
7040 {
7041 "type": "github_reference",
7042 "number": 42,
7043 "title": "Fix rendering",
7044 "referenceType": "issue",
7045 "state": "open",
7046 "url": "https://github.com/example/repo/issues/42"
7047 }
7048 ]))
7049 .expect("attachments should deserialize");
7050
7051 assert_eq!(attachments.len(), 5);
7052 assert!(matches!(
7053 &attachments[0],
7054 Attachment::File {
7055 path,
7056 display_name,
7057 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7058 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7059 ));
7060 assert!(matches!(
7061 &attachments[1],
7062 Attachment::Directory { path, display_name }
7063 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7064 ));
7065 assert!(matches!(
7066 &attachments[2],
7067 Attachment::Selection {
7068 file_path,
7069 display_name,
7070 selection:
7071 AttachmentSelectionRange {
7072 start: AttachmentSelectionPosition { line: 1, character: 2 },
7073 end: AttachmentSelectionPosition { line: 3, character: 4 },
7074 },
7075 ..
7076 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
7077 ));
7078 assert!(matches!(
7079 &attachments[3],
7080 Attachment::Blob {
7081 data,
7082 mime_type,
7083 display_name,
7084 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
7085 ));
7086 assert!(matches!(
7087 &attachments[4],
7088 Attachment::GitHubReference {
7089 number: 42,
7090 title,
7091 reference_type: GitHubReferenceType::Issue,
7092 state,
7093 url,
7094 } if title == "Fix rendering"
7095 && state == "open"
7096 && url == "https://github.com/example/repo/issues/42"
7097 ));
7098 }
7099
7100 #[test]
7101 fn ensures_display_names_for_variants_that_support_them() {
7102 let mut attachments = vec![
7103 Attachment::File {
7104 path: PathBuf::from("/tmp/file.rs"),
7105 display_name: None,
7106 line_range: None,
7107 },
7108 Attachment::Selection {
7109 file_path: PathBuf::from("/tmp/src/lib.rs"),
7110 display_name: None,
7111 text: "fn main() {}".to_string(),
7112 selection: AttachmentSelectionRange {
7113 start: AttachmentSelectionPosition {
7114 line: 0,
7115 character: 0,
7116 },
7117 end: AttachmentSelectionPosition {
7118 line: 0,
7119 character: 10,
7120 },
7121 },
7122 },
7123 Attachment::Blob {
7124 data: "Zm9v".to_string(),
7125 mime_type: "image/png".to_string(),
7126 display_name: None,
7127 },
7128 Attachment::GitHubReference {
7129 number: 7,
7130 title: "Track regressions".to_string(),
7131 reference_type: GitHubReferenceType::Issue,
7132 state: "open".to_string(),
7133 url: "https://example.com/issues/7".to_string(),
7134 },
7135 ];
7136
7137 ensure_attachment_display_names(&mut attachments);
7138
7139 assert_eq!(attachments[0].display_name(), Some("file.rs"));
7140 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
7141 assert_eq!(attachments[2].display_name(), Some("attachment"));
7142 assert_eq!(attachments[3].display_name(), None);
7143 assert_eq!(
7144 attachments[3].label(),
7145 Some("Track regressions".to_string())
7146 );
7147 }
7148
7149 #[test]
7150 fn github_anchored_attachment_variants_round_trip() {
7151 let cases = vec![
7152 (
7153 "github_commit",
7154 json!({
7155 "type": "github_commit",
7156 "message": "Fix the thing",
7157 "oid": "abc123",
7158 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
7159 "url": "https://github.com/octocat/repo/commit/abc123"
7160 }),
7161 ),
7162 (
7163 "github_release",
7164 json!({
7165 "type": "github_release",
7166 "name": "v1.2.3",
7167 "repo": { "name": "repo", "owner": "octocat" },
7168 "tagName": "v1.2.3",
7169 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
7170 }),
7171 ),
7172 (
7173 "github_actions_job",
7174 json!({
7175 "type": "github_actions_job",
7176 "conclusion": "failure",
7177 "jobId": 99,
7178 "jobName": "build",
7179 "repo": { "name": "repo", "owner": "octocat" },
7180 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
7181 "workflowName": "CI"
7182 }),
7183 ),
7184 (
7185 "github_repository",
7186 json!({
7187 "type": "github_repository",
7188 "description": "An example repository",
7189 "ref": "main",
7190 "repo": { "name": "repo", "owner": "octocat" },
7191 "url": "https://github.com/octocat/repo"
7192 }),
7193 ),
7194 (
7195 "github_file_diff",
7196 json!({
7197 "type": "github_file_diff",
7198 "base": {
7199 "path": "src/lib.rs",
7200 "ref": "main",
7201 "repo": { "name": "repo", "owner": "octocat" }
7202 },
7203 "head": {
7204 "path": "src/lib.rs",
7205 "ref": "feature",
7206 "repo": { "name": "repo", "owner": "octocat" }
7207 },
7208 "url": "https://github.com/octocat/repo/compare/main...feature"
7209 }),
7210 ),
7211 (
7212 "github_tree_comparison",
7213 json!({
7214 "type": "github_tree_comparison",
7215 "base": {
7216 "repo": { "name": "repo", "owner": "octocat" },
7217 "revision": "main"
7218 },
7219 "head": {
7220 "repo": { "name": "repo", "owner": "octocat" },
7221 "revision": "feature"
7222 },
7223 "url": "https://github.com/octocat/repo/compare/main...feature"
7224 }),
7225 ),
7226 (
7227 "github_url",
7228 json!({
7229 "type": "github_url",
7230 "url": "https://github.com/octocat/repo/wiki"
7231 }),
7232 ),
7233 (
7234 "github_file",
7235 json!({
7236 "type": "github_file",
7237 "path": "src/main.rs",
7238 "ref": "main",
7239 "repo": { "name": "repo", "owner": "octocat" },
7240 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
7241 }),
7242 ),
7243 (
7244 "github_snippet",
7245 json!({
7246 "type": "github_snippet",
7247 "lineRange": { "start": 10, "end": 20 },
7248 "path": "src/main.rs",
7249 "ref": "main",
7250 "repo": { "name": "repo", "owner": "octocat" },
7251 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
7252 }),
7253 ),
7254 ];
7255
7256 for (expected_type, input) in cases {
7257 let attachment: Attachment = serde_json::from_value(input.clone())
7258 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
7259
7260 let serialized_string = serde_json::to_string(&attachment)
7265 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
7266
7267 assert_eq!(
7269 serialized_string.matches("\"type\":").count(),
7270 1,
7271 "{expected_type} must serialize a single `type` key"
7272 );
7273
7274 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
7275 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
7276 assert_eq!(
7277 serialized.get("type").and_then(|value| value.as_str()),
7278 Some(expected_type),
7279 "{expected_type} must serialize the correct discriminator"
7280 );
7281
7282 assert_eq!(
7284 serialized, input,
7285 "{expected_type} should round-trip without data loss"
7286 );
7287 let reparsed: Attachment = serde_json::from_value(serialized)
7288 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
7289 assert_eq!(
7290 reparsed, attachment,
7291 "{expected_type} should re-deserialize to the same value"
7292 );
7293 }
7294 }
7295}
7296
7297#[cfg(test)]
7298mod permission_builder_tests {
7299 use std::sync::Arc;
7300
7301 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
7302 use crate::permission;
7303 use crate::types::{
7304 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
7305 SessionId,
7306 };
7307
7308 fn data() -> PermissionRequestData {
7309 PermissionRequestData {
7310 extra: serde_json::json!({"tool": "shell"}),
7311 ..Default::default()
7312 }
7313 }
7314
7315 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7318 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7319 }
7320
7321 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7322 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7323 }
7324
7325 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
7326 handler
7327 .handle(SessionId::from("s1"), RequestId::new("1"), data())
7328 .await
7329 }
7330
7331 #[tokio::test]
7332 async fn approve_all_with_handler_present_approves() {
7333 let cfg = SessionConfig::default()
7334 .with_permission_handler(Arc::new(ApproveAllHandler))
7335 .approve_all_permissions();
7336 let h = resolve_create(cfg).expect("policy + handler yields handler");
7337 assert!(matches!(
7338 dispatch(&h).await,
7339 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7340 ));
7341 }
7342
7343 #[tokio::test]
7344 async fn approve_all_standalone_produces_handler() {
7345 let cfg = SessionConfig::default().approve_all_permissions();
7346 let h = resolve_create(cfg).expect("policy alone yields handler");
7347 assert!(matches!(
7348 dispatch(&h).await,
7349 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7350 ));
7351 }
7352
7353 #[tokio::test]
7356 async fn approve_all_is_order_independent() {
7357 let a = SessionConfig::default()
7358 .with_permission_handler(Arc::new(ApproveAllHandler))
7359 .approve_all_permissions();
7360 let b = SessionConfig::default()
7361 .approve_all_permissions()
7362 .with_permission_handler(Arc::new(ApproveAllHandler));
7363 let ha = resolve_create(a).unwrap();
7364 let hb = resolve_create(b).unwrap();
7365 assert!(matches!(
7366 dispatch(&ha).await,
7367 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7368 ));
7369 assert!(matches!(
7370 dispatch(&hb).await,
7371 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7372 ));
7373 }
7374
7375 #[tokio::test]
7376 async fn deny_all_is_order_independent() {
7377 let a = SessionConfig::default()
7378 .with_permission_handler(Arc::new(ApproveAllHandler))
7379 .deny_all_permissions();
7380 let b = SessionConfig::default()
7381 .deny_all_permissions()
7382 .with_permission_handler(Arc::new(ApproveAllHandler));
7383 let ha = resolve_create(a).unwrap();
7384 let hb = resolve_create(b).unwrap();
7385 assert!(matches!(
7386 dispatch(&ha).await,
7387 PermissionResult::Decision(PermissionDecision::Reject(_))
7388 ));
7389 assert!(matches!(
7390 dispatch(&hb).await,
7391 PermissionResult::Decision(PermissionDecision::Reject(_))
7392 ));
7393 }
7394
7395 #[tokio::test]
7396 async fn approve_permissions_if_consults_predicate() {
7397 let cfg = SessionConfig::default().approve_permissions_if(|d| {
7398 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7399 });
7400 let h = resolve_create(cfg).unwrap();
7401 assert!(matches!(
7402 dispatch(&h).await,
7403 PermissionResult::Decision(PermissionDecision::Reject(_))
7404 ));
7405 }
7406
7407 #[tokio::test]
7408 async fn approve_permissions_if_is_order_independent() {
7409 let predicate = |d: &PermissionRequestData| {
7410 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7411 };
7412 let a = SessionConfig::default()
7413 .with_permission_handler(Arc::new(ApproveAllHandler))
7414 .approve_permissions_if(predicate);
7415 let b = SessionConfig::default()
7416 .approve_permissions_if(predicate)
7417 .with_permission_handler(Arc::new(ApproveAllHandler));
7418 let ha = resolve_create(a).unwrap();
7419 let hb = resolve_create(b).unwrap();
7420 assert!(matches!(
7421 dispatch(&ha).await,
7422 PermissionResult::Decision(PermissionDecision::Reject(_))
7423 ));
7424 assert!(matches!(
7425 dispatch(&hb).await,
7426 PermissionResult::Decision(PermissionDecision::Reject(_))
7427 ));
7428 }
7429
7430 #[tokio::test]
7431 async fn resume_session_config_approve_all_works() {
7432 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
7433 .with_permission_handler(Arc::new(ApproveAllHandler))
7434 .approve_all_permissions();
7435 let h = resolve_resume(cfg).unwrap();
7436 assert!(matches!(
7437 dispatch(&h).await,
7438 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7439 ));
7440 }
7441
7442 #[tokio::test]
7443 async fn resume_session_config_approve_all_is_order_independent() {
7444 let a = ResumeSessionConfig::new(SessionId::from("s1"))
7445 .with_permission_handler(Arc::new(ApproveAllHandler))
7446 .approve_all_permissions();
7447 let b = ResumeSessionConfig::new(SessionId::from("s1"))
7448 .approve_all_permissions()
7449 .with_permission_handler(Arc::new(ApproveAllHandler));
7450 let ha = resolve_resume(a).unwrap();
7451 let hb = resolve_resume(b).unwrap();
7452 assert!(matches!(
7453 dispatch(&ha).await,
7454 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7455 ));
7456 assert!(matches!(
7457 dispatch(&hb).await,
7458 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7459 ));
7460 }
7461
7462 #[test]
7463 fn session_config_enable_experimental_mode_serializes_when_set() {
7464 let cfg = SessionConfig::default().with_enable_experimental_mode(false);
7465 assert_eq!(cfg.enable_experimental_mode, Some(false));
7466
7467 let (wire, _runtime) = cfg
7468 .into_wire(Some(SessionId::from("experimental-mode")))
7469 .expect("enable_experimental_mode config has no duplicate handlers");
7470 assert_eq!(wire.is_experimental_mode, Some(false));
7471
7472 let json = serde_json::to_value(&wire).unwrap();
7473 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7474 }
7475
7476 #[test]
7477 fn session_config_enable_experimental_mode_omitted_when_none() {
7478 let cfg = SessionConfig::default();
7479 assert_eq!(cfg.enable_experimental_mode, None);
7480
7481 let (wire, _runtime) = cfg
7482 .into_wire(Some(SessionId::from("no-experimental-mode")))
7483 .expect("default config has no duplicate handlers");
7484 assert_eq!(wire.is_experimental_mode, None);
7485
7486 let json = serde_json::to_value(&wire).unwrap();
7487 assert!(json.get("isExperimentalMode").is_none());
7488 }
7489
7490 #[test]
7491 fn resume_session_config_enable_experimental_mode_serializes_when_set() {
7492 let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
7493 .with_enable_experimental_mode(false);
7494 assert_eq!(cfg.enable_experimental_mode, Some(false));
7495
7496 let (wire, _runtime) = cfg
7497 .into_wire()
7498 .expect("resume enable_experimental_mode config has no duplicate handlers");
7499 assert_eq!(wire.is_experimental_mode, Some(false));
7500
7501 let json = serde_json::to_value(&wire).unwrap();
7502 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7503 }
7504
7505 #[test]
7506 fn resume_session_config_enable_experimental_mode_omitted_when_none() {
7507 let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
7508 assert_eq!(cfg.enable_experimental_mode, None);
7509
7510 let (wire, _runtime) = cfg
7511 .into_wire()
7512 .expect("default resume config has no duplicate handlers");
7513 assert_eq!(wire.is_experimental_mode, None);
7514
7515 let json = serde_json::to_value(&wire).unwrap();
7516 assert!(json.get("isExperimentalMode").is_none());
7517 }
7518}