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};
24pub use crate::generated::api_types::{ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus};
26pub use crate::generated::session_events::AutoTier;
28use crate::generated::session_events::ReasoningSummary;
29pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig};
31use crate::github_token::GitHubTokenProvider;
32use crate::handler::{
33 AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler,
34 PermissionHandler, UserInputHandler,
35};
36use crate::hooks::SessionHooks;
37use crate::provider_token::BearerTokenProvider;
38pub use crate::session_fs::{
39 DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig,
40 SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult,
41 SessionFsSqliteQueryType, SessionFsSqliteTransactionError,
42 SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionStatement,
43};
44pub use crate::trace_context::{TraceContext, TraceContextProvider};
45use crate::transforms::SystemMessageTransform;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50#[allow(dead_code)]
51#[non_exhaustive]
52pub(crate) enum ConnectionState {
53 Disconnected,
55 Connecting,
57 Connected,
59 Error,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
68#[non_exhaustive]
69pub enum SessionLifecycleEventType {
70 #[serde(rename = "session.created")]
72 Created,
73 #[serde(rename = "session.deleted")]
75 Deleted,
76 #[serde(rename = "session.updated")]
78 Updated,
79 #[serde(rename = "session.foreground")]
81 Foreground,
82 #[serde(rename = "session.background")]
84 Background,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct SessionLifecycleEventMetadata {
90 #[serde(rename = "startTime")]
92 pub start_time: String,
93 #[serde(rename = "modifiedTime")]
95 pub modified_time: String,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub summary: Option<String>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct SessionLifecycleEvent {
105 #[serde(rename = "type")]
107 pub event_type: SessionLifecycleEventType,
108 #[serde(rename = "sessionId")]
110 pub session_id: SessionId,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub metadata: Option<SessionLifecycleEventMetadata>,
114}
115
116#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
122#[serde(transparent)]
123pub struct SessionId(String);
124
125impl SessionId {
126 pub fn new(id: impl Into<String>) -> Self {
128 Self(id.into())
129 }
130
131 pub fn as_str(&self) -> &str {
133 &self.0
134 }
135
136 pub fn into_inner(self) -> String {
138 self.0
139 }
140}
141
142impl std::ops::Deref for SessionId {
143 type Target = str;
144
145 fn deref(&self) -> &str {
146 &self.0
147 }
148}
149
150impl std::fmt::Display for SessionId {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 f.write_str(&self.0)
153 }
154}
155
156impl From<String> for SessionId {
157 fn from(s: String) -> Self {
158 Self(s)
159 }
160}
161
162impl From<&str> for SessionId {
163 fn from(s: &str) -> Self {
164 Self(s.to_owned())
165 }
166}
167
168impl AsRef<str> for SessionId {
169 fn as_ref(&self) -> &str {
170 &self.0
171 }
172}
173
174impl std::borrow::Borrow<str> for SessionId {
175 fn borrow(&self) -> &str {
176 &self.0
177 }
178}
179
180impl From<SessionId> for String {
181 fn from(id: SessionId) -> String {
182 id.0
183 }
184}
185
186impl PartialEq<str> for SessionId {
187 fn eq(&self, other: &str) -> bool {
188 self.0 == other
189 }
190}
191
192impl PartialEq<String> for SessionId {
193 fn eq(&self, other: &String) -> bool {
194 &self.0 == other
195 }
196}
197
198impl PartialEq<SessionId> for String {
199 fn eq(&self, other: &SessionId) -> bool {
200 self == &other.0
201 }
202}
203
204impl PartialEq<&str> for SessionId {
205 fn eq(&self, other: &&str) -> bool {
206 self.0 == *other
207 }
208}
209
210impl PartialEq<&SessionId> for SessionId {
211 fn eq(&self, other: &&SessionId) -> bool {
212 self.0 == other.0
213 }
214}
215
216impl PartialEq<SessionId> for &SessionId {
217 fn eq(&self, other: &SessionId) -> bool {
218 self.0 == other.0
219 }
220}
221
222#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
228#[serde(transparent)]
229pub struct RequestId(String);
230
231impl RequestId {
232 pub fn new(id: impl Into<String>) -> Self {
234 Self(id.into())
235 }
236
237 pub fn into_inner(self) -> String {
239 self.0
240 }
241}
242
243impl std::ops::Deref for RequestId {
244 type Target = str;
245
246 fn deref(&self) -> &str {
247 &self.0
248 }
249}
250
251impl std::fmt::Display for RequestId {
252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253 f.write_str(&self.0)
254 }
255}
256
257impl From<String> for RequestId {
258 fn from(s: String) -> Self {
259 Self(s)
260 }
261}
262
263impl From<&str> for RequestId {
264 fn from(s: &str) -> Self {
265 Self(s.to_owned())
266 }
267}
268
269impl AsRef<str> for RequestId {
270 fn as_ref(&self) -> &str {
271 &self.0
272 }
273}
274
275impl std::borrow::Borrow<str> for RequestId {
276 fn borrow(&self) -> &str {
277 &self.0
278 }
279}
280
281impl From<RequestId> for String {
282 fn from(id: RequestId) -> String {
283 id.0
284 }
285}
286
287impl PartialEq<str> for RequestId {
288 fn eq(&self, other: &str) -> bool {
289 self.0 == other
290 }
291}
292
293impl PartialEq<String> for RequestId {
294 fn eq(&self, other: &String) -> bool {
295 &self.0 == other
296 }
297}
298
299impl PartialEq<RequestId> for String {
300 fn eq(&self, other: &RequestId) -> bool {
301 self == &other.0
302 }
303}
304
305impl PartialEq<&str> for RequestId {
306 fn eq(&self, other: &&str) -> bool {
307 self.0 == *other
308 }
309}
310
311#[derive(Clone, Default, Serialize, Deserialize)]
326#[serde(rename_all = "camelCase")]
327#[non_exhaustive]
328pub struct Tool {
329 pub name: String,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub namespaced_name: Option<String>,
335 #[serde(default)]
337 pub description: String,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
340 pub instructions: Option<String>,
341 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
343 pub parameters: IndexMap<String, Value>,
344 #[serde(default, skip_serializing_if = "is_false")]
348 pub overrides_built_in_tool: bool,
349 #[serde(default, skip_serializing_if = "is_false")]
353 pub skip_permission: bool,
354 #[serde(default, skip_serializing_if = "is_false")]
359 pub is_terminal: bool,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub defer: Option<DeferMode>,
367 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
372 pub metadata: IndexMap<String, Value>,
373 #[serde(skip)]
385 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
386}
387
388#[inline]
389fn is_false(b: &bool) -> bool {
390 !*b
391}
392
393#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
396#[serde(rename_all = "lowercase")]
397pub enum DeferMode {
398 Auto,
400 Never,
402}
403
404impl Tool {
405 pub fn new(name: impl Into<String>) -> Self {
425 Self {
426 name: name.into(),
427 ..Default::default()
428 }
429 }
430
431 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
434 self.namespaced_name = Some(namespaced_name.into());
435 self
436 }
437
438 pub fn with_description(mut self, description: impl Into<String>) -> Self {
440 self.description = description.into();
441 self
442 }
443
444 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
446 self.instructions = Some(instructions.into());
447 self
448 }
449
450 pub fn with_parameters(mut self, parameters: Value) -> Self {
464 self.parameters = crate::tool::tool_parameters(parameters);
465 self
466 }
467
468 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
472 self.overrides_built_in_tool = overrides;
473 self
474 }
475
476 pub fn with_skip_permission(mut self, skip: bool) -> Self {
480 self.skip_permission = skip;
481 self
482 }
483
484 #[must_use]
491 pub fn with_is_terminal(mut self, is_terminal: bool) -> Self {
492 self.is_terminal = is_terminal;
493 self
494 }
495
496 pub fn with_defer(mut self, defer: DeferMode) -> Self {
500 self.defer = Some(defer);
501 self
502 }
503
504 pub fn with_metadata(mut self, metadata: IndexMap<String, Value>) -> Self {
507 self.metadata = metadata;
508 self
509 }
510
511 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
515 self.handler = Some(handler);
516 self
517 }
518
519 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
524 self.handler.as_ref()
525 }
526}
527
528impl std::fmt::Debug for Tool {
529 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
530 f.debug_struct("Tool")
531 .field("name", &self.name)
532 .field("namespaced_name", &self.namespaced_name)
533 .field("description", &self.description)
534 .field("instructions", &self.instructions)
535 .field("parameters", &self.parameters)
536 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
537 .field("skip_permission", &self.skip_permission)
538 .field("is_terminal", &self.is_terminal)
539 .field("defer", &self.defer)
540 .field("metadata", &self.metadata)
541 .field(
542 "handler",
543 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
544 )
545 .finish()
546 }
547}
548
549#[non_exhaustive]
552#[derive(Debug, Clone)]
553pub struct CommandContext {
554 pub session_id: SessionId,
556 pub command: String,
558 pub command_name: String,
560 pub args: String,
562}
563
564#[async_trait::async_trait]
570pub trait CommandHandler: Send + Sync {
571 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
573}
574
575#[non_exhaustive]
581#[derive(Clone)]
582pub struct CommandDefinition {
583 pub name: String,
585 pub description: Option<String>,
587 pub handler: Arc<dyn CommandHandler>,
589}
590
591impl CommandDefinition {
592 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
595 Self {
596 name: name.into(),
597 description: None,
598 handler,
599 }
600 }
601
602 pub fn with_description(mut self, description: impl Into<String>) -> Self {
604 self.description = Some(description.into());
605 self
606 }
607}
608
609impl std::fmt::Debug for CommandDefinition {
610 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
611 f.debug_struct("CommandDefinition")
612 .field("name", &self.name)
613 .field("description", &self.description)
614 .field("handler", &"<set>")
615 .finish()
616 }
617}
618
619impl Serialize for CommandDefinition {
620 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
621 use serde::ser::SerializeStruct;
622 let mut state = serializer.serialize_struct("CommandDefinition", 2)?;
623 state.serialize_field("name", &self.name)?;
624 state.serialize_field("description", self.description.as_deref().unwrap_or(""))?;
625 state.end()
626 }
627}
628
629#[derive(Debug, Clone, Default, Serialize, Deserialize)]
636#[serde(rename_all = "camelCase")]
637#[non_exhaustive]
638pub struct CustomAgentConfig {
639 pub name: String,
641 #[serde(default, skip_serializing_if = "Option::is_none")]
643 pub display_name: Option<String>,
644 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub description: Option<String>,
647 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub tools: Option<Vec<String>>,
650 pub prompt: String,
652 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
655 #[serde(default, skip_serializing_if = "Option::is_none")]
657 pub infer: Option<bool>,
658 #[serde(default, skip_serializing_if = "Option::is_none")]
660 pub skills: Option<Vec<String>>,
661 #[serde(default, skip_serializing_if = "Option::is_none")]
666 pub model: Option<String>,
667 #[serde(default, skip_serializing_if = "Option::is_none")]
672 pub reasoning_effort: Option<String>,
673}
674
675impl CustomAgentConfig {
676 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
683 Self {
684 name: name.into(),
685 prompt: prompt.into(),
686 ..Self::default()
687 }
688 }
689
690 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
692 self.display_name = Some(display_name.into());
693 self
694 }
695
696 pub fn with_description(mut self, description: impl Into<String>) -> Self {
698 self.description = Some(description.into());
699 self
700 }
701
702 pub fn with_tools<I, S>(mut self, tools: I) -> Self
705 where
706 I: IntoIterator<Item = S>,
707 S: Into<String>,
708 {
709 self.tools = Some(tools.into_iter().map(Into::into).collect());
710 self
711 }
712
713 pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
715 self.mcp_servers = Some(mcp_servers);
716 self
717 }
718
719 pub fn with_infer(mut self, infer: bool) -> Self {
721 self.infer = Some(infer);
722 self
723 }
724
725 pub fn with_skills<I, S>(mut self, skills: I) -> Self
727 where
728 I: IntoIterator<Item = S>,
729 S: Into<String>,
730 {
731 self.skills = Some(skills.into_iter().map(Into::into).collect());
732 self
733 }
734
735 pub fn with_model(mut self, model: impl Into<String>) -> Self {
737 self.model = Some(model.into());
738 self
739 }
740
741 pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into<String>) -> Self {
743 self.reasoning_effort = Some(reasoning_effort.into());
744 self
745 }
746}
747
748#[derive(Debug, Clone, Default, Serialize, Deserialize)]
755#[serde(rename_all = "camelCase")]
756pub struct DefaultAgentConfig {
757 #[serde(default, skip_serializing_if = "Option::is_none")]
759 pub excluded_tools: Option<Vec<String>>,
760}
761
762#[derive(Debug, Clone, Default, Serialize, Deserialize)]
768#[serde(rename_all = "camelCase")]
769#[non_exhaustive]
770pub struct LargeToolOutputConfig {
771 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub enabled: Option<bool>,
774 #[serde(default, skip_serializing_if = "Option::is_none")]
777 pub max_size_bytes: Option<u64>,
778 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
781 pub output_directory: Option<PathBuf>,
782}
783
784impl LargeToolOutputConfig {
785 pub fn new() -> Self {
788 Self::default()
789 }
790
791 pub fn with_enabled(mut self, enabled: bool) -> Self {
793 self.enabled = Some(enabled);
794 self
795 }
796
797 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
799 self.max_size_bytes = Some(max_size_bytes);
800 self
801 }
802
803 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
805 self.output_directory = Some(output_directory.into());
806 self
807 }
808}
809
810#[derive(Debug, Clone, Default, Serialize, Deserialize)]
816#[serde(rename_all = "camelCase")]
817#[non_exhaustive]
818pub struct ToolSearchConfig {
819 #[serde(default, skip_serializing_if = "Option::is_none")]
821 pub enabled: Option<bool>,
822 #[serde(default, skip_serializing_if = "Option::is_none")]
825 pub defer_threshold: Option<u32>,
826}
827
828impl ToolSearchConfig {
829 pub fn new() -> Self {
832 Self::default()
833 }
834
835 pub fn with_enabled(mut self, enabled: bool) -> Self {
837 self.enabled = Some(enabled);
838 self
839 }
840
841 pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
844 self.defer_threshold = Some(defer_threshold);
845 self
846 }
847}
848
849#[derive(Debug, Clone, Default, Serialize, Deserialize)]
854#[serde(rename_all = "camelCase")]
855#[non_exhaustive]
856pub struct GitHubMcpToolConfig {
857 #[serde(default, skip_serializing_if = "Option::is_none")]
859 pub enable_all_tools: Option<bool>,
860 #[serde(default, skip_serializing_if = "Option::is_none")]
862 pub additional_toolsets: Option<Vec<String>>,
863 #[serde(default, skip_serializing_if = "Option::is_none")]
865 pub additional_tools: Option<Vec<String>>,
866 #[serde(default, skip_serializing_if = "Option::is_none")]
868 pub enable_insiders_mode: Option<bool>,
869 #[serde(default, skip_serializing_if = "Option::is_none")]
873 pub disable_form_deferral: Option<bool>,
874}
875
876impl GitHubMcpToolConfig {
877 pub fn new() -> Self {
879 Self::default()
880 }
881
882 pub fn with_enable_all_tools(mut self, value: bool) -> Self {
884 self.enable_all_tools = Some(value);
885 self
886 }
887
888 pub fn with_additional_toolsets<I, S>(mut self, values: I) -> Self
890 where
891 I: IntoIterator<Item = S>,
892 S: Into<String>,
893 {
894 self.additional_toolsets = Some(values.into_iter().map(Into::into).collect());
895 self
896 }
897
898 pub fn with_additional_tools<I, S>(mut self, values: I) -> Self
900 where
901 I: IntoIterator<Item = S>,
902 S: Into<String>,
903 {
904 self.additional_tools = Some(values.into_iter().map(Into::into).collect());
905 self
906 }
907
908 pub fn with_enable_insiders_mode(mut self, value: bool) -> Self {
910 self.enable_insiders_mode = Some(value);
911 self
912 }
913
914 pub fn with_disable_form_deferral(mut self, value: bool) -> Self {
918 self.disable_form_deferral = Some(value);
919 self
920 }
921}
922
923#[derive(Debug, Clone, Default, Serialize, Deserialize)]
930#[serde(rename_all = "camelCase")]
931#[non_exhaustive]
932pub struct InfiniteSessionConfig {
933 #[serde(default, skip_serializing_if = "Option::is_none")]
935 pub enabled: Option<bool>,
936 #[serde(default, skip_serializing_if = "Option::is_none")]
939 pub background_compaction_threshold: Option<f64>,
940 #[serde(default, skip_serializing_if = "Option::is_none")]
943 pub buffer_exhaustion_threshold: Option<f64>,
944}
945
946impl InfiniteSessionConfig {
947 pub fn new() -> Self {
950 Self::default()
951 }
952
953 pub fn with_enabled(mut self, enabled: bool) -> Self {
956 self.enabled = Some(enabled);
957 self
958 }
959
960 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
963 self.background_compaction_threshold = Some(threshold);
964 self
965 }
966
967 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
970 self.buffer_exhaustion_threshold = Some(threshold);
971 self
972 }
973}
974
975#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
986#[serde(rename_all = "camelCase")]
987#[non_exhaustive]
988pub struct MemoryConfiguration {
989 pub enabled: bool,
991}
992
993impl MemoryConfiguration {
994 pub fn enabled() -> Self {
996 Self { enabled: true }
997 }
998
999 pub fn disabled() -> Self {
1001 Self { enabled: false }
1002 }
1003
1004 pub fn with_enabled(mut self, enabled: bool) -> Self {
1006 self.enabled = enabled;
1007 self
1008 }
1009}
1010
1011#[derive(Debug, Clone, Serialize, Deserialize)]
1013#[serde(rename_all = "camelCase")]
1014#[non_exhaustive]
1015pub struct CloudSessionRepository {
1016 pub owner: String,
1018 pub name: String,
1020 #[serde(skip_serializing_if = "Option::is_none")]
1022 pub branch: Option<String>,
1023}
1024
1025impl CloudSessionRepository {
1026 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
1028 Self {
1029 owner: owner.into(),
1030 name: name.into(),
1031 branch: None,
1032 }
1033 }
1034
1035 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
1037 self.branch = Some(branch.into());
1038 self
1039 }
1040}
1041
1042#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1044#[serde(rename_all = "camelCase")]
1045#[non_exhaustive]
1046pub struct CloudSessionOptions {
1047 #[serde(skip_serializing_if = "Option::is_none")]
1049 pub repository: Option<CloudSessionRepository>,
1050}
1051
1052impl CloudSessionOptions {
1053 pub fn with_repository(repository: CloudSessionRepository) -> Self {
1055 Self {
1056 repository: Some(repository),
1057 }
1058 }
1059}
1060
1061#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1063#[serde(rename_all = "camelCase")]
1064pub struct ExtensionInfo {
1065 pub source: String,
1067 pub name: String,
1069}
1070
1071impl ExtensionInfo {
1072 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
1074 Self {
1075 source: source.into(),
1076 name: name.into(),
1077 }
1078 }
1079}
1080
1081#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1092#[serde(rename_all = "camelCase")]
1093pub struct CanvasProviderIdentity {
1094 pub id: String,
1096 #[serde(skip_serializing_if = "Option::is_none")]
1098 pub name: Option<String>,
1099}
1100
1101impl CanvasProviderIdentity {
1102 pub fn new(id: impl Into<String>) -> Self {
1104 Self {
1105 id: id.into(),
1106 name: None,
1107 }
1108 }
1109
1110 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1112 self.name = Some(name.into());
1113 self
1114 }
1115}
1116
1117#[derive(Debug, Clone, Serialize, Deserialize)]
1151#[serde(tag = "type", rename_all = "lowercase")]
1152#[non_exhaustive]
1153pub enum McpServerConfig {
1154 #[serde(alias = "local")]
1158 Stdio(McpStdioServerConfig),
1159 Http(McpHttpServerConfig),
1161 Sse(McpHttpServerConfig),
1163}
1164
1165#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1169#[serde(rename_all = "camelCase")]
1170pub struct McpStdioServerConfig {
1171 #[serde(default, skip_serializing_if = "Option::is_none")]
1177 pub tools: Option<Vec<String>>,
1178 #[serde(default, skip_serializing_if = "Option::is_none")]
1180 pub timeout: Option<i64>,
1181 pub command: String,
1183 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1185 pub args: Vec<String>,
1186 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1189 pub env: HashMap<String, String>,
1190 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1192 pub working_directory: Option<String>,
1193}
1194
1195#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1199#[serde(rename_all = "camelCase")]
1200pub struct McpHttpServerConfig {
1201 #[serde(default, skip_serializing_if = "Option::is_none")]
1207 pub tools: Option<Vec<String>>,
1208 #[serde(default, skip_serializing_if = "Option::is_none")]
1210 pub timeout: Option<i64>,
1211 pub url: String,
1213 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1215 pub headers: HashMap<String, String>,
1216}
1217
1218#[derive(Clone, Default, Serialize, Deserialize)]
1224#[serde(rename_all = "camelCase")]
1225#[non_exhaustive]
1226pub struct ProviderConfig {
1227 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1230 pub provider_type: Option<String>,
1231 #[serde(default, skip_serializing_if = "Option::is_none")]
1234 pub wire_api: Option<String>,
1235 #[serde(default, skip_serializing_if = "Option::is_none")]
1240 pub transport: Option<String>,
1241 pub base_url: String,
1243 #[serde(default, skip_serializing_if = "Option::is_none")]
1245 pub api_key: Option<String>,
1246 #[serde(default, skip_serializing_if = "Option::is_none")]
1250 pub bearer_token: Option<String>,
1251 #[serde(skip)]
1254 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1255 #[serde(default, skip_serializing_if = "Option::is_none")]
1256 pub(crate) has_bearer_token_provider: Option<bool>,
1257 #[serde(default, skip_serializing_if = "Option::is_none")]
1259 pub azure: Option<AzureProviderOptions>,
1260 #[serde(default, skip_serializing_if = "Option::is_none")]
1262 pub headers: Option<HashMap<String, String>>,
1263 #[serde(default, skip_serializing_if = "Option::is_none")]
1267 pub model_id: Option<String>,
1268 #[serde(default, skip_serializing_if = "Option::is_none")]
1275 pub wire_model: Option<String>,
1276 #[serde(default, skip_serializing_if = "Option::is_none")]
1281 pub max_prompt_tokens: Option<i64>,
1282 #[serde(default, skip_serializing_if = "Option::is_none")]
1285 pub max_output_tokens: Option<i64>,
1286}
1287
1288impl std::fmt::Debug for ProviderConfig {
1289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1290 f.debug_struct("ProviderConfig")
1291 .field("provider_type", &self.provider_type)
1292 .field("wire_api", &self.wire_api)
1293 .field("transport", &self.transport)
1294 .field("base_url", &self.base_url)
1295 .field("api_key", &self.api_key)
1296 .field("bearer_token", &self.bearer_token)
1297 .field(
1298 "bearer_token_provider",
1299 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1300 )
1301 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1302 .field("azure", &self.azure)
1303 .field("headers", &self.headers)
1304 .field("model_id", &self.model_id)
1305 .field("wire_model", &self.wire_model)
1306 .field("max_prompt_tokens", &self.max_prompt_tokens)
1307 .field("max_output_tokens", &self.max_output_tokens)
1308 .finish()
1309 }
1310}
1311
1312impl ProviderConfig {
1313 pub fn new(base_url: impl Into<String>) -> Self {
1316 Self {
1317 base_url: base_url.into(),
1318 ..Self::default()
1319 }
1320 }
1321
1322 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1324 self.provider_type = Some(provider_type.into());
1325 self
1326 }
1327
1328 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1330 self.wire_api = Some(wire_api.into());
1331 self
1332 }
1333
1334 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1337 self.transport = Some(transport.into());
1338 self
1339 }
1340
1341 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1343 self.api_key = Some(api_key.into());
1344 self
1345 }
1346
1347 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1350 self.bearer_token = Some(bearer_token.into());
1351 self
1352 }
1353
1354 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1360 self.bearer_token_provider = Some(provider);
1361 self
1362 }
1363
1364 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1366 self.azure = Some(azure);
1367 self
1368 }
1369
1370 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1372 self.headers = Some(headers);
1373 self
1374 }
1375
1376 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1379 self.model_id = Some(model_id.into());
1380 self
1381 }
1382
1383 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1388 self.wire_model = Some(wire_model.into());
1389 self
1390 }
1391
1392 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1396 self.max_prompt_tokens = Some(max);
1397 self
1398 }
1399
1400 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1403 self.max_output_tokens = Some(max);
1404 self
1405 }
1406}
1407
1408#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1421#[serde(rename_all = "camelCase")]
1422#[non_exhaustive]
1423pub struct CapiSessionOptions {
1424 #[serde(default, skip_serializing_if = "Option::is_none")]
1435 pub auto_tier: Option<AutoTier>,
1436
1437 #[serde(default, skip_serializing_if = "Option::is_none")]
1443 pub enable_web_socket_responses: Option<bool>,
1444}
1445
1446impl CapiSessionOptions {
1447 pub fn new() -> Self {
1449 Self::default()
1450 }
1451
1452 pub fn with_auto_tier(mut self, auto_tier: AutoTier) -> Self {
1454 self.auto_tier = Some(auto_tier);
1455 self
1456 }
1457
1458 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1460 self.enable_web_socket_responses = Some(enable);
1461 self
1462 }
1463}
1464
1465#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1467#[serde(rename_all = "camelCase")]
1468pub struct AzureProviderOptions {
1469 #[serde(default, skip_serializing_if = "Option::is_none")]
1471 pub api_version: Option<String>,
1472}
1473
1474#[derive(Clone, Default, Serialize, Deserialize)]
1485#[serde(rename_all = "camelCase")]
1486#[non_exhaustive]
1487pub struct NamedProviderConfig {
1488 pub name: String,
1491 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1494 pub provider_type: Option<String>,
1495 #[serde(default, skip_serializing_if = "Option::is_none")]
1498 pub wire_api: Option<String>,
1499 pub base_url: String,
1501 #[serde(default, skip_serializing_if = "Option::is_none")]
1503 pub api_key: Option<String>,
1504 #[serde(default, skip_serializing_if = "Option::is_none")]
1507 pub bearer_token: Option<String>,
1508 #[serde(skip)]
1511 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1512 #[serde(default, skip_serializing_if = "Option::is_none")]
1513 pub(crate) has_bearer_token_provider: Option<bool>,
1514 #[serde(default, skip_serializing_if = "Option::is_none")]
1516 pub azure: Option<AzureProviderOptions>,
1517 #[serde(default, skip_serializing_if = "Option::is_none")]
1519 pub headers: Option<HashMap<String, String>>,
1520}
1521
1522impl std::fmt::Debug for NamedProviderConfig {
1523 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1524 f.debug_struct("NamedProviderConfig")
1525 .field("name", &self.name)
1526 .field("provider_type", &self.provider_type)
1527 .field("wire_api", &self.wire_api)
1528 .field("base_url", &self.base_url)
1529 .field("api_key", &self.api_key)
1530 .field("bearer_token", &self.bearer_token)
1531 .field(
1532 "bearer_token_provider",
1533 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1534 )
1535 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1536 .field("azure", &self.azure)
1537 .field("headers", &self.headers)
1538 .finish()
1539 }
1540}
1541
1542impl NamedProviderConfig {
1543 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1546 Self {
1547 name: name.into(),
1548 base_url: base_url.into(),
1549 ..Self::default()
1550 }
1551 }
1552
1553 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1555 self.provider_type = Some(provider_type.into());
1556 self
1557 }
1558
1559 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1561 self.wire_api = Some(wire_api.into());
1562 self
1563 }
1564
1565 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1567 self.api_key = Some(api_key.into());
1568 self
1569 }
1570
1571 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1574 self.bearer_token = Some(bearer_token.into());
1575 self
1576 }
1577
1578 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1584 self.bearer_token_provider = Some(provider);
1585 self
1586 }
1587
1588 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1590 self.azure = Some(azure);
1591 self
1592 }
1593
1594 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1596 self.headers = Some(headers);
1597 self
1598 }
1599}
1600
1601fn prepare_bearer_token_providers(
1602 provider: &mut Option<ProviderConfig>,
1603 providers: &mut Option<Vec<NamedProviderConfig>>,
1604) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1605 let mut bearer_token_providers = HashMap::new();
1606
1607 if let Some(provider) = provider.as_mut()
1608 && let Some(token_provider) = provider.bearer_token_provider.take()
1609 {
1610 provider.has_bearer_token_provider = Some(true);
1611 bearer_token_providers.insert("default".to_string(), token_provider);
1612 }
1613
1614 if let Some(providers) = providers.as_mut() {
1615 for provider in providers {
1616 if let Some(token_provider) = provider.bearer_token_provider.take() {
1617 provider.has_bearer_token_provider = Some(true);
1618 bearer_token_providers.insert(provider.name.clone(), token_provider);
1619 }
1620 }
1621 }
1622
1623 bearer_token_providers
1624}
1625
1626#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1634#[serde(rename_all = "camelCase")]
1635#[non_exhaustive]
1636pub struct ProviderModelConfig {
1637 pub id: String,
1640 pub provider: String,
1642 #[serde(default, skip_serializing_if = "Option::is_none")]
1645 pub wire_model: Option<String>,
1646 #[serde(default, skip_serializing_if = "Option::is_none")]
1649 pub model_id: Option<String>,
1650 #[serde(default, skip_serializing_if = "Option::is_none")]
1652 pub name: Option<String>,
1653 #[serde(default, skip_serializing_if = "Option::is_none")]
1655 pub max_prompt_tokens: Option<i64>,
1656 #[serde(default, skip_serializing_if = "Option::is_none")]
1658 pub max_context_window_tokens: Option<i64>,
1659 #[serde(default, skip_serializing_if = "Option::is_none")]
1661 pub max_output_tokens: Option<i64>,
1662 #[serde(default, skip_serializing_if = "Option::is_none")]
1665 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1666}
1667
1668impl ProviderModelConfig {
1669 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1672 Self {
1673 id: id.into(),
1674 provider: provider.into(),
1675 ..Self::default()
1676 }
1677 }
1678
1679 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1681 self.wire_model = Some(wire_model.into());
1682 self
1683 }
1684
1685 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1688 self.model_id = Some(model_id.into());
1689 self
1690 }
1691
1692 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1694 self.name = Some(name.into());
1695 self
1696 }
1697
1698 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1700 self.max_prompt_tokens = Some(max);
1701 self
1702 }
1703
1704 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1706 self.max_context_window_tokens = Some(max);
1707 self
1708 }
1709
1710 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1712 self.max_output_tokens = Some(max);
1713 self
1714 }
1715
1716 pub fn with_capabilities(
1718 mut self,
1719 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1720 ) -> Self {
1721 self.capabilities = Some(capabilities);
1722 self
1723 }
1724}
1725
1726#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1730#[serde(untagged)]
1731pub enum ExpFlagValue {
1732 Bool(bool),
1734 Integer(i64),
1736 Float(f64),
1738 String(String),
1740 Null,
1742}
1743
1744#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1748#[serde(rename_all = "PascalCase")]
1749pub struct ExpConfigEntry {
1750 pub id: String,
1752 pub parameters: HashMap<String, ExpFlagValue>,
1754}
1755
1756#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1762#[serde(rename_all = "PascalCase")]
1763pub struct CopilotExpAssignmentResponse {
1764 #[serde(default)]
1766 pub features: Vec<String>,
1767 #[serde(default)]
1769 pub flights: HashMap<String, String>,
1770 #[serde(default)]
1772 pub configs: Vec<ExpConfigEntry>,
1773 #[serde(default, skip_serializing_if = "Option::is_none")]
1775 pub parameter_groups: Option<Value>,
1776 #[serde(default, skip_serializing_if = "Option::is_none")]
1778 pub flighting_version: Option<i64>,
1779 #[serde(default, skip_serializing_if = "Option::is_none")]
1781 pub impression_id: Option<String>,
1782 #[serde(default)]
1784 pub assignment_context: String,
1785}
1786
1787pub struct DisableBypassPermissionsModes;
1789
1790impl DisableBypassPermissionsModes {
1791 pub const ALLOW_AUTO_ONLY: &'static str = "allow-auto-only";
1793 pub const DISABLE: &'static str = "disable";
1795}
1796
1797#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1806#[serde(rename_all = "camelCase")]
1807#[non_exhaustive]
1808pub struct ManagedSettingsPermissions {
1809 #[serde(default, skip_serializing_if = "Option::is_none")]
1813 pub disable_bypass_permissions_mode: Option<String>,
1814 #[serde(default, skip_serializing_if = "Option::is_none")]
1816 pub deny: Option<Vec<String>>,
1817 #[serde(default, skip_serializing_if = "Option::is_none")]
1819 pub ask: Option<Vec<String>>,
1820 #[serde(default, skip_serializing_if = "Option::is_none")]
1822 pub allow: Option<Vec<String>>,
1823}
1824
1825impl ManagedSettingsPermissions {
1826 pub fn with_disable_bypass_permissions_mode(mut self, value: impl Into<String>) -> Self {
1828 self.disable_bypass_permissions_mode = Some(value.into());
1829 self
1830 }
1831
1832 pub fn with_deny(mut self, rules: Vec<String>) -> Self {
1834 self.deny = Some(rules);
1835 self
1836 }
1837
1838 pub fn with_ask(mut self, rules: Vec<String>) -> Self {
1840 self.ask = Some(rules);
1841 self
1842 }
1843
1844 pub fn with_allow(mut self, rules: Vec<String>) -> Self {
1846 self.allow = Some(rules);
1847 self
1848 }
1849}
1850
1851#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1861#[serde(rename_all = "camelCase")]
1862#[non_exhaustive]
1863pub struct ManagedSettings {
1864 #[serde(default, skip_serializing_if = "Option::is_none")]
1866 pub permissions: Option<ManagedSettingsPermissions>,
1867}
1868
1869impl ManagedSettings {
1870 pub fn with_permissions(mut self, permissions: ManagedSettingsPermissions) -> Self {
1872 self.permissions = Some(permissions);
1873 self
1874 }
1875}
1876
1877#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1879#[serde(rename_all = "lowercase")]
1880#[non_exhaustive]
1881pub enum AskUserVariant {
1882 #[default]
1884 Legacy,
1885 Elicitation,
1887}
1888
1889#[derive(Clone)]
1941#[non_exhaustive]
1942pub struct SessionConfig {
1943 pub session_id: Option<SessionId>,
1945 pub model: Option<String>,
1947 pub client_name: Option<String>,
1949 pub reasoning_effort: Option<String>,
1951 pub reasoning_summary: Option<ReasoningSummary>,
1955 pub context_tier: Option<String>,
1958 pub streaming: Option<bool>,
1960 pub system_message: Option<SystemMessageConfig>,
1962 pub ask_user_variant: Option<AskUserVariant>,
1967 pub tools: Option<Vec<Tool>>,
1969 pub canvases: Option<Vec<CanvasDeclaration>>,
1971 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1976 pub request_canvas_renderer: Option<bool>,
1978 pub request_extensions: Option<bool>,
1980 pub extension_sdk_path: Option<String>,
1984 pub extension_info: Option<ExtensionInfo>,
1986 pub canvas_provider: Option<CanvasProviderIdentity>,
1989 pub available_tools: Option<Vec<String>>,
1991 pub excluded_tools: Option<Vec<String>>,
1993 pub excluded_builtin_agents: Option<Vec<String>>,
1999 pub included_builtin_skills: Option<Vec<String>>,
2003 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
2005 pub mcp_oauth_token_storage: Option<String>,
2014 pub auth_client_id_metadata_url: Option<String>,
2021 pub enable_config_discovery: Option<bool>,
2024 pub skip_embedding_retrieval: Option<bool>,
2026 pub embedding_cache_storage: Option<String>,
2029 pub organization_custom_instructions: Option<String>,
2031 pub enable_on_demand_instruction_discovery: Option<bool>,
2033 pub enable_file_hooks: Option<bool>,
2035 pub enable_host_git_operations: Option<bool>,
2037 pub enable_session_store: Option<bool>,
2039 pub enable_skills: Option<bool>,
2041 pub enable_mcp_apps: Option<bool>,
2068 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
2073 pub skill_directories: Option<Vec<PathBuf>>,
2075 pub instruction_directories: Option<Vec<PathBuf>>,
2078 pub plugin_directories: Option<Vec<PathBuf>>,
2080 pub large_output: Option<LargeToolOutputConfig>,
2082 pub tool_search: Option<ToolSearchConfig>,
2086 pub disabled_skills: Option<Vec<String>>,
2089 pub disabled_mcp_servers: Option<Vec<String>>,
2093 pub hooks: Option<bool>,
2097 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2099 pub default_agent: Option<DefaultAgentConfig>,
2103 pub agent: Option<String>,
2106 pub infinite_sessions: Option<InfiniteSessionConfig>,
2109 pub provider: Option<ProviderConfig>,
2113 pub capi: Option<CapiSessionOptions>,
2119 pub providers: Option<Vec<NamedProviderConfig>>,
2126 pub models: Option<Vec<ProviderModelConfig>>,
2132 pub enable_session_telemetry: Option<bool>,
2140 pub enable_citations: Option<bool>,
2142 pub enable_file_change_tracking: Option<bool>,
2145 pub session_limits: Option<SessionLimitsConfig>,
2147 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2150 pub memory: Option<MemoryConfiguration>,
2152 pub config_directory: Option<PathBuf>,
2155 pub working_directory: Option<PathBuf>,
2158 pub additional_directories: Option<Vec<PathBuf>>,
2162 pub github_token: Option<String>,
2168 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2174 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2180 pub cloud: Option<CloudSessionOptions>,
2183 pub include_sub_agent_streaming_events: Option<bool>,
2187 pub commands: Option<Vec<CommandDefinition>>,
2191 pub feature_flags: Option<HashMap<String, bool>>,
2197 #[doc(hidden)]
2204 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
2205 pub enable_managed_settings: Option<bool>,
2213 pub managed_settings: Option<ManagedSettings>,
2222 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2227 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2231 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2234 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2237 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2241 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2244 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2247 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2251 pub(crate) permission_policy: Option<crate::permission::Policy>,
2255 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2260 pub skip_custom_instructions: Option<bool>,
2264 pub custom_agents_local_only: Option<bool>,
2268 pub enable_experimental_mode: Option<bool>,
2273 pub coauthor_enabled: Option<bool>,
2277 pub manage_schedule_enabled: Option<bool>,
2281 pub event_buffer_capacity: Option<usize>,
2298}
2299
2300impl std::fmt::Debug for SessionConfig {
2301 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2302 f.debug_struct("SessionConfig")
2303 .field("session_id", &self.session_id)
2304 .field("model", &self.model)
2305 .field("client_name", &self.client_name)
2306 .field("reasoning_effort", &self.reasoning_effort)
2307 .field("reasoning_summary", &self.reasoning_summary)
2308 .field("context_tier", &self.context_tier)
2309 .field("streaming", &self.streaming)
2310 .field("system_message", &self.system_message)
2311 .field("ask_user_variant", &self.ask_user_variant)
2312 .field("tools", &self.tools)
2313 .field("canvases", &self.canvases)
2314 .field(
2315 "canvas_handler",
2316 &self.canvas_handler.as_ref().map(|_| "<set>"),
2317 )
2318 .field("request_canvas_renderer", &self.request_canvas_renderer)
2319 .field("request_extensions", &self.request_extensions)
2320 .field("extension_sdk_path", &self.extension_sdk_path)
2321 .field("extension_info", &self.extension_info)
2322 .field("canvas_provider", &self.canvas_provider)
2323 .field("available_tools", &self.available_tools)
2324 .field("excluded_tools", &self.excluded_tools)
2325 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2326 .field("included_builtin_skills", &self.included_builtin_skills)
2327 .field("mcp_servers", &self.mcp_servers)
2328 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2329 .field(
2330 "auth_client_id_metadata_url",
2331 &self.auth_client_id_metadata_url,
2332 )
2333 .field("embedding_cache_storage", &self.embedding_cache_storage)
2334 .field("enable_config_discovery", &self.enable_config_discovery)
2335 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2336 .field(
2337 "organization_custom_instructions",
2338 &self
2339 .organization_custom_instructions
2340 .as_ref()
2341 .map(|_| "<redacted>"),
2342 )
2343 .field(
2344 "enable_on_demand_instruction_discovery",
2345 &self.enable_on_demand_instruction_discovery,
2346 )
2347 .field("enable_file_hooks", &self.enable_file_hooks)
2348 .field(
2349 "enable_host_git_operations",
2350 &self.enable_host_git_operations,
2351 )
2352 .field("enable_session_store", &self.enable_session_store)
2353 .field("enable_skills", &self.enable_skills)
2354 .field("enable_mcp_apps", &self.enable_mcp_apps)
2355 .field("skill_directories", &self.skill_directories)
2356 .field("instruction_directories", &self.instruction_directories)
2357 .field("plugin_directories", &self.plugin_directories)
2358 .field("large_output", &self.large_output)
2359 .field("tool_search", &self.tool_search)
2360 .field("disabled_skills", &self.disabled_skills)
2361 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
2362 .field("hooks", &self.hooks)
2363 .field("custom_agents", &self.custom_agents)
2364 .field("default_agent", &self.default_agent)
2365 .field("agent", &self.agent)
2366 .field("infinite_sessions", &self.infinite_sessions)
2367 .field("provider", &self.provider)
2368 .field("capi", &self.capi)
2369 .field("enable_session_telemetry", &self.enable_session_telemetry)
2370 .field("enable_citations", &self.enable_citations)
2371 .field(
2372 "enable_file_change_tracking",
2373 &self.enable_file_change_tracking,
2374 )
2375 .field("session_limits", &self.session_limits)
2376 .field("model_capabilities", &self.model_capabilities)
2377 .field("memory", &self.memory)
2378 .field("config_directory", &self.config_directory)
2379 .field("working_directory", &self.working_directory)
2380 .field("additional_directories", &self.additional_directories)
2381 .field(
2382 "github_token",
2383 &self.github_token.as_ref().map(|_| "<redacted>"),
2384 )
2385 .field(
2386 "github_token_provider",
2387 &self.github_token_provider.as_ref().map(|_| "<set>"),
2388 )
2389 .field("remote_session", &self.remote_session)
2390 .field("cloud", &self.cloud)
2391 .field(
2392 "include_sub_agent_streaming_events",
2393 &self.include_sub_agent_streaming_events,
2394 )
2395 .field("commands", &self.commands)
2396 .field("feature_flags", &self.feature_flags)
2397 .field("exp_assignments", &self.exp_assignments)
2398 .field("enable_managed_settings", &self.enable_managed_settings)
2399 .field("enable_experimental_mode", &self.enable_experimental_mode)
2400 .field("managed_settings", &self.managed_settings)
2401 .field(
2402 "session_fs_provider",
2403 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2404 )
2405 .field(
2406 "permission_handler",
2407 &self.permission_handler.as_ref().map(|_| "<set>"),
2408 )
2409 .field(
2410 "elicitation_handler",
2411 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2412 )
2413 .field(
2414 "mcp_auth_handler",
2415 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2416 )
2417 .field(
2418 "user_input_handler",
2419 &self.user_input_handler.as_ref().map(|_| "<set>"),
2420 )
2421 .field(
2422 "exit_plan_mode_handler",
2423 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2424 )
2425 .field(
2426 "auto_mode_switch_handler",
2427 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2428 )
2429 .field(
2430 "hooks_handler",
2431 &self.hooks_handler.as_ref().map(|_| "<set>"),
2432 )
2433 .field(
2434 "system_message_transform",
2435 &self.system_message_transform.as_ref().map(|_| "<set>"),
2436 )
2437 .field("event_buffer_capacity", &self.event_buffer_capacity)
2438 .finish()
2439 }
2440}
2441
2442impl Default for SessionConfig {
2443 fn default() -> Self {
2449 Self {
2450 session_id: None,
2451 model: None,
2452 client_name: None,
2453 reasoning_effort: None,
2454 reasoning_summary: None,
2455 context_tier: None,
2456 streaming: None,
2457 system_message: None,
2458 ask_user_variant: None,
2459 tools: None,
2460 canvases: None,
2461 canvas_handler: None,
2462 request_canvas_renderer: None,
2463 request_extensions: None,
2464 extension_sdk_path: None,
2465 extension_info: None,
2466 canvas_provider: None,
2467 available_tools: None,
2468 excluded_tools: None,
2469 excluded_builtin_agents: None,
2470 included_builtin_skills: None,
2471 mcp_servers: None,
2472 mcp_oauth_token_storage: None,
2473 auth_client_id_metadata_url: None,
2474 enable_config_discovery: None,
2475 skip_embedding_retrieval: None,
2476 organization_custom_instructions: None,
2477 enable_on_demand_instruction_discovery: None,
2478 enable_file_hooks: None,
2479 enable_host_git_operations: None,
2480 enable_session_store: None,
2481 enable_skills: None,
2482 embedding_cache_storage: None,
2483 enable_mcp_apps: None,
2484 github_mcp_tool_config: None,
2485 skill_directories: None,
2486 instruction_directories: None,
2487 plugin_directories: None,
2488 large_output: None,
2489 tool_search: None,
2490 disabled_skills: None,
2491 disabled_mcp_servers: None,
2492 hooks: None,
2493 custom_agents: None,
2494 default_agent: None,
2495 agent: None,
2496 infinite_sessions: None,
2497 provider: None,
2498 capi: None,
2499 providers: None,
2500 models: None,
2501 enable_session_telemetry: None,
2502 enable_citations: None,
2503 enable_file_change_tracking: None,
2504 session_limits: None,
2505 model_capabilities: None,
2506 memory: None,
2507 config_directory: None,
2508 working_directory: None,
2509 additional_directories: None,
2510 github_token: None,
2511 github_token_provider: None,
2512 remote_session: None,
2513 cloud: None,
2514 include_sub_agent_streaming_events: None,
2515 commands: None,
2516 feature_flags: None,
2517 exp_assignments: None,
2518 enable_managed_settings: None,
2519 managed_settings: None,
2520 session_fs_provider: None,
2521 permission_handler: None,
2522 elicitation_handler: None,
2523 mcp_auth_handler: None,
2524 user_input_handler: None,
2525 exit_plan_mode_handler: None,
2526 auto_mode_switch_handler: None,
2527 hooks_handler: None,
2528 permission_policy: None,
2529 system_message_transform: None,
2530 skip_custom_instructions: None,
2531 custom_agents_local_only: None,
2532 enable_experimental_mode: None,
2533 coauthor_enabled: None,
2534 manage_schedule_enabled: None,
2535 event_buffer_capacity: None,
2536 }
2537 }
2538}
2539
2540pub(crate) struct SessionConfigRuntime {
2546 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2547 pub permission_policy: Option<crate::permission::Policy>,
2548 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2549 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2550 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2551 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2552 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2553 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2554 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2555 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2556 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2557 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2558 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2559 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2560 pub commands: Option<Vec<CommandDefinition>>,
2561}
2562
2563impl SessionConfig {
2564 pub(crate) fn into_wire(
2576 mut self,
2577 session_id: Option<SessionId>,
2578 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2579 if self.github_token.is_some() && self.github_token_provider.is_some() {
2580 return Err(crate::Error::with_message(
2581 crate::ErrorKind::InvalidConfig,
2582 "github_token and github_token_provider are mutually exclusive",
2583 ));
2584 }
2585 let permission_active =
2586 self.permission_handler.is_some() || self.permission_policy.is_some();
2587 let request_user_input = self.user_input_handler.is_some();
2588 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2589 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2590 let request_elicitation = self.elicitation_handler.is_some();
2591 let hooks_flag = self.hooks_handler.is_some();
2592
2593 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2594 if let Some(tools) = self.tools.as_mut() {
2595 for tool in tools.iter_mut() {
2596 if let Some(handler) = tool.handler.take()
2597 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2598 {
2599 return Err(crate::Error::with_message(
2600 crate::ErrorKind::InvalidConfig,
2601 format!("duplicate tool handler registered for name {:?}", tool.name),
2602 ));
2603 }
2604 }
2605 }
2606
2607 let wire_commands = self.commands.as_ref().map(|cmds| {
2608 cmds.iter()
2609 .map(|c| crate::wire::CommandWireDefinition {
2610 name: c.name.clone(),
2611 description: c.description.clone().unwrap_or_default(),
2612 })
2613 .collect()
2614 });
2615 let wire_canvases = self.canvases.clone();
2616 let canvas_handler = self.canvas_handler.clone();
2617 let bearer_token_providers =
2618 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2619
2620 let wire = crate::wire::SessionCreateWire {
2621 session_id,
2622 model: self.model,
2623 client_name: self.client_name,
2624 reasoning_effort: self.reasoning_effort,
2625 reasoning_summary: self.reasoning_summary,
2626 context_tier: self.context_tier,
2627 streaming: self.streaming,
2628 system_message: self.system_message,
2629 ask_user_variant: self.ask_user_variant,
2630 tools: self.tools,
2631 canvases: wire_canvases,
2632 request_canvas_renderer: self.request_canvas_renderer,
2633 request_extensions: self.request_extensions,
2634 extension_sdk_path: self.extension_sdk_path,
2635 extension_info: self.extension_info,
2636 canvas_provider: self.canvas_provider,
2637 available_tools: self.available_tools,
2638 excluded_tools: self.excluded_tools,
2639 excluded_builtin_agents: self.excluded_builtin_agents,
2640 tool_filter_precedence: "excluded",
2641 mcp_servers: self.mcp_servers,
2642 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2643 auth_client_id_metadata_url: self.auth_client_id_metadata_url,
2644 embedding_cache_storage: self.embedding_cache_storage,
2645 env_value_mode: "direct",
2646 enable_config_discovery: self.enable_config_discovery,
2647 skip_embedding_retrieval: self.skip_embedding_retrieval,
2648 organization_custom_instructions: self.organization_custom_instructions,
2649 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2650 enable_file_hooks: self.enable_file_hooks,
2651 enable_host_git_operations: self.enable_host_git_operations,
2652 enable_session_store: self.enable_session_store,
2653 enable_skills: self.enable_skills,
2654 request_user_input,
2655 request_permission: permission_active,
2656 request_exit_plan_mode,
2657 request_auto_mode_switch,
2658 request_elicitation,
2659 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2660 github_mcp_tool_config: self.github_mcp_tool_config,
2661 hooks: hooks_flag,
2662 skill_directories: self.skill_directories,
2663 instruction_directories: self.instruction_directories,
2664 plugin_directories: self.plugin_directories,
2665 large_output: self.large_output,
2666 tool_search: self.tool_search,
2667 disabled_skills: self.disabled_skills,
2668 disabled_mcp_servers: self.disabled_mcp_servers,
2669 custom_agents: self.custom_agents,
2670 custom_agents_local_only: self.custom_agents_local_only,
2671 default_agent: self.default_agent,
2672 agent: self.agent,
2673 infinite_sessions: self.infinite_sessions,
2674 provider: self.provider,
2675 capi: self.capi,
2676 providers: self.providers,
2677 models: self.models,
2678 enable_session_telemetry: self.enable_session_telemetry,
2679 enable_citations: self.enable_citations,
2680 enable_file_change_tracking: self.enable_file_change_tracking,
2681 session_limits: self.session_limits,
2682 model_capabilities: self.model_capabilities,
2683 memory: self.memory,
2684 config_dir: self.config_directory,
2685 working_directory: self.working_directory,
2686 additional_directories: self.additional_directories,
2687 github_token: self.github_token,
2688 github_token_provider_registration_id: None,
2689 remote_session: self.remote_session,
2690 cloud: self.cloud,
2691 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2692 enable_github_telemetry_forwarding: None,
2693 commands: wire_commands,
2694 feature_flags: self.feature_flags,
2695 exp_assignments: self.exp_assignments,
2696 enable_managed_settings: self.enable_managed_settings,
2697 is_experimental_mode: self.enable_experimental_mode,
2698 managed_settings: self.managed_settings,
2699 };
2700
2701 let runtime = SessionConfigRuntime {
2702 permission_handler: self.permission_handler,
2703 permission_policy: self.permission_policy,
2704 elicitation_handler: self.elicitation_handler,
2705 mcp_auth_handler: self.mcp_auth_handler,
2706 user_input_handler: self.user_input_handler,
2707 exit_plan_mode_handler: self.exit_plan_mode_handler,
2708 auto_mode_switch_handler: self.auto_mode_switch_handler,
2709 hooks_handler: self.hooks_handler,
2710 system_message_transform: self.system_message_transform,
2711 tool_handlers,
2712 canvas_handler,
2713 session_fs_provider: self.session_fs_provider,
2714 bearer_token_providers,
2715 github_token_provider: self.github_token_provider,
2716 commands: self.commands,
2717 };
2718
2719 Ok((wire, runtime))
2720 }
2721
2722 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2726 self.permission_handler = Some(handler);
2727 self
2728 }
2729
2730 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2733 self.elicitation_handler = Some(handler);
2734 self
2735 }
2736
2737 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2739 self.mcp_auth_handler = Some(handler);
2740 self
2741 }
2742
2743 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2746 self.user_input_handler = Some(handler);
2747 self
2748 }
2749
2750 pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self {
2752 self.ask_user_variant = Some(variant);
2753 self
2754 }
2755
2756 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2758 self.exit_plan_mode_handler = Some(handler);
2759 self
2760 }
2761
2762 pub fn with_auto_mode_switch_handler(
2764 mut self,
2765 handler: Arc<dyn AutoModeSwitchHandler>,
2766 ) -> Self {
2767 self.auto_mode_switch_handler = Some(handler);
2768 self
2769 }
2770
2771 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2776 self.commands = Some(commands);
2777 self
2778 }
2779
2780 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2784 self.session_fs_provider = Some(provider);
2785 self
2786 }
2787
2788 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2791 self.hooks_handler = Some(hooks);
2792 self
2793 }
2794
2795 pub fn with_system_message_transform(
2799 mut self,
2800 transform: Arc<dyn SystemMessageTransform>,
2801 ) -> Self {
2802 self.system_message_transform = Some(transform);
2803 self
2804 }
2805
2806 pub fn approve_all_permissions(mut self) -> Self {
2812 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2813 self
2814 }
2815
2816 pub fn deny_all_permissions(mut self) -> Self {
2819 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2820 self
2821 }
2822
2823 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2828 where
2829 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2830 {
2831 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2832 self
2833 }
2834
2835 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2837 self.session_id = Some(id.into());
2838 self
2839 }
2840
2841 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2843 self.model = Some(model.into());
2844 self
2845 }
2846
2847 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2849 self.client_name = Some(name.into());
2850 self
2851 }
2852
2853 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2855 self.reasoning_effort = Some(effort.into());
2856 self
2857 }
2858
2859 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2861 self.reasoning_summary = Some(summary);
2862 self
2863 }
2864
2865 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2867 self.context_tier = Some(tier.into());
2868 self
2869 }
2870
2871 pub fn with_streaming(mut self, streaming: bool) -> Self {
2873 self.streaming = Some(streaming);
2874 self
2875 }
2876
2877 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2879 self.system_message = Some(system_message);
2880 self
2881 }
2882
2883 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2885 self.tools = Some(tools.into_iter().collect());
2886 self
2887 }
2888
2889 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2894 self.canvases = Some(canvases.into_iter().collect());
2895 self
2896 }
2897
2898 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2900 self.canvas_handler = Some(handler);
2901 self
2902 }
2903
2904 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2906 self.request_canvas_renderer = Some(request);
2907 self
2908 }
2909
2910 pub fn with_request_extensions(mut self, request: bool) -> Self {
2912 self.request_extensions = Some(request);
2913 self
2914 }
2915
2916 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2920 self.extension_sdk_path = Some(path.into());
2921 self
2922 }
2923
2924 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2926 self.extension_info = Some(extension_info);
2927 self
2928 }
2929
2930 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2933 self.canvas_provider = Some(canvas_provider);
2934 self
2935 }
2936
2937 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2939 where
2940 I: IntoIterator<Item = S>,
2941 S: Into<String>,
2942 {
2943 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2944 self
2945 }
2946
2947 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2949 where
2950 I: IntoIterator<Item = S>,
2951 S: Into<String>,
2952 {
2953 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2954 self
2955 }
2956
2957 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2959 where
2960 I: IntoIterator<Item = S>,
2961 S: Into<String>,
2962 {
2963 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2964 self
2965 }
2966
2967 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2969 self.mcp_servers = Some(servers);
2970 self
2971 }
2972
2973 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2981 self.mcp_oauth_token_storage = Some(mode.into());
2982 self
2983 }
2984
2985 pub fn with_auth_client_id_metadata_url(mut self, url: impl Into<String>) -> Self {
2987 self.auth_client_id_metadata_url = Some(url.into());
2988 self
2989 }
2990
2991 pub fn with_embedding_cache_storage(
2993 mut self,
2994 embedding_cache_storage: impl Into<String>,
2995 ) -> Self {
2996 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2997 self
2998 }
2999
3000 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3003 self.enable_config_discovery = Some(enable);
3004 self
3005 }
3006
3007 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3009 self.skip_embedding_retrieval = Some(value);
3010 self
3011 }
3012
3013 pub fn with_organization_custom_instructions(
3015 mut self,
3016 instructions: impl Into<String>,
3017 ) -> Self {
3018 self.organization_custom_instructions = Some(instructions.into());
3019 self
3020 }
3021
3022 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3024 self.enable_on_demand_instruction_discovery = Some(value);
3025 self
3026 }
3027
3028 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3030 self.enable_file_hooks = Some(value);
3031 self
3032 }
3033
3034 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3036 self.enable_host_git_operations = Some(value);
3037 self
3038 }
3039
3040 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3042 self.enable_session_store = Some(value);
3043 self
3044 }
3045
3046 pub fn with_enable_skills(mut self, value: bool) -> Self {
3048 self.enable_skills = Some(value);
3049 self
3050 }
3051
3052 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3058 self.enable_mcp_apps = Some(enable);
3059 self
3060 }
3061
3062 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
3064 self.github_mcp_tool_config = Some(config);
3065 self
3066 }
3067
3068 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3070 where
3071 I: IntoIterator<Item = P>,
3072 P: Into<PathBuf>,
3073 {
3074 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3075 self
3076 }
3077
3078 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
3080 where
3081 I: IntoIterator<Item = S>,
3082 S: Into<String>,
3083 {
3084 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
3085 self
3086 }
3087
3088 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3092 where
3093 I: IntoIterator<Item = P>,
3094 P: Into<PathBuf>,
3095 {
3096 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3097 self
3098 }
3099
3100 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3102 where
3103 I: IntoIterator<Item = P>,
3104 P: Into<PathBuf>,
3105 {
3106 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3107 self
3108 }
3109
3110 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3112 self.large_output = Some(config);
3113 self
3114 }
3115
3116 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
3119 self.tool_search = Some(config);
3120 self
3121 }
3122
3123 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3125 where
3126 I: IntoIterator<Item = S>,
3127 S: Into<String>,
3128 {
3129 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3130 self
3131 }
3132
3133 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
3135 where
3136 I: IntoIterator<Item = S>,
3137 S: Into<String>,
3138 {
3139 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
3140 self
3141 }
3142
3143 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3145 mut self,
3146 agents: I,
3147 ) -> Self {
3148 self.custom_agents = Some(agents.into_iter().collect());
3149 self
3150 }
3151
3152 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3154 self.default_agent = Some(agent);
3155 self
3156 }
3157
3158 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3161 self.agent = Some(name.into());
3162 self
3163 }
3164
3165 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3168 self.infinite_sessions = Some(config);
3169 self
3170 }
3171
3172 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3174 self.provider = Some(provider);
3175 self
3176 }
3177
3178 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3180 self.capi = Some(capi);
3181 self
3182 }
3183
3184 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3190 self.providers = Some(providers);
3191 self
3192 }
3193
3194 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3200 self.models = Some(models);
3201 self
3202 }
3203
3204 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3208 self.enable_session_telemetry = Some(enable);
3209 self
3210 }
3211
3212 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3214 self.enable_citations = Some(enable);
3215 self
3216 }
3217
3218 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
3221 self.enable_file_change_tracking = Some(enable);
3222 self
3223 }
3224
3225 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3227 self.session_limits = Some(limits);
3228 self
3229 }
3230
3231 pub fn with_model_capabilities(
3233 mut self,
3234 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3235 ) -> Self {
3236 self.model_capabilities = Some(capabilities);
3237 self
3238 }
3239
3240 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3242 self.memory = Some(memory);
3243 self
3244 }
3245
3246 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3248 self.config_directory = Some(dir.into());
3249 self
3250 }
3251
3252 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3255 self.working_directory = Some(dir.into());
3256 self
3257 }
3258
3259 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
3261 where
3262 I: IntoIterator<Item = P>,
3263 P: Into<PathBuf>,
3264 {
3265 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
3266 self
3267 }
3268
3269 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3274 self.github_token = Some(token.into());
3275 self
3276 }
3277
3278 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
3284 self.github_token_provider = Some(provider);
3285 self
3286 }
3287
3288 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3291 self.include_sub_agent_streaming_events = Some(include);
3292 self
3293 }
3294
3295 pub fn with_remote_session(
3297 mut self,
3298 mode: crate::generated::api_types::RemoteSessionMode,
3299 ) -> Self {
3300 self.remote_session = Some(mode);
3301 self
3302 }
3303
3304 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
3306 self.cloud = Some(cloud);
3307 self
3308 }
3309
3310 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3312 self.skip_custom_instructions = Some(value);
3313 self
3314 }
3315
3316 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3318 self.custom_agents_local_only = Some(value);
3319 self
3320 }
3321
3322 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
3324 self.enable_experimental_mode = Some(enable_experimental_mode);
3325 self
3326 }
3327
3328 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3330 self.coauthor_enabled = Some(value);
3331 self
3332 }
3333
3334 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3336 self.manage_schedule_enabled = Some(value);
3337 self
3338 }
3339
3340 pub fn with_feature_flags(mut self, feature_flags: HashMap<String, bool>) -> Self {
3342 self.feature_flags = Some(feature_flags);
3343 self
3344 }
3345
3346 pub fn with_event_buffer_capacity(mut self, capacity: usize) -> Self {
3354 self.event_buffer_capacity = Some(capacity);
3355 self
3356 }
3357
3358 #[doc(hidden)]
3366 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
3367 self.exp_assignments = Some(assignments);
3368 self
3369 }
3370
3371 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3378 self.enable_managed_settings = Some(enabled);
3379 self
3380 }
3381
3382 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
3387 self.managed_settings = Some(managed_settings);
3388 self
3389 }
3390}
3391#[derive(Clone)]
3398#[non_exhaustive]
3399pub struct ResumeSessionConfig {
3400 pub session_id: SessionId,
3402 pub model: Option<String>,
3405 pub client_name: Option<String>,
3407 pub reasoning_effort: Option<String>,
3409 pub reasoning_summary: Option<ReasoningSummary>,
3413 pub context_tier: Option<String>,
3416 pub streaming: Option<bool>,
3418 pub system_message: Option<SystemMessageConfig>,
3421 pub ask_user_variant: Option<AskUserVariant>,
3426 pub tools: Option<Vec<Tool>>,
3428 pub canvases: Option<Vec<CanvasDeclaration>>,
3430 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
3433 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3435 pub request_canvas_renderer: Option<bool>,
3437 pub request_extensions: Option<bool>,
3439 pub extension_sdk_path: Option<String>,
3443 pub extension_info: Option<ExtensionInfo>,
3445 pub canvas_provider: Option<CanvasProviderIdentity>,
3448 pub available_tools: Option<Vec<String>>,
3450 pub excluded_tools: Option<Vec<String>>,
3452 pub excluded_builtin_agents: Option<Vec<String>>,
3458 pub included_builtin_skills: Option<Vec<String>>,
3462 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3464 pub mcp_oauth_token_storage: Option<String>,
3467 pub auth_client_id_metadata_url: Option<String>,
3473 pub enable_config_discovery: Option<bool>,
3476 pub skip_embedding_retrieval: Option<bool>,
3478 pub embedding_cache_storage: Option<String>,
3480 pub organization_custom_instructions: Option<String>,
3482 pub enable_on_demand_instruction_discovery: Option<bool>,
3484 pub enable_file_hooks: Option<bool>,
3486 pub enable_host_git_operations: Option<bool>,
3488 pub enable_session_store: Option<bool>,
3490 pub enable_skills: Option<bool>,
3492 pub enable_mcp_apps: Option<bool>,
3498 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
3503 pub skill_directories: Option<Vec<PathBuf>>,
3505 pub instruction_directories: Option<Vec<PathBuf>>,
3508 pub plugin_directories: Option<Vec<PathBuf>>,
3510 pub large_output: Option<LargeToolOutputConfig>,
3512 pub tool_search: Option<ToolSearchConfig>,
3515 pub disabled_skills: Option<Vec<String>>,
3517 pub disabled_mcp_servers: Option<Vec<String>>,
3520 pub hooks: Option<bool>,
3522 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3524 pub default_agent: Option<DefaultAgentConfig>,
3526 pub agent: Option<String>,
3528 pub infinite_sessions: Option<InfiniteSessionConfig>,
3530 pub provider: Option<ProviderConfig>,
3532 pub capi: Option<CapiSessionOptions>,
3538 pub providers: Option<Vec<NamedProviderConfig>>,
3544 pub models: Option<Vec<ProviderModelConfig>>,
3550 pub enable_session_telemetry: Option<bool>,
3558 pub enable_citations: Option<bool>,
3560 pub enable_file_change_tracking: Option<bool>,
3564 pub session_limits: Option<SessionLimitsConfig>,
3566 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3568 pub memory: Option<MemoryConfiguration>,
3570 pub config_directory: Option<PathBuf>,
3572 pub working_directory: Option<PathBuf>,
3574 pub additional_directories: Option<Vec<PathBuf>>,
3577 pub github_token: Option<String>,
3580 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
3583 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3586 pub include_sub_agent_streaming_events: Option<bool>,
3588 pub commands: Option<Vec<CommandDefinition>>,
3592 pub feature_flags: Option<HashMap<String, bool>>,
3596 #[doc(hidden)]
3601 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3602 pub enable_managed_settings: Option<bool>,
3608 pub managed_settings: Option<ManagedSettings>,
3614 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3619 pub suppress_resume_event: Option<bool>,
3622 pub continue_pending_work: Option<bool>,
3630 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3633 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3636 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3638 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3641 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3644 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3647 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3649 pub(crate) permission_policy: Option<crate::permission::Policy>,
3651 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3653 pub skip_custom_instructions: Option<bool>,
3655 pub custom_agents_local_only: Option<bool>,
3657 pub enable_experimental_mode: Option<bool>,
3662 pub coauthor_enabled: Option<bool>,
3664 pub manage_schedule_enabled: Option<bool>,
3666 pub event_buffer_capacity: Option<usize>,
3668}
3669
3670impl std::fmt::Debug for ResumeSessionConfig {
3671 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3672 f.debug_struct("ResumeSessionConfig")
3673 .field("session_id", &self.session_id)
3674 .field("model", &self.model)
3675 .field("client_name", &self.client_name)
3676 .field("reasoning_effort", &self.reasoning_effort)
3677 .field("reasoning_summary", &self.reasoning_summary)
3678 .field("context_tier", &self.context_tier)
3679 .field("streaming", &self.streaming)
3680 .field("system_message", &self.system_message)
3681 .field("ask_user_variant", &self.ask_user_variant)
3682 .field("tools", &self.tools)
3683 .field("canvases", &self.canvases)
3684 .field(
3685 "canvas_handler",
3686 &self.canvas_handler.as_ref().map(|_| "<set>"),
3687 )
3688 .field("open_canvases", &self.open_canvases)
3689 .field("request_canvas_renderer", &self.request_canvas_renderer)
3690 .field("request_extensions", &self.request_extensions)
3691 .field("extension_sdk_path", &self.extension_sdk_path)
3692 .field("extension_info", &self.extension_info)
3693 .field("canvas_provider", &self.canvas_provider)
3694 .field("available_tools", &self.available_tools)
3695 .field("excluded_tools", &self.excluded_tools)
3696 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3697 .field("included_builtin_skills", &self.included_builtin_skills)
3698 .field("mcp_servers", &self.mcp_servers)
3699 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3700 .field(
3701 "auth_client_id_metadata_url",
3702 &self.auth_client_id_metadata_url,
3703 )
3704 .field("embedding_cache_storage", &self.embedding_cache_storage)
3705 .field("enable_config_discovery", &self.enable_config_discovery)
3706 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3707 .field(
3708 "organization_custom_instructions",
3709 &self
3710 .organization_custom_instructions
3711 .as_ref()
3712 .map(|_| "<redacted>"),
3713 )
3714 .field(
3715 "enable_on_demand_instruction_discovery",
3716 &self.enable_on_demand_instruction_discovery,
3717 )
3718 .field("enable_file_hooks", &self.enable_file_hooks)
3719 .field(
3720 "enable_host_git_operations",
3721 &self.enable_host_git_operations,
3722 )
3723 .field("enable_session_store", &self.enable_session_store)
3724 .field("enable_skills", &self.enable_skills)
3725 .field("enable_mcp_apps", &self.enable_mcp_apps)
3726 .field("skill_directories", &self.skill_directories)
3727 .field("instruction_directories", &self.instruction_directories)
3728 .field("plugin_directories", &self.plugin_directories)
3729 .field("large_output", &self.large_output)
3730 .field("tool_search", &self.tool_search)
3731 .field("disabled_skills", &self.disabled_skills)
3732 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
3733 .field("hooks", &self.hooks)
3734 .field("custom_agents", &self.custom_agents)
3735 .field("default_agent", &self.default_agent)
3736 .field("agent", &self.agent)
3737 .field("infinite_sessions", &self.infinite_sessions)
3738 .field("provider", &self.provider)
3739 .field("capi", &self.capi)
3740 .field("enable_session_telemetry", &self.enable_session_telemetry)
3741 .field("enable_citations", &self.enable_citations)
3742 .field(
3743 "enable_file_change_tracking",
3744 &self.enable_file_change_tracking,
3745 )
3746 .field("session_limits", &self.session_limits)
3747 .field("model_capabilities", &self.model_capabilities)
3748 .field("memory", &self.memory)
3749 .field("config_directory", &self.config_directory)
3750 .field("working_directory", &self.working_directory)
3751 .field("additional_directories", &self.additional_directories)
3752 .field(
3753 "github_token",
3754 &self.github_token.as_ref().map(|_| "<redacted>"),
3755 )
3756 .field(
3757 "github_token_provider",
3758 &self.github_token_provider.as_ref().map(|_| "<set>"),
3759 )
3760 .field("remote_session", &self.remote_session)
3761 .field(
3762 "include_sub_agent_streaming_events",
3763 &self.include_sub_agent_streaming_events,
3764 )
3765 .field("commands", &self.commands)
3766 .field("feature_flags", &self.feature_flags)
3767 .field("exp_assignments", &self.exp_assignments)
3768 .field("enable_managed_settings", &self.enable_managed_settings)
3769 .field("enable_experimental_mode", &self.enable_experimental_mode)
3770 .field("managed_settings", &self.managed_settings)
3771 .field(
3772 "session_fs_provider",
3773 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3774 )
3775 .field(
3776 "permission_handler",
3777 &self.permission_handler.as_ref().map(|_| "<set>"),
3778 )
3779 .field(
3780 "elicitation_handler",
3781 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3782 )
3783 .field(
3784 "user_input_handler",
3785 &self.user_input_handler.as_ref().map(|_| "<set>"),
3786 )
3787 .field(
3788 "exit_plan_mode_handler",
3789 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3790 )
3791 .field(
3792 "auto_mode_switch_handler",
3793 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3794 )
3795 .field(
3796 "hooks_handler",
3797 &self.hooks_handler.as_ref().map(|_| "<set>"),
3798 )
3799 .field(
3800 "system_message_transform",
3801 &self.system_message_transform.as_ref().map(|_| "<set>"),
3802 )
3803 .field("suppress_resume_event", &self.suppress_resume_event)
3804 .field("continue_pending_work", &self.continue_pending_work)
3805 .field("event_buffer_capacity", &self.event_buffer_capacity)
3806 .finish()
3807 }
3808}
3809
3810impl ResumeSessionConfig {
3811 pub(crate) fn into_wire(
3819 mut self,
3820 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3821 if self.github_token.is_some() && self.github_token_provider.is_some() {
3822 return Err(crate::Error::with_message(
3823 crate::ErrorKind::InvalidConfig,
3824 "github_token and github_token_provider are mutually exclusive",
3825 ));
3826 }
3827 let permission_active =
3828 self.permission_handler.is_some() || self.permission_policy.is_some();
3829 let request_user_input = self.user_input_handler.is_some();
3830 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3831 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3832 let request_elicitation = self.elicitation_handler.is_some();
3833 let hooks_flag = self.hooks_handler.is_some();
3834
3835 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3836 if let Some(tools) = self.tools.as_mut() {
3837 for tool in tools.iter_mut() {
3838 if let Some(handler) = tool.handler.take()
3839 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3840 {
3841 return Err(crate::Error::with_message(
3842 crate::ErrorKind::InvalidConfig,
3843 format!("duplicate tool handler registered for name {:?}", tool.name),
3844 ));
3845 }
3846 }
3847 }
3848
3849 let wire_commands = self.commands.as_ref().map(|cmds| {
3850 cmds.iter()
3851 .map(|c| crate::wire::CommandWireDefinition {
3852 name: c.name.clone(),
3853 description: c.description.clone().unwrap_or_default(),
3854 })
3855 .collect()
3856 });
3857 let wire_canvases = self.canvases.clone();
3858 let canvas_handler = self.canvas_handler.clone();
3859 let bearer_token_providers =
3860 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3861
3862 let wire = crate::wire::SessionResumeWire {
3863 session_id: self.session_id,
3864 model: self.model,
3865 client_name: self.client_name,
3866 reasoning_effort: self.reasoning_effort,
3867 reasoning_summary: self.reasoning_summary,
3868 context_tier: self.context_tier,
3869 streaming: self.streaming,
3870 system_message: self.system_message,
3871 ask_user_variant: self.ask_user_variant,
3872 tools: self.tools,
3873 canvases: wire_canvases,
3874 open_canvases: self.open_canvases,
3875 request_canvas_renderer: self.request_canvas_renderer,
3876 request_extensions: self.request_extensions,
3877 extension_sdk_path: self.extension_sdk_path,
3878 extension_info: self.extension_info,
3879 canvas_provider: self.canvas_provider,
3880 available_tools: self.available_tools,
3881 excluded_tools: self.excluded_tools,
3882 excluded_builtin_agents: self.excluded_builtin_agents,
3883 tool_filter_precedence: "excluded",
3884 mcp_servers: self.mcp_servers,
3885 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3886 auth_client_id_metadata_url: self.auth_client_id_metadata_url,
3887 embedding_cache_storage: self.embedding_cache_storage,
3888 env_value_mode: "direct",
3889 enable_config_discovery: self.enable_config_discovery,
3890 skip_embedding_retrieval: self.skip_embedding_retrieval,
3891 organization_custom_instructions: self.organization_custom_instructions,
3892 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3893 enable_file_hooks: self.enable_file_hooks,
3894 enable_host_git_operations: self.enable_host_git_operations,
3895 enable_session_store: self.enable_session_store,
3896 enable_skills: self.enable_skills,
3897 request_user_input,
3898 request_permission: permission_active,
3899 request_exit_plan_mode,
3900 request_auto_mode_switch,
3901 request_elicitation,
3902 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3903 github_mcp_tool_config: self.github_mcp_tool_config,
3904 hooks: hooks_flag,
3905 skill_directories: self.skill_directories,
3906 instruction_directories: self.instruction_directories,
3907 plugin_directories: self.plugin_directories,
3908 large_output: self.large_output,
3909 tool_search: self.tool_search,
3910 disabled_skills: self.disabled_skills,
3911 disabled_mcp_servers: self.disabled_mcp_servers,
3912 custom_agents: self.custom_agents,
3913 custom_agents_local_only: self.custom_agents_local_only,
3914 default_agent: self.default_agent,
3915 agent: self.agent,
3916 infinite_sessions: self.infinite_sessions,
3917 provider: self.provider,
3918 capi: self.capi,
3919 providers: self.providers,
3920 models: self.models,
3921 enable_session_telemetry: self.enable_session_telemetry,
3922 enable_citations: self.enable_citations,
3923 enable_file_change_tracking: self.enable_file_change_tracking,
3924 session_limits: self.session_limits,
3925 model_capabilities: self.model_capabilities,
3926 memory: self.memory,
3927 config_dir: self.config_directory,
3928 working_directory: self.working_directory,
3929 additional_directories: self.additional_directories,
3930 github_token: self.github_token,
3931 github_token_provider_registration_id: None,
3932 remote_session: self.remote_session,
3933 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3934 enable_github_telemetry_forwarding: None,
3935 commands: wire_commands,
3936 feature_flags: self.feature_flags,
3937 exp_assignments: self.exp_assignments,
3938 enable_managed_settings: self.enable_managed_settings,
3939 is_experimental_mode: self.enable_experimental_mode,
3940 managed_settings: self.managed_settings,
3941 suppress_resume_event: self.suppress_resume_event,
3942 continue_pending_work: self.continue_pending_work,
3943 };
3944
3945 let runtime = SessionConfigRuntime {
3946 permission_handler: self.permission_handler,
3947 permission_policy: self.permission_policy,
3948 elicitation_handler: self.elicitation_handler,
3949 mcp_auth_handler: self.mcp_auth_handler,
3950 user_input_handler: self.user_input_handler,
3951 exit_plan_mode_handler: self.exit_plan_mode_handler,
3952 auto_mode_switch_handler: self.auto_mode_switch_handler,
3953 hooks_handler: self.hooks_handler,
3954 system_message_transform: self.system_message_transform,
3955 tool_handlers,
3956 canvas_handler,
3957 session_fs_provider: self.session_fs_provider,
3958 bearer_token_providers,
3959 github_token_provider: self.github_token_provider,
3960 commands: self.commands,
3961 };
3962
3963 Ok((wire, runtime))
3964 }
3965
3966 pub fn new(session_id: SessionId) -> Self {
3971 Self {
3972 session_id,
3973 model: None,
3974 client_name: None,
3975 reasoning_effort: None,
3976 reasoning_summary: None,
3977 context_tier: None,
3978 streaming: None,
3979 system_message: None,
3980 ask_user_variant: None,
3981 tools: None,
3982 canvases: None,
3983 canvas_handler: None,
3984 open_canvases: None,
3985 request_canvas_renderer: None,
3986 request_extensions: None,
3987 extension_sdk_path: None,
3988 extension_info: None,
3989 canvas_provider: None,
3990 available_tools: None,
3991 excluded_tools: None,
3992 excluded_builtin_agents: None,
3993 included_builtin_skills: None,
3994 mcp_servers: None,
3995 mcp_oauth_token_storage: None,
3996 auth_client_id_metadata_url: None,
3997 enable_config_discovery: None,
3998 skip_embedding_retrieval: None,
3999 organization_custom_instructions: None,
4000 enable_on_demand_instruction_discovery: None,
4001 enable_file_hooks: None,
4002 enable_host_git_operations: None,
4003 enable_session_store: None,
4004 enable_skills: None,
4005 embedding_cache_storage: None,
4006 enable_mcp_apps: None,
4007 github_mcp_tool_config: None,
4008 skill_directories: None,
4009 instruction_directories: None,
4010 plugin_directories: None,
4011 large_output: None,
4012 tool_search: None,
4013 disabled_skills: None,
4014 disabled_mcp_servers: None,
4015 hooks: None,
4016 custom_agents: None,
4017 default_agent: None,
4018 agent: None,
4019 infinite_sessions: None,
4020 provider: None,
4021 capi: None,
4022 providers: None,
4023 models: None,
4024 enable_session_telemetry: None,
4025 enable_citations: None,
4026 enable_file_change_tracking: None,
4027 session_limits: None,
4028 model_capabilities: None,
4029 memory: None,
4030 config_directory: None,
4031 working_directory: None,
4032 additional_directories: None,
4033 github_token: None,
4034 github_token_provider: None,
4035 remote_session: None,
4036 include_sub_agent_streaming_events: None,
4037 commands: None,
4038 feature_flags: None,
4039 exp_assignments: None,
4040 enable_managed_settings: None,
4041 managed_settings: None,
4042 session_fs_provider: None,
4043 suppress_resume_event: None,
4044 continue_pending_work: None,
4045 permission_handler: None,
4046 elicitation_handler: None,
4047 mcp_auth_handler: None,
4048 user_input_handler: None,
4049 exit_plan_mode_handler: None,
4050 auto_mode_switch_handler: None,
4051 hooks_handler: None,
4052 permission_policy: None,
4053 system_message_transform: None,
4054 skip_custom_instructions: None,
4055 custom_agents_local_only: None,
4056 enable_experimental_mode: None,
4057 coauthor_enabled: None,
4058 manage_schedule_enabled: None,
4059 event_buffer_capacity: None,
4060 }
4061 }
4062
4063 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
4065 self.permission_handler = Some(handler);
4066 self
4067 }
4068
4069 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
4071 self.elicitation_handler = Some(handler);
4072 self
4073 }
4074
4075 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
4077 self.mcp_auth_handler = Some(handler);
4078 self
4079 }
4080
4081 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
4083 self.user_input_handler = Some(handler);
4084 self
4085 }
4086
4087 pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self {
4089 self.ask_user_variant = Some(variant);
4090 self
4091 }
4092
4093 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
4095 self.exit_plan_mode_handler = Some(handler);
4096 self
4097 }
4098
4099 pub fn with_auto_mode_switch_handler(
4101 mut self,
4102 handler: Arc<dyn AutoModeSwitchHandler>,
4103 ) -> Self {
4104 self.auto_mode_switch_handler = Some(handler);
4105 self
4106 }
4107
4108 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
4111 self.hooks_handler = Some(hooks);
4112 self
4113 }
4114
4115 pub fn with_system_message_transform(
4117 mut self,
4118 transform: Arc<dyn SystemMessageTransform>,
4119 ) -> Self {
4120 self.system_message_transform = Some(transform);
4121 self
4122 }
4123
4124 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
4128 self.commands = Some(commands);
4129 self
4130 }
4131
4132 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
4135 self.session_fs_provider = Some(provider);
4136 self
4137 }
4138
4139 pub fn approve_all_permissions(mut self) -> Self {
4142 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
4143 self
4144 }
4145
4146 pub fn deny_all_permissions(mut self) -> Self {
4149 self.permission_policy = Some(crate::permission::Policy::DenyAll);
4150 self
4151 }
4152
4153 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
4156 where
4157 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
4158 {
4159 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
4160 self
4161 }
4162
4163 pub fn with_model(mut self, model: impl Into<String>) -> Self {
4165 self.model = Some(model.into());
4166 self
4167 }
4168
4169 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
4171 self.client_name = Some(name.into());
4172 self
4173 }
4174
4175 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4177 self.reasoning_effort = Some(effort.into());
4178 self
4179 }
4180
4181 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4183 self.reasoning_summary = Some(summary);
4184 self
4185 }
4186
4187 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
4190 self.context_tier = Some(tier.into());
4191 self
4192 }
4193
4194 pub fn with_streaming(mut self, streaming: bool) -> Self {
4196 self.streaming = Some(streaming);
4197 self
4198 }
4199
4200 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
4203 self.system_message = Some(system_message);
4204 self
4205 }
4206
4207 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
4209 self.tools = Some(tools.into_iter().collect());
4210 self
4211 }
4212
4213 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
4215 self.canvases = Some(canvases.into_iter().collect());
4216 self
4217 }
4218
4219 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
4221 self.canvas_handler = Some(handler);
4222 self
4223 }
4224
4225 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
4227 mut self,
4228 open_canvases: I,
4229 ) -> Self {
4230 self.open_canvases = Some(open_canvases.into_iter().collect());
4231 self
4232 }
4233
4234 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
4236 self.request_canvas_renderer = Some(request);
4237 self
4238 }
4239
4240 pub fn with_request_extensions(mut self, request: bool) -> Self {
4242 self.request_extensions = Some(request);
4243 self
4244 }
4245
4246 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
4250 self.extension_sdk_path = Some(path.into());
4251 self
4252 }
4253
4254 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
4256 self.extension_info = Some(extension_info);
4257 self
4258 }
4259
4260 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
4263 self.canvas_provider = Some(canvas_provider);
4264 self
4265 }
4266
4267 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
4269 where
4270 I: IntoIterator<Item = S>,
4271 S: Into<String>,
4272 {
4273 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
4274 self
4275 }
4276
4277 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
4279 where
4280 I: IntoIterator<Item = S>,
4281 S: Into<String>,
4282 {
4283 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
4284 self
4285 }
4286
4287 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
4289 where
4290 I: IntoIterator<Item = S>,
4291 S: Into<String>,
4292 {
4293 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
4294 self
4295 }
4296
4297 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
4299 self.mcp_servers = Some(servers);
4300 self
4301 }
4302
4303 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
4306 self.mcp_oauth_token_storage = Some(mode.into());
4307 self
4308 }
4309
4310 pub fn with_auth_client_id_metadata_url(mut self, url: impl Into<String>) -> Self {
4312 self.auth_client_id_metadata_url = Some(url.into());
4313 self
4314 }
4315
4316 pub fn with_embedding_cache_storage(
4318 mut self,
4319 embedding_cache_storage: impl Into<String>,
4320 ) -> Self {
4321 self.embedding_cache_storage = Some(embedding_cache_storage.into());
4322 self
4323 }
4324
4325 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
4328 self.enable_config_discovery = Some(enable);
4329 self
4330 }
4331
4332 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
4334 self.skip_embedding_retrieval = Some(value);
4335 self
4336 }
4337
4338 pub fn with_organization_custom_instructions(
4340 mut self,
4341 instructions: impl Into<String>,
4342 ) -> Self {
4343 self.organization_custom_instructions = Some(instructions.into());
4344 self
4345 }
4346
4347 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
4349 self.enable_on_demand_instruction_discovery = Some(value);
4350 self
4351 }
4352
4353 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
4355 self.enable_file_hooks = Some(value);
4356 self
4357 }
4358
4359 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
4361 self.enable_host_git_operations = Some(value);
4362 self
4363 }
4364
4365 pub fn with_enable_session_store(mut self, value: bool) -> Self {
4367 self.enable_session_store = Some(value);
4368 self
4369 }
4370
4371 pub fn with_enable_skills(mut self, value: bool) -> Self {
4373 self.enable_skills = Some(value);
4374 self
4375 }
4376
4377 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
4383 self.enable_mcp_apps = Some(enable);
4384 self
4385 }
4386
4387 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
4389 self.github_mcp_tool_config = Some(config);
4390 self
4391 }
4392
4393 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
4395 where
4396 I: IntoIterator<Item = P>,
4397 P: Into<PathBuf>,
4398 {
4399 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
4400 self
4401 }
4402
4403 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
4405 where
4406 I: IntoIterator<Item = S>,
4407 S: Into<String>,
4408 {
4409 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
4410 self
4411 }
4412
4413 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
4417 where
4418 I: IntoIterator<Item = P>,
4419 P: Into<PathBuf>,
4420 {
4421 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
4422 self
4423 }
4424
4425 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
4427 where
4428 I: IntoIterator<Item = P>,
4429 P: Into<PathBuf>,
4430 {
4431 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
4432 self
4433 }
4434
4435 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
4437 self.large_output = Some(config);
4438 self
4439 }
4440
4441 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
4444 self.tool_search = Some(config);
4445 self
4446 }
4447
4448 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
4450 where
4451 I: IntoIterator<Item = S>,
4452 S: Into<String>,
4453 {
4454 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
4455 self
4456 }
4457
4458 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
4460 where
4461 I: IntoIterator<Item = S>,
4462 S: Into<String>,
4463 {
4464 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
4465 self
4466 }
4467
4468 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
4470 mut self,
4471 agents: I,
4472 ) -> Self {
4473 self.custom_agents = Some(agents.into_iter().collect());
4474 self
4475 }
4476
4477 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
4479 self.default_agent = Some(agent);
4480 self
4481 }
4482
4483 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
4485 self.agent = Some(name.into());
4486 self
4487 }
4488
4489 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
4491 self.infinite_sessions = Some(config);
4492 self
4493 }
4494
4495 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
4497 self.provider = Some(provider);
4498 self
4499 }
4500
4501 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
4503 self.capi = Some(capi);
4504 self
4505 }
4506
4507 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
4513 self.providers = Some(providers);
4514 self
4515 }
4516
4517 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
4523 self.models = Some(models);
4524 self
4525 }
4526
4527 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
4531 self.enable_session_telemetry = Some(enable);
4532 self
4533 }
4534
4535 pub fn with_enable_citations(mut self, enable: bool) -> Self {
4537 self.enable_citations = Some(enable);
4538 self
4539 }
4540
4541 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
4544 self.enable_file_change_tracking = Some(enable);
4545 self
4546 }
4547
4548 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
4550 self.session_limits = Some(limits);
4551 self
4552 }
4553
4554 pub fn with_model_capabilities(
4556 mut self,
4557 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
4558 ) -> Self {
4559 self.model_capabilities = Some(capabilities);
4560 self
4561 }
4562
4563 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
4565 self.memory = Some(memory);
4566 self
4567 }
4568
4569 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4571 self.config_directory = Some(dir.into());
4572 self
4573 }
4574
4575 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4577 self.working_directory = Some(dir.into());
4578 self
4579 }
4580
4581 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
4583 where
4584 I: IntoIterator<Item = P>,
4585 P: Into<PathBuf>,
4586 {
4587 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
4588 self
4589 }
4590
4591 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
4595 self.github_token = Some(token.into());
4596 self
4597 }
4598
4599 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
4605 self.github_token_provider = Some(provider);
4606 self
4607 }
4608
4609 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4611 self.include_sub_agent_streaming_events = Some(include);
4612 self
4613 }
4614
4615 pub fn with_remote_session(
4617 mut self,
4618 mode: crate::generated::api_types::RemoteSessionMode,
4619 ) -> Self {
4620 self.remote_session = Some(mode);
4621 self
4622 }
4623
4624 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4627 self.suppress_resume_event = Some(suppress);
4628 self
4629 }
4630
4631 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4637 self.continue_pending_work = Some(continue_pending);
4638 self
4639 }
4640
4641 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4643 self.skip_custom_instructions = Some(value);
4644 self
4645 }
4646
4647 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4649 self.custom_agents_local_only = Some(value);
4650 self
4651 }
4652
4653 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
4655 self.enable_experimental_mode = Some(enable_experimental_mode);
4656 self
4657 }
4658
4659 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4661 self.coauthor_enabled = Some(value);
4662 self
4663 }
4664
4665 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4667 self.manage_schedule_enabled = Some(value);
4668 self
4669 }
4670
4671 pub fn with_feature_flags(mut self, feature_flags: HashMap<String, bool>) -> Self {
4673 self.feature_flags = Some(feature_flags);
4674 self
4675 }
4676
4677 pub fn with_event_buffer_capacity(mut self, capacity: usize) -> Self {
4685 self.event_buffer_capacity = Some(capacity);
4686 self
4687 }
4688
4689 #[doc(hidden)]
4693 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4694 self.exp_assignments = Some(assignments);
4695 self
4696 }
4697
4698 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4701 self.enable_managed_settings = Some(enabled);
4702 self
4703 }
4704
4705 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
4709 self.managed_settings = Some(managed_settings);
4710 self
4711 }
4712}
4713
4714#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4720#[serde(rename_all = "camelCase")]
4721#[non_exhaustive]
4722pub struct SystemMessageConfig {
4723 #[serde(skip_serializing_if = "Option::is_none")]
4725 pub mode: Option<String>,
4726 #[serde(skip_serializing_if = "Option::is_none")]
4728 pub content: Option<String>,
4729 #[serde(skip_serializing_if = "Option::is_none")]
4731 pub sections: Option<HashMap<String, SectionOverride>>,
4732}
4733
4734impl SystemMessageConfig {
4735 pub fn new() -> Self {
4738 Self::default()
4739 }
4740
4741 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4744 self.mode = Some(mode.into());
4745 self
4746 }
4747
4748 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4751 self.content = Some(content.into());
4752 self
4753 }
4754
4755 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4757 self.sections = Some(sections);
4758 self
4759 }
4760}
4761
4762#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4768#[serde(rename_all = "camelCase")]
4769pub struct SectionOverride {
4770 #[serde(skip_serializing_if = "Option::is_none")]
4773 pub action: Option<String>,
4774 #[serde(skip_serializing_if = "Option::is_none")]
4776 pub content: Option<String>,
4777}
4778
4779#[derive(Debug, Clone, Serialize, Deserialize)]
4781#[serde(rename_all = "camelCase")]
4782pub struct CreateSessionResult {
4783 pub session_id: SessionId,
4785 #[serde(skip_serializing_if = "Option::is_none")]
4787 pub workspace_path: Option<PathBuf>,
4788 #[serde(default, alias = "remote_url")]
4790 pub remote_url: Option<String>,
4791 #[serde(skip_serializing_if = "Option::is_none")]
4793 pub capabilities: Option<SessionCapabilities>,
4794}
4795
4796#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4798#[serde(rename_all = "camelCase")]
4799pub(crate) struct ResumeSessionResult {
4800 #[serde(default)]
4802 pub session_id: Option<SessionId>,
4803 #[serde(default, skip_serializing_if = "Option::is_none")]
4805 pub workspace_path: Option<PathBuf>,
4806 #[serde(default, alias = "remote_url")]
4808 pub remote_url: Option<String>,
4809 #[serde(default, skip_serializing_if = "Option::is_none")]
4811 pub capabilities: Option<SessionCapabilities>,
4812 #[serde(
4814 default,
4815 alias = "openCanvasInstances",
4816 skip_serializing_if = "Option::is_none"
4817 )]
4818 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4819}
4820
4821#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4823#[serde(rename_all = "lowercase")]
4824pub enum LogLevel {
4825 #[default]
4827 Info,
4828 Warning,
4830 Error,
4832}
4833
4834#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4839#[serde(rename_all = "camelCase")]
4840pub struct LogOptions {
4841 #[serde(skip_serializing_if = "Option::is_none")]
4843 pub level: Option<LogLevel>,
4844 #[serde(skip_serializing_if = "Option::is_none")]
4847 pub ephemeral: Option<bool>,
4848}
4849
4850impl LogOptions {
4851 pub fn with_level(mut self, level: LogLevel) -> Self {
4853 self.level = Some(level);
4854 self
4855 }
4856
4857 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4859 self.ephemeral = Some(ephemeral);
4860 self
4861 }
4862}
4863
4864#[derive(Debug, Clone, Default)]
4868pub struct SetModelOptions {
4869 pub reasoning_effort: Option<String>,
4872 pub reasoning_summary: Option<ReasoningSummary>,
4876 pub context_tier: Option<ContextTier>,
4879 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4883 pub auto_tier: Option<AutoTierPreference>,
4891}
4892
4893#[derive(Debug, Clone, PartialEq, Eq)]
4902pub enum AutoTierPreference {
4903 Tier(AutoTier),
4905 Reset,
4907}
4908
4909impl SetModelOptions {
4910 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4912 self.reasoning_effort = Some(effort.into());
4913 self
4914 }
4915
4916 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4918 self.reasoning_summary = Some(summary);
4919 self
4920 }
4921
4922 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4924 self.context_tier = Some(tier);
4925 self
4926 }
4927
4928 pub fn with_model_capabilities(
4930 mut self,
4931 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4932 ) -> Self {
4933 self.model_capabilities = Some(caps);
4934 self
4935 }
4936
4937 pub fn with_auto_tier(mut self, tier: AutoTier) -> Self {
4939 self.auto_tier = Some(AutoTierPreference::Tier(tier));
4940 self
4941 }
4942
4943 pub fn with_reset_auto_tier(mut self) -> Self {
4946 self.auto_tier = Some(AutoTierPreference::Reset);
4947 self
4948 }
4949}
4950
4951#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4958#[serde(rename_all = "camelCase")]
4959pub struct PingResponse {
4960 #[serde(default)]
4962 pub message: String,
4963 #[serde(default)]
4965 pub timestamp: String,
4966 #[serde(skip_serializing_if = "Option::is_none")]
4968 pub protocol_version: Option<u32>,
4969}
4970
4971#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4973#[serde(rename_all = "camelCase")]
4974pub struct AttachmentLineRange {
4975 pub start: u32,
4977 pub end: u32,
4979}
4980
4981#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4983#[serde(rename_all = "camelCase")]
4984pub struct AttachmentSelectionPosition {
4985 pub line: u32,
4987 pub character: u32,
4989}
4990
4991#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4993#[serde(rename_all = "camelCase")]
4994pub struct AttachmentSelectionRange {
4995 pub start: AttachmentSelectionPosition,
4997 pub end: AttachmentSelectionPosition,
4999}
5000
5001#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5003#[serde(rename_all = "snake_case")]
5004#[non_exhaustive]
5005pub enum GitHubReferenceType {
5006 Issue,
5008 Pr,
5010 Discussion,
5012}
5013
5014#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5020#[serde(rename_all = "camelCase")]
5021pub struct GitHubRepoPointer {
5022 #[serde(skip_serializing_if = "Option::is_none")]
5024 pub id: Option<i64>,
5025 pub name: String,
5027 pub owner: String,
5029}
5030
5031#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5033#[serde(rename_all = "camelCase")]
5034pub struct GitHubFileDiffSide {
5035 pub path: String,
5037 pub r#ref: String,
5039 pub repo: GitHubRepoPointer,
5041}
5042
5043#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5045#[serde(rename_all = "camelCase")]
5046pub struct GitHubTreeComparisonSide {
5047 pub repo: GitHubRepoPointer,
5049 pub revision: String,
5051}
5052
5053#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5055#[serde(rename_all = "camelCase")]
5056pub struct GitHubSnippetLineRange {
5057 pub start: i64,
5059 pub end: i64,
5061}
5062
5063#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5065#[serde(
5066 tag = "type",
5067 rename_all = "camelCase",
5068 rename_all_fields = "camelCase"
5069)]
5070#[non_exhaustive]
5071pub enum Attachment {
5072 File {
5074 path: PathBuf,
5076 #[serde(skip_serializing_if = "Option::is_none")]
5078 display_name: Option<String>,
5079 #[serde(skip_serializing_if = "Option::is_none")]
5081 line_range: Option<AttachmentLineRange>,
5082 },
5083 Directory {
5085 path: PathBuf,
5087 #[serde(skip_serializing_if = "Option::is_none")]
5089 display_name: Option<String>,
5090 },
5091 Selection {
5093 file_path: PathBuf,
5095 text: String,
5097 #[serde(skip_serializing_if = "Option::is_none")]
5099 display_name: Option<String>,
5100 selection: AttachmentSelectionRange,
5102 },
5103 Blob {
5105 data: String,
5107 mime_type: String,
5109 #[serde(skip_serializing_if = "Option::is_none")]
5111 display_name: Option<String>,
5112 },
5113 #[serde(rename = "github_reference")]
5115 GitHubReference {
5116 number: u64,
5118 title: String,
5120 reference_type: GitHubReferenceType,
5122 state: String,
5124 url: String,
5126 },
5127 #[serde(rename = "github_commit")]
5129 GitHubCommit {
5130 message: String,
5132 oid: String,
5134 repo: GitHubRepoPointer,
5136 url: String,
5138 },
5139 #[serde(rename = "github_release")]
5141 GitHubRelease {
5142 name: String,
5144 repo: GitHubRepoPointer,
5146 tag_name: String,
5148 url: String,
5150 },
5151 #[serde(rename = "github_actions_job")]
5153 GitHubActionsJob {
5154 #[serde(skip_serializing_if = "Option::is_none")]
5157 conclusion: Option<String>,
5158 job_id: i64,
5160 job_name: String,
5162 repo: GitHubRepoPointer,
5164 url: String,
5166 workflow_name: String,
5168 },
5169 #[serde(rename = "github_repository")]
5171 GitHubRepository {
5172 #[serde(skip_serializing_if = "Option::is_none")]
5174 description: Option<String>,
5175 #[serde(skip_serializing_if = "Option::is_none")]
5178 r#ref: Option<String>,
5179 repo: GitHubRepoPointer,
5181 url: String,
5183 },
5184 #[serde(rename = "github_file_diff")]
5186 GitHubFileDiff {
5187 #[serde(skip_serializing_if = "Option::is_none")]
5189 base: Option<GitHubFileDiffSide>,
5190 #[serde(skip_serializing_if = "Option::is_none")]
5192 head: Option<GitHubFileDiffSide>,
5193 url: String,
5195 },
5196 #[serde(rename = "github_tree_comparison")]
5198 GitHubTreeComparison {
5199 base: GitHubTreeComparisonSide,
5201 head: GitHubTreeComparisonSide,
5203 url: String,
5205 },
5206 #[serde(rename = "github_url")]
5208 GitHubUrl {
5209 url: String,
5211 },
5212 #[serde(rename = "github_file")]
5214 GitHubFile {
5215 path: String,
5217 r#ref: String,
5219 repo: GitHubRepoPointer,
5221 url: String,
5223 },
5224 #[serde(rename = "github_snippet")]
5226 GitHubSnippet {
5227 line_range: GitHubSnippetLineRange,
5229 path: String,
5231 r#ref: String,
5233 repo: GitHubRepoPointer,
5235 url: String,
5237 },
5238}
5239
5240impl Attachment {
5241 pub fn display_name(&self) -> Option<&str> {
5243 match self {
5244 Self::File { display_name, .. }
5245 | Self::Directory { display_name, .. }
5246 | Self::Selection { display_name, .. }
5247 | Self::Blob { display_name, .. } => display_name.as_deref(),
5248 Self::GitHubReference { .. }
5249 | Self::GitHubCommit { .. }
5250 | Self::GitHubRelease { .. }
5251 | Self::GitHubActionsJob { .. }
5252 | Self::GitHubRepository { .. }
5253 | Self::GitHubFileDiff { .. }
5254 | Self::GitHubTreeComparison { .. }
5255 | Self::GitHubUrl { .. }
5256 | Self::GitHubFile { .. }
5257 | Self::GitHubSnippet { .. } => None,
5258 }
5259 }
5260
5261 pub fn label(&self) -> Option<String> {
5263 if let Some(display_name) = self
5264 .display_name()
5265 .map(str::trim)
5266 .filter(|name| !name.is_empty())
5267 {
5268 return Some(display_name.to_string());
5269 }
5270
5271 match self {
5272 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
5273 format!("#{}", number)
5274 } else {
5275 title.trim().to_string()
5276 }),
5277 _ => self.derived_display_name(),
5278 }
5279 }
5280
5281 pub fn ensure_display_name(&mut self) {
5283 if self
5284 .display_name()
5285 .map(str::trim)
5286 .is_some_and(|name| !name.is_empty())
5287 {
5288 return;
5289 }
5290
5291 let Some(derived_display_name) = self.derived_display_name() else {
5292 return;
5293 };
5294
5295 match self {
5296 Self::File { display_name, .. }
5297 | Self::Directory { display_name, .. }
5298 | Self::Selection { display_name, .. }
5299 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
5300 Self::GitHubReference { .. }
5301 | Self::GitHubCommit { .. }
5302 | Self::GitHubRelease { .. }
5303 | Self::GitHubActionsJob { .. }
5304 | Self::GitHubRepository { .. }
5305 | Self::GitHubFileDiff { .. }
5306 | Self::GitHubTreeComparison { .. }
5307 | Self::GitHubUrl { .. }
5308 | Self::GitHubFile { .. }
5309 | Self::GitHubSnippet { .. } => {}
5310 }
5311 }
5312
5313 fn derived_display_name(&self) -> Option<String> {
5314 match self {
5315 Self::File { path, .. } | Self::Directory { path, .. } => {
5316 Some(attachment_name_from_path(path))
5317 }
5318 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
5319 Self::Blob { .. } => Some("attachment".to_string()),
5320 Self::GitHubReference { .. }
5321 | Self::GitHubCommit { .. }
5322 | Self::GitHubRelease { .. }
5323 | Self::GitHubActionsJob { .. }
5324 | Self::GitHubRepository { .. }
5325 | Self::GitHubFileDiff { .. }
5326 | Self::GitHubTreeComparison { .. }
5327 | Self::GitHubUrl { .. }
5328 | Self::GitHubFile { .. }
5329 | Self::GitHubSnippet { .. } => None,
5330 }
5331 }
5332}
5333
5334fn attachment_name_from_path(path: &Path) -> String {
5335 path.file_name()
5336 .map(|name| name.to_string_lossy().into_owned())
5337 .filter(|name| !name.is_empty())
5338 .unwrap_or_else(|| {
5339 let full = path.to_string_lossy();
5340 if full.is_empty() {
5341 "attachment".to_string()
5342 } else {
5343 full.into_owned()
5344 }
5345 })
5346}
5347
5348pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
5350 for attachment in attachments {
5351 attachment.ensure_display_name();
5352 }
5353}
5354
5355#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5360#[non_exhaustive]
5361pub enum MessageSource {
5362 User,
5364 System,
5366 Agent(String),
5368}
5369
5370impl std::fmt::Display for MessageSource {
5371 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5372 match self {
5373 Self::User => f.write_str("user"),
5374 Self::System => f.write_str("system"),
5375 Self::Agent(id) => write!(f, "agent-{id}"),
5376 }
5377 }
5378}
5379
5380impl Serialize for MessageSource {
5381 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
5382 serializer.collect_str(self)
5383 }
5384}
5385
5386impl<'de> Deserialize<'de> for MessageSource {
5387 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
5388 let value = String::deserialize(deserializer)?;
5389 match value.as_str() {
5390 "user" => Ok(Self::User),
5391 "system" => Ok(Self::System),
5392 value => value
5393 .strip_prefix("agent-")
5394 .map(|id| Self::Agent(id.to_owned()))
5395 .ok_or_else(|| serde::de::Error::custom("expected user, system, or agent-<id>")),
5396 }
5397 }
5398}
5399
5400#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5405#[serde(rename_all = "lowercase")]
5406#[non_exhaustive]
5407pub enum DeliveryMode {
5408 Enqueue,
5410 Immediate,
5412}
5413
5414#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5419#[serde(rename_all = "lowercase")]
5420#[non_exhaustive]
5421pub enum AgentMode {
5422 Interactive,
5424 Plan,
5426 Autopilot,
5428 Shell,
5430}
5431
5432#[derive(Debug, Clone)]
5461#[non_exhaustive]
5462pub struct MessageOptions {
5463 pub prompt: String,
5465 pub source: Option<MessageSource>,
5468 pub mode: Option<DeliveryMode>,
5474 pub agent_mode: Option<AgentMode>,
5478 pub attachments: Option<Vec<Attachment>>,
5480 pub wait_timeout: Option<Duration>,
5483 pub request_headers: Option<HashMap<String, String>>,
5487 pub traceparent: Option<String>,
5494 pub tracestate: Option<String>,
5498 pub display_prompt: Option<String>,
5500}
5501
5502impl MessageOptions {
5503 pub fn new(prompt: impl Into<String>) -> Self {
5505 Self {
5506 prompt: prompt.into(),
5507 source: None,
5508 mode: None,
5509 agent_mode: None,
5510 attachments: None,
5511 wait_timeout: None,
5512 request_headers: None,
5513 traceparent: None,
5514 tracestate: None,
5515 display_prompt: None,
5516 }
5517 }
5518
5519 pub fn with_source(mut self, source: MessageSource) -> Self {
5521 self.source = Some(source);
5522 self
5523 }
5524
5525 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
5531 self.mode = Some(mode);
5532 self
5533 }
5534
5535 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
5539 self.agent_mode = Some(agent_mode);
5540 self
5541 }
5542
5543 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
5545 self.attachments = Some(attachments);
5546 self
5547 }
5548
5549 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
5551 self.wait_timeout = Some(timeout);
5552 self
5553 }
5554
5555 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
5557 self.request_headers = Some(headers);
5558 self
5559 }
5560
5561 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
5566 self.traceparent = ctx.traceparent;
5567 self.tracestate = ctx.tracestate;
5568 self
5569 }
5570
5571 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5573 self.traceparent = Some(traceparent.into());
5574 self
5575 }
5576
5577 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5579 self.tracestate = Some(tracestate.into());
5580 self
5581 }
5582
5583 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5585 self.display_prompt = Some(display_prompt.into());
5586 self
5587 }
5588}
5589
5590impl From<&str> for MessageOptions {
5591 fn from(prompt: &str) -> Self {
5592 Self::new(prompt)
5593 }
5594}
5595
5596impl From<String> for MessageOptions {
5597 fn from(prompt: String) -> Self {
5598 Self::new(prompt)
5599 }
5600}
5601
5602impl From<&String> for MessageOptions {
5603 fn from(prompt: &String) -> Self {
5604 Self::new(prompt.clone())
5605 }
5606}
5607
5608#[derive(Debug, Clone, Serialize, Deserialize)]
5610#[serde(rename_all = "camelCase")]
5611#[non_exhaustive]
5612pub struct GetStatusResponse {
5613 pub version: String,
5615 pub protocol_version: u32,
5617}
5618
5619#[derive(Debug, Clone, Serialize, Deserialize)]
5621#[serde(rename_all = "camelCase")]
5622#[non_exhaustive]
5623pub struct GetAuthStatusResponse {
5624 pub is_authenticated: bool,
5626 #[serde(skip_serializing_if = "Option::is_none")]
5629 pub auth_type: Option<String>,
5630 #[serde(skip_serializing_if = "Option::is_none")]
5632 pub host: Option<String>,
5633 #[serde(skip_serializing_if = "Option::is_none")]
5635 pub login: Option<String>,
5636 #[serde(skip_serializing_if = "Option::is_none")]
5638 pub status_message: Option<String>,
5639}
5640
5641#[derive(Debug, Clone, Serialize, Deserialize)]
5645#[serde(rename_all = "camelCase")]
5646pub struct SessionEventNotification {
5647 pub session_id: SessionId,
5649 pub event: SessionEvent,
5651}
5652
5653#[derive(Debug, Clone, Serialize, Deserialize)]
5660#[serde(rename_all = "camelCase")]
5661pub struct SessionEvent {
5662 pub id: String,
5664 pub timestamp: String,
5666 pub parent_id: Option<String>,
5668 #[serde(skip_serializing_if = "Option::is_none")]
5670 pub ephemeral: Option<bool>,
5671 #[serde(skip_serializing_if = "Option::is_none")]
5674 pub agent_id: Option<String>,
5675 #[serde(skip_serializing_if = "Option::is_none")]
5677 pub debug_cli_received_at_ms: Option<i64>,
5678 #[serde(skip_serializing_if = "Option::is_none")]
5680 pub debug_ws_forwarded_at_ms: Option<i64>,
5681 #[serde(rename = "type")]
5683 pub event_type: String,
5684 pub data: Value,
5686}
5687
5688impl SessionEvent {
5689 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5694 use serde::de::IntoDeserializer;
5695 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5696 self.event_type.as_str().into_deserializer();
5697 crate::generated::SessionEventType::deserialize(deserializer)
5698 .unwrap_or(crate::generated::SessionEventType::Unknown)
5699 }
5700
5701 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5707 serde_json::from_value(self.data.clone()).ok()
5708 }
5709
5710 pub fn is_transient_error(&self) -> bool {
5714 self.event_type == "session.error"
5715 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5716 }
5717}
5718
5719#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5724#[serde(rename_all = "camelCase")]
5725#[non_exhaustive]
5726pub struct ToolInvocation {
5727 pub session_id: SessionId,
5729 pub tool_call_id: String,
5731 pub tool_name: String,
5733 pub arguments: Value,
5735 #[serde(skip)]
5743 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5744 #[serde(default, skip_serializing_if = "Option::is_none")]
5749 pub traceparent: Option<String>,
5750 #[serde(default, skip_serializing_if = "Option::is_none")]
5753 pub tracestate: Option<String>,
5754}
5755
5756impl ToolInvocation {
5757 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5778 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5779 }
5780
5781 pub fn trace_context(&self) -> TraceContext {
5784 TraceContext {
5785 traceparent: self.traceparent.clone(),
5786 tracestate: self.tracestate.clone(),
5787 }
5788 }
5789}
5790
5791#[derive(Debug, Clone, Serialize, Deserialize)]
5793#[serde(rename_all = "camelCase")]
5794pub struct ToolBinaryResult {
5795 pub data: String,
5797 pub mime_type: String,
5799 pub r#type: String,
5801 #[serde(default, skip_serializing_if = "Option::is_none")]
5803 pub description: Option<String>,
5804}
5805
5806#[derive(Debug, Clone, Serialize, Deserialize)]
5813#[serde(rename_all = "camelCase")]
5814#[non_exhaustive]
5815pub struct ToolResultExpanded {
5816 pub text_result_for_llm: String,
5818 pub result_type: String,
5820 #[serde(default, skip_serializing_if = "Option::is_none")]
5822 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5823 #[serde(skip_serializing_if = "Option::is_none")]
5825 pub session_log: Option<String>,
5826 #[serde(skip_serializing_if = "Option::is_none")]
5828 pub error: Option<String>,
5829 #[serde(default, skip_serializing_if = "Option::is_none")]
5831 pub tool_telemetry: Option<HashMap<String, Value>>,
5832 #[serde(default, skip_serializing_if = "Option::is_none")]
5834 pub tool_references: Option<Vec<String>>,
5835}
5836
5837impl ToolResultExpanded {
5838 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5842 Self {
5843 text_result_for_llm: text_result_for_llm.into(),
5844 result_type: result_type.into(),
5845 binary_results_for_llm: None,
5846 session_log: None,
5847 error: None,
5848 tool_telemetry: None,
5849 tool_references: None,
5850 }
5851 }
5852
5853 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5855 self.binary_results_for_llm = Some(results);
5856 self
5857 }
5858
5859 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5861 self.session_log = Some(session_log.into());
5862 self
5863 }
5864
5865 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5867 self.error = Some(error.into());
5868 self
5869 }
5870
5871 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5873 self.tool_telemetry = Some(telemetry);
5874 self
5875 }
5876
5877 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5879 where
5880 I: IntoIterator<Item = S>,
5881 S: Into<String>,
5882 {
5883 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5884 self
5885 }
5886}
5887
5888#[derive(Debug, Clone, Serialize, Deserialize)]
5890#[serde(untagged)]
5891#[non_exhaustive]
5892pub enum ToolResult {
5893 Text(String),
5895 Expanded(ToolResultExpanded),
5897}
5898
5899#[derive(Debug, Clone, Serialize, Deserialize)]
5901#[serde(rename_all = "camelCase")]
5902pub struct ToolResultResponse {
5903 pub result: ToolResult,
5905}
5906
5907#[derive(Debug, Clone, Serialize, Deserialize)]
5909#[serde(rename_all = "camelCase")]
5910pub struct SessionMetadata {
5911 pub session_id: SessionId,
5913 pub start_time: String,
5915 pub modified_time: String,
5917 #[serde(skip_serializing_if = "Option::is_none")]
5919 pub summary: Option<String>,
5920 pub is_remote: bool,
5922}
5923
5924#[derive(Debug, Clone, Serialize, Deserialize)]
5926#[serde(rename_all = "camelCase")]
5927pub struct ListSessionsResponse {
5928 pub sessions: Vec<SessionMetadata>,
5930}
5931
5932#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5936#[serde(rename_all = "camelCase")]
5937pub struct SessionListFilter {
5938 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5940 pub working_directory: Option<String>,
5941 #[serde(default, skip_serializing_if = "Option::is_none")]
5943 pub git_root: Option<String>,
5944 #[serde(default, skip_serializing_if = "Option::is_none")]
5946 pub repository: Option<String>,
5947 #[serde(default, skip_serializing_if = "Option::is_none")]
5949 pub branch: Option<String>,
5950}
5951
5952#[derive(Debug, Clone, Serialize, Deserialize)]
5954#[serde(rename_all = "camelCase")]
5955pub struct GetSessionMetadataResponse {
5956 #[serde(skip_serializing_if = "Option::is_none")]
5958 pub session: Option<SessionMetadata>,
5959}
5960
5961#[derive(Debug, Clone, Serialize, Deserialize)]
5963#[serde(rename_all = "camelCase")]
5964pub struct GetLastSessionIdResponse {
5965 #[serde(skip_serializing_if = "Option::is_none")]
5967 pub session_id: Option<SessionId>,
5968}
5969
5970#[derive(Debug, Clone, Serialize, Deserialize)]
5972#[serde(rename_all = "camelCase")]
5973pub struct GetForegroundSessionResponse {
5974 #[serde(skip_serializing_if = "Option::is_none")]
5976 pub session_id: Option<SessionId>,
5977}
5978
5979#[derive(Debug, Clone, Serialize, Deserialize)]
5981#[serde(rename_all = "camelCase")]
5982pub struct GetMessagesResponse {
5983 pub events: Vec<SessionEvent>,
5985}
5986
5987#[derive(Debug, Clone, Serialize, Deserialize)]
5989#[serde(rename_all = "camelCase")]
5990pub struct ElicitationResult {
5991 pub action: String,
5993 #[serde(skip_serializing_if = "Option::is_none")]
5995 pub content: Option<Value>,
5996}
5997
5998#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6004#[serde(rename_all = "camelCase")]
6005#[non_exhaustive]
6006pub enum ElicitationMode {
6007 Form,
6009 Url,
6011 #[serde(other)]
6013 Unknown,
6014}
6015
6016#[derive(Debug, Clone, Serialize, Deserialize)]
6023#[serde(rename_all = "camelCase")]
6024pub struct ElicitationRequest {
6025 pub message: String,
6027 #[serde(skip_serializing_if = "Option::is_none")]
6029 pub requested_schema: Option<Value>,
6030 #[serde(skip_serializing_if = "Option::is_none")]
6032 pub mode: Option<ElicitationMode>,
6033 #[serde(skip_serializing_if = "Option::is_none")]
6035 pub elicitation_source: Option<String>,
6036 #[serde(skip_serializing_if = "Option::is_none")]
6038 pub url: Option<String>,
6039}
6040
6041#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6046#[serde(rename_all = "camelCase")]
6047pub struct SessionCapabilities {
6048 #[serde(skip_serializing_if = "Option::is_none")]
6050 pub ui: Option<UiCapabilities>,
6051}
6052
6053#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6055#[serde(rename_all = "camelCase")]
6056pub struct UiCapabilities {
6057 #[serde(skip_serializing_if = "Option::is_none")]
6059 pub elicitation: Option<bool>,
6060 #[serde(skip_serializing_if = "Option::is_none")]
6071 pub mcp_apps: Option<bool>,
6072 #[serde(skip_serializing_if = "Option::is_none")]
6074 pub canvases: Option<bool>,
6075}
6076
6077#[derive(Debug, Clone, Default)]
6079pub struct UiInputOptions<'a> {
6080 pub title: Option<&'a str>,
6082 pub description: Option<&'a str>,
6084 pub min_length: Option<u64>,
6086 pub max_length: Option<u64>,
6088 pub format: Option<InputFormat>,
6090 pub default: Option<&'a str>,
6092}
6093
6094#[derive(Debug, Clone, Copy)]
6096#[non_exhaustive]
6097pub enum InputFormat {
6098 Email,
6100 Uri,
6102 Date,
6104 DateTime,
6106}
6107
6108impl InputFormat {
6109 pub fn as_str(&self) -> &'static str {
6111 match self {
6112 Self::Email => "email",
6113 Self::Uri => "uri",
6114 Self::Date => "date",
6115 Self::DateTime => "date-time",
6116 }
6117 }
6118}
6119
6120pub use crate::generated::api_types::{
6125 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
6126 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
6127 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
6128 PermissionDecisionApproveOnce, PermissionDecisionContext, PermissionDecisionOutcome,
6129 PermissionDecisionReject, PermissionDecisionSource, PermissionDecisionSurface,
6130 PermissionDecisionUserNotAvailable, PermissionResponseCapability,
6131};
6132
6133#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6139#[serde(rename_all = "kebab-case")]
6140#[non_exhaustive]
6141pub enum PermissionRequestKind {
6142 Shell,
6144 Write,
6146 Read,
6148 Url,
6150 Mcp,
6152 CustomTool,
6154 Memory,
6156 Hook,
6158 #[serde(other)]
6161 Unknown,
6162}
6163
6164#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6170#[serde(rename_all = "camelCase")]
6171pub struct PermissionRequestData {
6172 #[serde(default, skip_serializing_if = "Option::is_none")]
6176 pub kind: Option<PermissionRequestKind>,
6177 #[serde(default, skip_serializing_if = "Option::is_none")]
6180 pub tool_call_id: Option<String>,
6181 #[serde(default, skip_serializing_if = "Option::is_none")]
6183 pub managed_approval_required: Option<bool>,
6184 #[serde(default, skip_serializing_if = "is_false")]
6186 pub managed_settings_enabled: bool,
6187 #[serde(flatten)]
6191 pub extra: Value,
6192}
6193
6194#[derive(Debug, Clone, Serialize, Deserialize)]
6196#[serde(rename_all = "camelCase")]
6197pub struct ExitPlanModeData {
6198 #[serde(default)]
6200 pub summary: String,
6201 #[serde(default, skip_serializing_if = "Option::is_none")]
6203 pub plan_content: Option<String>,
6204 #[serde(default)]
6206 pub actions: Vec<String>,
6207 #[serde(default = "default_recommended_action")]
6209 pub recommended_action: String,
6210}
6211
6212fn default_recommended_action() -> String {
6213 "autopilot".to_string()
6214}
6215
6216impl Default for ExitPlanModeData {
6217 fn default() -> Self {
6218 Self {
6219 summary: String::new(),
6220 plan_content: None,
6221 actions: Vec::new(),
6222 recommended_action: default_recommended_action(),
6223 }
6224 }
6225}
6226
6227#[cfg(test)]
6228mod tests {
6229 use std::collections::HashMap;
6230 use std::path::PathBuf;
6231
6232 use serde_json::json;
6233
6234 use super::{
6235 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
6236 AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions,
6237 ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode,
6238 ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
6239 InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
6240 MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig,
6241 ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
6242 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
6243 ToolResultResponse, ensure_attachment_display_names,
6244 };
6245 use crate::generated::session_events::TypedSessionEvent;
6246
6247 #[test]
6248 fn permission_response_capability_is_publicly_exported() {
6249 assert_eq!(
6250 serde_json::to_value(PermissionResponseCapability::Interactive).unwrap(),
6251 json!("interactive")
6252 );
6253 }
6254
6255 #[test]
6256 fn tool_builder_composes() {
6257 let tool = Tool::new("greet")
6258 .with_description("Say hello")
6259 .with_namespaced_name("hello/greet")
6260 .with_instructions("Pass the user's name")
6261 .with_parameters(json!({
6262 "type": "object",
6263 "properties": { "name": { "type": "string" } },
6264 "required": ["name"]
6265 }))
6266 .with_overrides_built_in_tool(true)
6267 .with_skip_permission(true);
6268 assert_eq!(tool.name, "greet");
6269 assert_eq!(tool.description, "Say hello");
6270 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
6271 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
6272 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
6273 assert!(tool.overrides_built_in_tool);
6274 assert!(tool.skip_permission);
6275 }
6276
6277 #[test]
6278 fn tool_defer_serialization() {
6279 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
6280 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
6281 let value = serde_json::to_value(&tool).unwrap();
6282 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
6283
6284 let plain = Tool::new("plain");
6285 let value = serde_json::to_value(&plain).unwrap();
6286 assert!(value.get("defer").is_none());
6287 }
6288
6289 #[test]
6290 fn tool_metadata_serialization() {
6291 use indexmap::IndexMap;
6292
6293 let mut metadata = IndexMap::new();
6294 metadata.insert(
6295 "github.com/copilot:safeForTelemetry".to_string(),
6296 json!({ "name": true, "inputsNames": false }),
6297 );
6298 let tool = Tool::new("lookup").with_metadata(metadata);
6299 let value = serde_json::to_value(&tool).unwrap();
6300 assert_eq!(
6301 value
6302 .get("metadata")
6303 .unwrap()
6304 .get("github.com/copilot:safeForTelemetry")
6305 .unwrap(),
6306 &json!({ "name": true, "inputsNames": false })
6307 );
6308
6309 let plain = Tool::new("plain");
6311 let value = serde_json::to_value(&plain).unwrap();
6312 assert!(value.get("metadata").is_none());
6313 }
6314
6315 #[test]
6316 fn custom_agent_config_builder_with_model() {
6317 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
6318 .with_model("claude-haiku-4.5")
6319 .with_display_name("My Agent");
6320 assert_eq!(agent.name, "my-agent");
6321 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
6322 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
6323 }
6324
6325 #[test]
6326 fn custom_agent_config_serializes_model() {
6327 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
6328 let wire = serde_json::to_value(&agent).unwrap();
6329 assert_eq!(wire["model"], "claude-haiku-4.5");
6330 assert_eq!(wire["name"], "model-agent");
6331 }
6332
6333 #[test]
6334 fn custom_agent_config_omits_model_when_none() {
6335 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
6336 let wire = serde_json::to_value(&agent).unwrap();
6337 assert!(wire.get("model").is_none());
6338 }
6339
6340 #[test]
6341 fn custom_agent_config_builder_with_reasoning_effort() {
6342 let agent =
6343 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6344 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
6345 }
6346
6347 #[test]
6348 fn custom_agent_config_serializes_reasoning_effort() {
6349 let agent =
6350 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6351 let wire = serde_json::to_value(&agent).unwrap();
6352 assert_eq!(wire["reasoningEffort"], "high");
6353 }
6354
6355 #[test]
6356 fn custom_agent_config_omits_reasoning_effort_when_none() {
6357 let agent = CustomAgentConfig::new("default-agent", "prompt");
6358 let wire = serde_json::to_value(&agent).unwrap();
6359 assert!(wire.get("reasoningEffort").is_none());
6360 }
6361
6362 #[test]
6363 #[should_panic(expected = "tool parameter schema must be a JSON object")]
6364 fn tool_with_parameters_panics_on_non_object_value() {
6365 let _ = Tool::new("noop").with_parameters(json!(null));
6366 }
6367
6368 #[test]
6369 fn tool_result_expanded_serializes_binary_results_for_llm() {
6370 let response = ToolResultResponse {
6371 result: ToolResult::Expanded(ToolResultExpanded {
6372 text_result_for_llm: "rendered chart".to_string(),
6373 result_type: "success".to_string(),
6374 binary_results_for_llm: Some(vec![ToolBinaryResult {
6375 data: "aW1n".to_string(),
6376 mime_type: "image/png".to_string(),
6377 r#type: "image".to_string(),
6378 description: Some("chart preview".to_string()),
6379 }]),
6380 session_log: None,
6381 error: None,
6382 tool_telemetry: None,
6383 tool_references: None,
6384 }),
6385 };
6386
6387 let wire = serde_json::to_value(&response).unwrap();
6388
6389 assert_eq!(
6390 wire,
6391 json!({
6392 "result": {
6393 "textResultForLlm": "rendered chart",
6394 "resultType": "success",
6395 "binaryResultsForLlm": [
6396 {
6397 "data": "aW1n",
6398 "mimeType": "image/png",
6399 "type": "image",
6400 "description": "chart preview"
6401 }
6402 ]
6403 }
6404 })
6405 );
6406 }
6407
6408 #[test]
6409 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
6410 let response = ToolResultResponse {
6411 result: ToolResult::Expanded(ToolResultExpanded {
6412 text_result_for_llm: "ok".to_string(),
6413 result_type: "success".to_string(),
6414 binary_results_for_llm: None,
6415 session_log: None,
6416 error: None,
6417 tool_telemetry: None,
6418 tool_references: None,
6419 }),
6420 };
6421
6422 let wire = serde_json::to_value(&response).unwrap();
6423
6424 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6425 assert!(wire["result"].get("binaryResultsForLlm").is_none());
6426 }
6427
6428 #[test]
6429 fn tool_result_expanded_serializes_tool_references() {
6430 let response = ToolResultResponse {
6431 result: ToolResult::Expanded(
6432 ToolResultExpanded::new("found 2 tools", "success")
6433 .with_tool_references(["get_weather", "check_status"]),
6434 ),
6435 };
6436
6437 let wire = serde_json::to_value(&response).unwrap();
6438
6439 assert_eq!(
6440 wire,
6441 json!({
6442 "result": {
6443 "textResultForLlm": "found 2 tools",
6444 "resultType": "success",
6445 "toolReferences": ["get_weather", "check_status"]
6446 }
6447 })
6448 );
6449 }
6450
6451 #[test]
6452 fn tool_result_expanded_omits_tool_references_when_none() {
6453 let response = ToolResultResponse {
6454 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
6455 };
6456
6457 let wire = serde_json::to_value(&response).unwrap();
6458
6459 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6460 assert!(wire["result"].get("toolReferences").is_none());
6461 }
6462
6463 #[test]
6464 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
6465 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
6468 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
6469
6470 assert_eq!(
6471 expanded.tool_references.as_deref(),
6472 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6473 );
6474 }
6475
6476 #[test]
6477 fn tool_result_expanded_deserializes_tool_references() {
6478 let wire = json!({
6479 "textResultForLlm": "found tools",
6480 "resultType": "success",
6481 "toolReferences": ["alpha", "beta"]
6482 });
6483
6484 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
6485
6486 assert_eq!(
6487 expanded.tool_references.as_deref(),
6488 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6489 );
6490 }
6491
6492 #[test]
6493 fn session_config_default_wire_flags_off_without_handlers() {
6494 let cfg = SessionConfig::default();
6495 assert_eq!(cfg.mcp_oauth_token_storage, None);
6496 let (wire, _runtime) = cfg
6500 .into_wire(Some(SessionId::from("default-flags")))
6501 .expect("default config has no duplicate handlers");
6502 assert!(!wire.request_user_input);
6503 assert!(!wire.request_permission);
6504 assert!(!wire.request_elicitation);
6505 assert!(!wire.request_exit_plan_mode);
6506 assert!(!wire.request_auto_mode_switch);
6507 assert!(!wire.hooks);
6508 assert!(!wire.request_mcp_apps);
6509 let json = serde_json::to_value(&wire).unwrap();
6510 assert!(json.get("askUserVariant").is_none());
6511 }
6512
6513 #[test]
6514 fn resume_session_config_new_wire_flags_off_without_handlers() {
6515 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
6516 assert_eq!(cfg.mcp_oauth_token_storage, None);
6517 let (wire, _runtime) = cfg
6518 .into_wire()
6519 .expect("default resume config has no duplicate handlers");
6520 assert!(!wire.request_user_input);
6521 assert!(!wire.request_permission);
6522 assert!(!wire.request_elicitation);
6523 assert!(!wire.request_exit_plan_mode);
6524 assert!(!wire.request_auto_mode_switch);
6525 assert!(!wire.hooks);
6526 assert!(!wire.request_mcp_apps);
6527 let json = serde_json::to_value(&wire).unwrap();
6528 assert!(json.get("askUserVariant").is_none());
6529 }
6530
6531 #[test]
6532 fn custom_agents_local_only_serializes_on_create_and_resume() {
6533 let (create_wire, _) = SessionConfig::default()
6534 .with_custom_agents_local_only(false)
6535 .into_wire(Some(SessionId::from("create-locality")))
6536 .expect("create config has no duplicate handlers");
6537 let create_json = serde_json::to_value(&create_wire).unwrap();
6538 assert_eq!(create_json["customAgentsLocalOnly"], false);
6539
6540 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
6541 .with_custom_agents_local_only(false)
6542 .into_wire()
6543 .expect("resume config has no duplicate handlers");
6544 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6545 assert_eq!(resume_json["customAgentsLocalOnly"], false);
6546
6547 let (unset_create_wire, _) = SessionConfig::default()
6548 .into_wire(Some(SessionId::from("create-unset")))
6549 .expect("create config has no duplicate handlers");
6550 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6551 assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
6552
6553 let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
6554 .into_wire()
6555 .expect("resume config has no duplicate handlers");
6556 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6557 assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
6558 }
6559
6560 #[test]
6561 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6562 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
6563 assert_eq!(cfg.enable_mcp_apps, Some(true));
6564
6565 let (wire, _runtime) = cfg
6566 .into_wire(Some(SessionId::from("enable-mcp-apps")))
6567 .expect("enable_mcp_apps config has no duplicate handlers");
6568 assert!(wire.request_mcp_apps);
6569
6570 let json = serde_json::to_value(&wire).unwrap();
6571 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6572 }
6573
6574 #[test]
6575 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6576 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
6577 .with_enable_mcp_apps(true);
6578 assert_eq!(cfg.enable_mcp_apps, Some(true));
6579
6580 let (wire, _runtime) = cfg
6581 .into_wire()
6582 .expect("resume enable_mcp_apps config has no duplicate handlers");
6583 assert!(wire.request_mcp_apps);
6584
6585 let json = serde_json::to_value(&wire).unwrap();
6586 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6587 }
6588
6589 #[test]
6590 fn github_mcp_tool_config_serializes_for_create_and_resume() {
6591 let github_config = GitHubMcpToolConfig::new()
6592 .with_enable_all_tools(true)
6593 .with_additional_toolsets(["repos"])
6594 .with_additional_tools(["get_issue"])
6595 .with_enable_insiders_mode(true)
6596 .with_disable_form_deferral(true);
6597
6598 let (create_wire, _) = SessionConfig::default()
6599 .with_github_mcp_tool_config(github_config.clone())
6600 .into_wire(Some(SessionId::from("github-mcp")))
6601 .expect("create config has no duplicate handlers");
6602 assert_eq!(
6603 serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6604 serde_json::json!({
6605 "enableAllTools": true,
6606 "additionalToolsets": ["repos"],
6607 "additionalTools": ["get_issue"],
6608 "enableInsidersMode": true,
6609 "disableFormDeferral": true,
6610 })
6611 );
6612
6613 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6614 .with_github_mcp_tool_config(github_config)
6615 .into_wire()
6616 .expect("resume config has no duplicate handlers");
6617 assert!(resume_wire.github_mcp_tool_config.is_some());
6618
6619 let (unset_wire, _) = SessionConfig::default()
6620 .into_wire(Some(SessionId::from("github-mcp-unset")))
6621 .expect("default config has no duplicate handlers");
6622 assert!(
6623 serde_json::to_value(&unset_wire)
6624 .unwrap()
6625 .get("githubMcpToolConfig")
6626 .is_none()
6627 );
6628 }
6629
6630 #[test]
6631 fn memory_configuration_constructors_and_serde() {
6632 assert!(MemoryConfiguration::enabled().enabled);
6633 assert!(!MemoryConfiguration::disabled().enabled);
6634 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6635
6636 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6637 assert_eq!(json, serde_json::json!({ "enabled": true }));
6638 }
6639
6640 #[test]
6641 fn session_config_with_memory_serializes() {
6642 let (wire, _runtime) = SessionConfig::default()
6643 .with_memory(MemoryConfiguration::enabled())
6644 .into_wire(Some(SessionId::from("memory-on")))
6645 .expect("no duplicate handlers");
6646 let json = serde_json::to_value(&wire).unwrap();
6647 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6648
6649 let (wire_off, _) = SessionConfig::default()
6650 .with_memory(MemoryConfiguration::disabled())
6651 .into_wire(Some(SessionId::from("memory-off")))
6652 .expect("no duplicate handlers");
6653 let json_off = serde_json::to_value(&wire_off).unwrap();
6654 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6655
6656 let (empty_wire, _) = SessionConfig::default()
6658 .into_wire(Some(SessionId::from("memory-unset")))
6659 .expect("no duplicate handlers");
6660 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6661 assert!(empty_json.get("memory").is_none());
6662 }
6663
6664 #[test]
6665 fn resume_session_config_with_memory_serializes() {
6666 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6667 .with_memory(MemoryConfiguration::enabled())
6668 .into_wire()
6669 .expect("no duplicate handlers");
6670 let json = serde_json::to_value(&wire).unwrap();
6671 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6672
6673 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6675 .into_wire()
6676 .expect("no duplicate handlers");
6677 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6678 assert!(empty_json.get("memory").is_none());
6679 }
6680
6681 #[test]
6682 fn feature_flags_serialize_on_create_and_resume() {
6683 let feature_flags = HashMap::from([
6684 ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true),
6685 ("DISABLED_TEST_FLAG".to_string(), false),
6686 ]);
6687 let expected = serde_json::json!({
6688 "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true,
6689 "DISABLED_TEST_FLAG": false,
6690 });
6691
6692 let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone());
6693 assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags));
6694 let (create_wire, _) = create_config
6695 .into_wire(Some(SessionId::from("feature-flags-create")))
6696 .expect("no duplicate handlers");
6697 let create_json = serde_json::to_value(&create_wire).unwrap();
6698 assert_eq!(create_json["featureFlags"], expected);
6699
6700 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume"))
6701 .with_feature_flags(feature_flags)
6702 .into_wire()
6703 .expect("no duplicate handlers");
6704 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6705 assert_eq!(resume_json["featureFlags"], expected);
6706
6707 let (unset_create_wire, _) = SessionConfig::default()
6708 .into_wire(Some(SessionId::from("feature-flags-create-unset")))
6709 .expect("no duplicate handlers");
6710 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6711 assert!(unset_create_json.get("featureFlags").is_none());
6712
6713 let (unset_resume_wire, _) =
6714 ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset"))
6715 .into_wire()
6716 .expect("no duplicate handlers");
6717 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6718 assert!(unset_resume_json.get("featureFlags").is_none());
6719 }
6720
6721 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6722 CopilotExpAssignmentResponse {
6723 features: vec!["copilot_exp_flag".to_string()],
6724 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6725 configs: vec![ExpConfigEntry {
6726 id: "cfg-1".to_string(),
6727 parameters: HashMap::from([
6728 ("threshold".to_string(), ExpFlagValue::Integer(5)),
6729 ("enabled".to_string(), ExpFlagValue::Bool(true)),
6730 ]),
6731 }],
6732 assignment_context: context.to_string(),
6733 ..Default::default()
6734 }
6735 }
6736
6737 #[test]
6738 fn exp_flag_value_round_trips_all_variants() {
6739 let values = serde_json::json!({
6740 "s": "text",
6741 "i": 7,
6742 "f": 1.5,
6743 "b": true,
6744 "n": null,
6745 });
6746 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6747 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6748 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6749 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6750 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6751 assert_eq!(parsed["n"], ExpFlagValue::Null);
6752 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6753 }
6754
6755 #[test]
6756 fn session_config_with_exp_assignments_serializes() {
6757 let assignments = sample_exp_assignments("ctx-123");
6758 let expected = serde_json::to_value(&assignments).unwrap();
6759 let (wire, _runtime) = SessionConfig::default()
6760 .with_exp_assignments(assignments)
6761 .into_wire(Some(SessionId::from("exp-on")))
6762 .expect("no duplicate handlers");
6763 let json = serde_json::to_value(&wire).unwrap();
6764 assert_eq!(json["expAssignments"], expected);
6765 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6766 assert_eq!(
6767 json["expAssignments"]["Flights"]["copilot_exp_flag"],
6768 "treatment"
6769 );
6770
6771 let (empty_wire, _) = SessionConfig::default()
6773 .into_wire(Some(SessionId::from("exp-unset")))
6774 .expect("no duplicate handlers");
6775 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6776 assert!(empty_json.get("expAssignments").is_none());
6777 }
6778
6779 #[test]
6780 fn resume_session_config_with_exp_assignments_serializes() {
6781 let assignments = sample_exp_assignments("ctx-456");
6782 let expected = serde_json::to_value(&assignments).unwrap();
6783 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6784 .with_exp_assignments(assignments)
6785 .into_wire()
6786 .expect("no duplicate handlers");
6787 let json = serde_json::to_value(&wire).unwrap();
6788 assert_eq!(json["expAssignments"], expected);
6789
6790 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6792 .into_wire()
6793 .expect("no duplicate handlers");
6794 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6795 assert!(empty_json.get("expAssignments").is_none());
6796 }
6797
6798 #[test]
6799 fn session_config_clone_preserves_exp_assignments() {
6800 let assignments = sample_exp_assignments("ctx-clone");
6801 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6802 let cloned = config.clone();
6803
6804 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6805
6806 let (wire, _runtime) = cloned
6807 .into_wire(Some(SessionId::from("exp-clone")))
6808 .expect("no duplicate handlers");
6809 let json = serde_json::to_value(&wire).unwrap();
6810 assert_eq!(
6811 json["expAssignments"],
6812 serde_json::to_value(&assignments).unwrap()
6813 );
6814 }
6815
6816 #[test]
6817 fn resume_session_config_clone_preserves_exp_assignments() {
6818 let assignments = sample_exp_assignments("ctx-clone-resume");
6819 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6820 .with_exp_assignments(assignments.clone());
6821 let cloned = config.clone();
6822
6823 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6824
6825 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6826 let json = serde_json::to_value(&wire).unwrap();
6827 assert_eq!(
6828 json["expAssignments"],
6829 serde_json::to_value(&assignments).unwrap()
6830 );
6831 }
6832
6833 #[test]
6834 #[allow(clippy::field_reassign_with_default)]
6835 fn session_config_into_wire_serializes_bucket_b_fields() {
6836 use std::path::PathBuf;
6837
6838 use super::{CloudSessionOptions, CloudSessionRepository};
6839
6840 let mut cfg = SessionConfig::default();
6841 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6842 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6843 cfg.github_token = Some("ghs_secret".to_string());
6844 cfg.include_sub_agent_streaming_events = Some(false);
6845 cfg.enable_session_telemetry = Some(false);
6846 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
6847 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
6848 cfg.enable_on_demand_instruction_discovery = Some(false);
6849 cfg.cloud = Some(CloudSessionOptions::with_repository(
6850 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
6851 ));
6852
6853 let (wire, _runtime) = cfg
6854 .into_wire(Some(SessionId::from("custom-id")))
6855 .expect("no duplicate handlers");
6856 let wire_json = serde_json::to_value(&wire).unwrap();
6857 assert_eq!(wire_json["sessionId"], "custom-id");
6858 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6859 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6860 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6861 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6862 assert_eq!(wire_json["enableSessionTelemetry"], false);
6863 assert_eq!(wire_json["reasoningSummary"], "concise");
6864 assert_eq!(wire_json["remoteSession"], "export");
6865 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6866 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6867 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6868 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6869
6870 let (empty_wire, _) = SessionConfig::default()
6872 .into_wire(Some(SessionId::from("empty")))
6873 .expect("default has no duplicate handlers");
6874 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6875 assert!(empty_json.get("gitHubToken").is_none());
6876 assert!(empty_json.get("enableSessionTelemetry").is_none());
6877 assert!(empty_json.get("reasoningSummary").is_none());
6878 assert!(empty_json.get("remoteSession").is_none());
6879 assert!(
6880 empty_json
6881 .get("enableOnDemandInstructionDiscovery")
6882 .is_none()
6883 );
6884 assert!(empty_json.get("cloud").is_none());
6885 }
6886
6887 #[test]
6888 fn session_config_into_wire_serializes_named_providers_and_models() {
6889 let cfg = SessionConfig::default()
6890 .with_providers(vec![
6891 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6892 .with_provider_type("openai")
6893 .with_wire_api("responses")
6894 .with_api_key("sk-test"),
6895 ])
6896 .with_models(vec![
6897 ProviderModelConfig::new("gpt-x", "my-openai")
6898 .with_wire_model("gpt-x-2025")
6899 .with_max_output_tokens(2048),
6900 ]);
6901
6902 let (wire, _) = cfg
6903 .into_wire(Some(SessionId::from("sess-providers")))
6904 .expect("no duplicate handlers");
6905 let wire_json = serde_json::to_value(&wire).unwrap();
6906 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6907 assert_eq!(
6908 wire_json["providers"][0]["baseUrl"],
6909 "https://api.example.com/v1"
6910 );
6911 assert_eq!(wire_json["providers"][0]["type"], "openai");
6912 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6913 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6914 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
6915 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
6916 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
6917 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
6918
6919 let (empty_wire, _) = SessionConfig::default()
6920 .into_wire(Some(SessionId::from("empty")))
6921 .expect("default has no duplicate handlers");
6922 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6923 assert!(empty_json.get("providers").is_none());
6924 assert!(empty_json.get("models").is_none());
6925 }
6926
6927 #[test]
6928 fn resume_config_into_wire_serializes_named_providers_and_models() {
6929 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
6930 .with_providers(vec![
6931 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
6932 .with_provider_type("azure")
6933 .with_azure(AzureProviderOptions {
6934 api_version: Some("2024-10-21".to_string()),
6935 }),
6936 ])
6937 .with_models(vec![
6938 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
6939 ]);
6940
6941 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6942 let wire_json = serde_json::to_value(&wire).unwrap();
6943 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
6944 assert_eq!(wire_json["providers"][0]["type"], "azure");
6945 assert_eq!(
6946 wire_json["providers"][0]["azure"]["apiVersion"],
6947 "2024-10-21"
6948 );
6949 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
6950 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
6951 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
6952
6953 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
6954 .into_wire()
6955 .expect("default has no duplicate handlers");
6956 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6957 assert!(empty_json.get("providers").is_none());
6958 assert!(empty_json.get("models").is_none());
6959 }
6960
6961 #[test]
6962 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
6963 use std::path::PathBuf;
6964
6965 let cfg = SessionConfig {
6966 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
6967 disabled_mcp_servers: Some(vec![
6968 "local-files".to_string(),
6969 "remote-github".to_string(),
6970 ]),
6971 large_output: Some(
6972 LargeToolOutputConfig::new()
6973 .with_enabled(true)
6974 .with_max_size_bytes(1024)
6975 .with_output_directory(PathBuf::from("/tmp/large-output")),
6976 ),
6977 ..Default::default()
6978 };
6979
6980 let (wire, _) = cfg
6981 .into_wire(Some(SessionId::from("sess-1")))
6982 .expect("no duplicate handlers");
6983 let wire_json = serde_json::to_value(&wire).unwrap();
6984 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
6985 assert_eq!(
6986 wire_json["disabledMcpServers"],
6987 serde_json::json!(["local-files", "remote-github"])
6988 );
6989 assert_eq!(wire_json["largeOutput"]["enabled"], true);
6990 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
6991 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
6992
6993 let (empty_wire, _) = SessionConfig::default()
6994 .into_wire(Some(SessionId::from("empty")))
6995 .expect("default has no duplicate handlers");
6996 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6997 assert!(empty_json.get("pluginDirectories").is_none());
6998 assert!(empty_json.get("disabledMcpServers").is_none());
6999 assert!(empty_json.get("largeOutput").is_none());
7000 }
7001
7002 #[test]
7003 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
7004 use std::path::PathBuf;
7005
7006 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
7007 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
7008 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
7009 cfg.github_token = Some("ghs_secret".to_string());
7010 cfg.include_sub_agent_streaming_events = Some(true);
7011 cfg.enable_session_telemetry = Some(false);
7012 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
7013 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
7014 cfg.enable_on_demand_instruction_discovery = Some(false);
7015
7016 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7017 let wire_json = serde_json::to_value(&wire).unwrap();
7018 assert_eq!(wire_json["sessionId"], "sess-1");
7019 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
7020 assert_eq!(wire_json["configDir"], "/tmp/cfg");
7021 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
7022 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
7023 assert_eq!(wire_json["enableSessionTelemetry"], false);
7024 assert_eq!(wire_json["reasoningSummary"], "detailed");
7025 assert_eq!(wire_json["remoteSession"], "on");
7026 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
7027
7028 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7030 .into_wire()
7031 .expect("default resume has no duplicate handlers");
7032 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7033 assert!(empty_json.get("reasoningSummary").is_none());
7034 assert!(empty_json.get("remoteSession").is_none());
7035 assert!(
7036 empty_json
7037 .get("enableOnDemandInstructionDiscovery")
7038 .is_none()
7039 );
7040 }
7041
7042 #[test]
7043 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
7044 use std::path::PathBuf;
7045
7046 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
7047 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
7048 cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]);
7049 cfg.large_output = Some(
7050 LargeToolOutputConfig::new()
7051 .with_enabled(false)
7052 .with_max_size_bytes(2048)
7053 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
7054 );
7055
7056 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7057 let wire_json = serde_json::to_value(&wire).unwrap();
7058 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
7059 assert_eq!(
7060 wire_json["disabledMcpServers"],
7061 serde_json::json!(["local-files-r"])
7062 );
7063 assert_eq!(wire_json["largeOutput"]["enabled"], false);
7064 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
7065 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
7066
7067 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7068 .into_wire()
7069 .expect("default resume has no duplicate handlers");
7070 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7071 assert!(empty_json.get("pluginDirectories").is_none());
7072 assert!(empty_json.get("disabledMcpServers").is_none());
7073 assert!(empty_json.get("largeOutput").is_none());
7074 }
7075
7076 #[test]
7077 fn auth_client_id_metadata_url_reaches_create_and_resume_wire_payloads() {
7078 let url = "https://example.com/oauth/client-metadata.json";
7079
7080 let (create_wire, _) = SessionConfig::default()
7081 .with_auth_client_id_metadata_url(url)
7082 .into_wire(None)
7083 .expect("default create has no duplicate handlers");
7084 let create_json = serde_json::to_value(&create_wire).unwrap();
7085 assert_eq!(create_json["authClientIdMetadataUrl"], url);
7086
7087 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-1"))
7088 .with_auth_client_id_metadata_url(url)
7089 .into_wire()
7090 .expect("default resume has no duplicate handlers");
7091 let resume_json = serde_json::to_value(&resume_wire).unwrap();
7092 assert_eq!(resume_json["authClientIdMetadataUrl"], url);
7093
7094 let (empty_create_wire, _) = SessionConfig::default()
7095 .into_wire(None)
7096 .expect("default create has no duplicate handlers");
7097 let empty_create_json = serde_json::to_value(&empty_create_wire).unwrap();
7098 assert!(empty_create_json.get("authClientIdMetadataUrl").is_none());
7099
7100 let (empty_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7101 .into_wire()
7102 .expect("default resume has no duplicate handlers");
7103 let empty_resume_json = serde_json::to_value(&empty_resume_wire).unwrap();
7104 assert!(empty_resume_json.get("authClientIdMetadataUrl").is_none());
7105 }
7106
7107 #[test]
7108 fn session_config_clones_disabled_mcp_servers() {
7109 let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]);
7110 let mut create_clone = create.clone();
7111 create_clone
7112 .disabled_mcp_servers
7113 .as_mut()
7114 .expect("configured disabled MCP servers")
7115 .push("remote-github".to_string());
7116 assert_eq!(
7117 create.disabled_mcp_servers.as_deref(),
7118 Some(&["local-files".to_string()][..])
7119 );
7120
7121 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
7122 .with_disabled_mcp_servers(["local-files"]);
7123 let mut resume_clone = resume.clone();
7124 resume_clone
7125 .disabled_mcp_servers
7126 .as_mut()
7127 .expect("configured disabled MCP servers")
7128 .push("remote-github".to_string());
7129 assert_eq!(
7130 resume.disabled_mcp_servers.as_deref(),
7131 Some(&["local-files".to_string()][..])
7132 );
7133 }
7134
7135 #[test]
7136 fn session_config_builder_composes() {
7137 use indexmap::IndexMap;
7138
7139 let cfg = SessionConfig::default()
7140 .with_session_id(SessionId::from("sess-1"))
7141 .with_model("claude-sonnet-4")
7142 .with_client_name("test-app")
7143 .with_reasoning_effort("medium")
7144 .with_reasoning_summary(ReasoningSummary::Concise)
7145 .with_context_tier("long_context")
7146 .with_streaming(true)
7147 .with_tools([Tool::new("greet")])
7148 .with_available_tools(["bash", "view"])
7149 .with_excluded_tools(["dangerous"])
7150 .with_mcp_servers(IndexMap::new())
7151 .with_mcp_oauth_token_storage("persistent")
7152 .with_enable_config_discovery(true)
7153 .with_enable_on_demand_instruction_discovery(true)
7154 .with_skill_directories([PathBuf::from("/tmp/skills")])
7155 .with_disabled_skills(["broken-skill"])
7156 .with_disabled_mcp_servers(["local-files"])
7157 .with_agent("researcher")
7158 .with_config_directory(PathBuf::from("/tmp/config"))
7159 .with_working_directory(PathBuf::from("/tmp/work"))
7160 .with_additional_directories([PathBuf::from("/tmp/shared")])
7161 .with_github_token("ghp_test")
7162 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7163 .with_enable_session_telemetry(false)
7164 .with_include_sub_agent_streaming_events(false)
7165 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
7166
7167 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
7168 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
7169 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
7170 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
7171 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
7172 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
7173 assert_eq!(cfg.streaming, Some(true));
7174 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
7175 assert_eq!(
7176 cfg.available_tools.as_deref(),
7177 Some(&["bash".to_string(), "view".to_string()][..])
7178 );
7179 assert_eq!(
7180 cfg.excluded_tools.as_deref(),
7181 Some(&["dangerous".to_string()][..])
7182 );
7183 assert!(cfg.mcp_servers.is_some());
7184 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
7185 assert_eq!(cfg.enable_config_discovery, Some(true));
7186 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
7187 assert_eq!(
7188 cfg.skill_directories.as_deref(),
7189 Some(&[PathBuf::from("/tmp/skills")][..])
7190 );
7191 assert_eq!(
7192 cfg.disabled_skills.as_deref(),
7193 Some(&["broken-skill".to_string()][..])
7194 );
7195 assert_eq!(
7196 cfg.disabled_mcp_servers.as_deref(),
7197 Some(&["local-files".to_string()][..])
7198 );
7199 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
7200 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
7201 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
7202 assert_eq!(
7203 cfg.additional_directories.as_deref(),
7204 Some(&[PathBuf::from("/tmp/shared")][..])
7205 );
7206 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
7207 assert_eq!(
7208 cfg.capi,
7209 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7210 );
7211 assert_eq!(cfg.enable_session_telemetry, Some(false));
7212 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
7213 assert_eq!(
7214 cfg.extension_info,
7215 Some(ExtensionInfo::new("github-app", "counter"))
7216 );
7217 }
7218
7219 #[test]
7220 fn resume_session_config_builder_composes() {
7221 use indexmap::IndexMap;
7222
7223 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
7224 .with_client_name("test-app")
7225 .with_reasoning_summary(ReasoningSummary::None)
7226 .with_context_tier("default")
7227 .with_streaming(true)
7228 .with_tools([Tool::new("greet")])
7229 .with_available_tools(["bash", "view"])
7230 .with_excluded_tools(["dangerous"])
7231 .with_mcp_servers(IndexMap::new())
7232 .with_mcp_oauth_token_storage("persistent")
7233 .with_enable_config_discovery(true)
7234 .with_enable_on_demand_instruction_discovery(false)
7235 .with_skill_directories([PathBuf::from("/tmp/skills")])
7236 .with_disabled_skills(["broken-skill"])
7237 .with_disabled_mcp_servers(["local-files"])
7238 .with_agent("researcher")
7239 .with_config_directory(PathBuf::from("/tmp/config"))
7240 .with_working_directory(PathBuf::from("/tmp/work"))
7241 .with_additional_directories([PathBuf::from("/tmp/shared")])
7242 .with_github_token("ghp_test")
7243 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7244 .with_enable_session_telemetry(false)
7245 .with_include_sub_agent_streaming_events(true)
7246 .with_suppress_resume_event(true)
7247 .with_continue_pending_work(true)
7248 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
7249
7250 assert_eq!(cfg.session_id.as_str(), "sess-2");
7251 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
7252 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
7253 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
7254 assert_eq!(cfg.streaming, Some(true));
7255 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
7256 assert_eq!(
7257 cfg.available_tools.as_deref(),
7258 Some(&["bash".to_string(), "view".to_string()][..])
7259 );
7260 assert_eq!(
7261 cfg.excluded_tools.as_deref(),
7262 Some(&["dangerous".to_string()][..])
7263 );
7264 assert!(cfg.mcp_servers.is_some());
7265 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
7266 assert_eq!(cfg.enable_config_discovery, Some(true));
7267 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
7268 assert_eq!(
7269 cfg.skill_directories.as_deref(),
7270 Some(&[PathBuf::from("/tmp/skills")][..])
7271 );
7272 assert_eq!(
7273 cfg.disabled_skills.as_deref(),
7274 Some(&["broken-skill".to_string()][..])
7275 );
7276 assert_eq!(
7277 cfg.disabled_mcp_servers.as_deref(),
7278 Some(&["local-files".to_string()][..])
7279 );
7280 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
7281 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
7282 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
7283 assert_eq!(
7284 cfg.additional_directories.as_deref(),
7285 Some(&[PathBuf::from("/tmp/shared")][..])
7286 );
7287 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
7288 assert_eq!(
7289 cfg.capi,
7290 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7291 );
7292 assert_eq!(cfg.enable_session_telemetry, Some(false));
7293 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
7294 assert_eq!(cfg.suppress_resume_event, Some(true));
7295 assert_eq!(cfg.continue_pending_work, Some(true));
7296 assert_eq!(
7297 cfg.extension_info,
7298 Some(ExtensionInfo::new("github-app", "counter"))
7299 );
7300 }
7301
7302 #[test]
7306 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
7307 let cfg =
7308 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
7309 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7310 let json = serde_json::to_value(&wire).unwrap();
7311 assert_eq!(json["continuePendingWork"], true);
7312
7313 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7315 .into_wire()
7316 .expect("no duplicate handlers");
7317 let json = serde_json::to_value(&wire).unwrap();
7318 assert!(json.get("continuePendingWork").is_none());
7319 }
7320
7321 #[test]
7322 fn session_configs_serialize_additional_directories() {
7323 let create = SessionConfig::default().with_additional_directories([
7324 PathBuf::from("/tmp/shared"),
7325 PathBuf::from("/tmp/generated"),
7326 ]);
7327 let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
7328 let create_json = serde_json::to_value(&create_wire).unwrap();
7329 assert_eq!(
7330 create_json["additionalDirectories"],
7331 serde_json::json!(["/tmp/shared", "/tmp/generated"])
7332 );
7333
7334 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
7335 .with_additional_directories([PathBuf::from("/tmp/resumed")]);
7336 let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
7337 let resume_json = serde_json::to_value(&resume_wire).unwrap();
7338 assert_eq!(
7339 resume_json["additionalDirectories"],
7340 serde_json::json!(["/tmp/resumed"])
7341 );
7342 }
7343
7344 #[test]
7348 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
7349 let cfg =
7350 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
7351 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7352 let json = serde_json::to_value(&wire).unwrap();
7353 assert_eq!(json["disableResume"], true);
7354 assert!(json.get("suppressResumeEvent").is_none());
7355 }
7356
7357 #[test]
7360 fn session_config_serializes_instruction_directories_to_camel_case() {
7361 let cfg =
7362 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
7363 let (wire, _) = cfg
7364 .into_wire(Some(SessionId::from("instr-on")))
7365 .expect("no duplicate handlers");
7366 let json = serde_json::to_value(&wire).unwrap();
7367 assert_eq!(
7368 json["instructionDirectories"],
7369 serde_json::json!(["/tmp/instr"])
7370 );
7371
7372 let (wire, _) = SessionConfig::default()
7374 .into_wire(Some(SessionId::from("instr-off")))
7375 .expect("no duplicate handlers");
7376 let json = serde_json::to_value(&wire).unwrap();
7377 assert!(json.get("instructionDirectories").is_none());
7378 }
7379
7380 #[test]
7383 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
7384 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
7385 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
7386 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7387 let json = serde_json::to_value(&wire).unwrap();
7388 assert_eq!(
7389 json["instructionDirectories"],
7390 serde_json::json!(["/tmp/instr"])
7391 );
7392
7393 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7394 .into_wire()
7395 .expect("no duplicate handlers");
7396 let json = serde_json::to_value(&wire).unwrap();
7397 assert!(json.get("instructionDirectories").is_none());
7398 }
7399
7400 #[test]
7401 fn custom_agent_config_builder_composes() {
7402 use indexmap::IndexMap;
7403
7404 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
7405 .with_display_name("Research Assistant")
7406 .with_description("Investigates technical questions.")
7407 .with_tools(["bash", "view"])
7408 .with_mcp_servers(IndexMap::new())
7409 .with_infer(true)
7410 .with_skills(["rust-coding-skill"]);
7411
7412 assert_eq!(cfg.name, "researcher");
7413 assert_eq!(cfg.prompt, "You are a research assistant.");
7414 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
7415 assert_eq!(
7416 cfg.description.as_deref(),
7417 Some("Investigates technical questions.")
7418 );
7419 assert_eq!(
7420 cfg.tools.as_deref(),
7421 Some(&["bash".to_string(), "view".to_string()][..])
7422 );
7423 assert!(cfg.mcp_servers.is_some());
7424 assert_eq!(cfg.infer, Some(true));
7425 assert_eq!(
7426 cfg.skills.as_deref(),
7427 Some(&["rust-coding-skill".to_string()][..])
7428 );
7429 }
7430
7431 #[test]
7432 fn mcp_servers_serialize_in_insertion_order() {
7433 use indexmap::IndexMap;
7434
7435 let order = [
7441 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
7442 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
7443 ];
7444 let mut servers = IndexMap::new();
7445 for name in order {
7446 servers.insert(
7447 name.to_string(),
7448 McpServerConfig::Stdio(McpStdioServerConfig {
7449 command: "run".to_string(),
7450 ..Default::default()
7451 }),
7452 );
7453 }
7454
7455 let (wire, _runtime) = SessionConfig::default()
7456 .with_mcp_servers(servers)
7457 .into_wire(None)
7458 .expect("into_wire should succeed");
7459 let json = serde_json::to_string(&wire).expect("serialize wire");
7460
7461 let positions: Vec<usize> = order
7462 .iter()
7463 .map(|name| {
7464 json.find(&format!("\"{name}\""))
7465 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
7466 })
7467 .collect();
7468 let mut ascending = positions.clone();
7469 ascending.sort_unstable();
7470 assert_eq!(
7471 positions, ascending,
7472 "mcp server keys must serialize in insertion order: {json}"
7473 );
7474 }
7475
7476 #[test]
7477 fn infinite_session_config_builder_composes() {
7478 let cfg = InfiniteSessionConfig::new()
7479 .with_enabled(true)
7480 .with_background_compaction_threshold(0.75)
7481 .with_buffer_exhaustion_threshold(0.92);
7482
7483 assert_eq!(cfg.enabled, Some(true));
7484 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
7485 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
7486 }
7487
7488 #[test]
7489 fn provider_config_builder_composes() {
7490 use std::collections::HashMap;
7491
7492 let mut headers = HashMap::new();
7493 headers.insert("X-Custom".to_string(), "value".to_string());
7494
7495 let cfg = ProviderConfig::new("https://api.example.com")
7496 .with_provider_type("openai")
7497 .with_wire_api("completions")
7498 .with_transport("websockets")
7499 .with_api_key("sk-test")
7500 .with_bearer_token("bearer-test")
7501 .with_headers(headers)
7502 .with_model_id("gpt-4")
7503 .with_wire_model("azure-gpt-4-deployment")
7504 .with_max_prompt_tokens(8192)
7505 .with_max_output_tokens(2048);
7506
7507 assert_eq!(cfg.base_url, "https://api.example.com");
7508 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
7509 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
7510 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
7511 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
7512 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
7513 assert_eq!(
7514 cfg.headers
7515 .as_ref()
7516 .and_then(|h| h.get("X-Custom"))
7517 .map(String::as_str),
7518 Some("value"),
7519 );
7520 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
7521 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
7522 assert_eq!(cfg.max_prompt_tokens, Some(8192));
7523 assert_eq!(cfg.max_output_tokens, Some(2048));
7524
7525 let wire = serde_json::to_value(&cfg).unwrap();
7527 assert_eq!(wire["modelId"], "gpt-4");
7528 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
7529 assert_eq!(wire["maxPromptTokens"], 8192);
7530 assert_eq!(wire["maxOutputTokens"], 2048);
7531
7532 let unset = ProviderConfig::new("https://api.example.com");
7533 let wire_unset = serde_json::to_value(&unset).unwrap();
7534 assert!(wire_unset.get("modelId").is_none());
7535 assert!(wire_unset.get("wireModel").is_none());
7536 assert!(wire_unset.get("maxPromptTokens").is_none());
7537 assert!(wire_unset.get("maxOutputTokens").is_none());
7538 }
7539
7540 #[test]
7541 fn capi_session_options_builder_composes_and_serializes() {
7542 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
7543
7544 assert_eq!(cfg.enable_web_socket_responses, Some(false));
7545
7546 let wire = serde_json::to_value(&cfg).unwrap();
7547 assert_eq!(
7548 wire,
7549 serde_json::json!({ "enableWebSocketResponses": false })
7550 );
7551
7552 let unset = CapiSessionOptions::new();
7553 let wire_unset = serde_json::to_value(&unset).unwrap();
7554 assert!(wire_unset.get("enableWebSocketResponses").is_none());
7555 assert!(wire_unset.get("autoTier").is_none());
7556 assert_eq!(wire_unset, json!({}));
7557 }
7558
7559 #[test]
7560 fn capi_auto_tier_canonical_values_round_trip_and_forward() {
7561 for (tier, value) in [
7562 (AutoTier::Efficiency, "efficiency"),
7563 (AutoTier::Balance, "balance"),
7564 (AutoTier::Intelligence, "intelligence"),
7565 ] {
7566 let exported: crate::AutoTier = tier.clone();
7567 let capi = CapiSessionOptions::new().with_auto_tier(exported);
7568 assert_eq!(capi.auto_tier, Some(tier));
7569 assert_eq!(
7570 serde_json::to_value(&capi).unwrap(),
7571 json!({"autoTier": value})
7572 );
7573 assert_eq!(
7574 serde_json::from_value::<CapiSessionOptions>(json!({"autoTier": value})).unwrap(),
7575 capi
7576 );
7577
7578 let capi = capi.with_enable_web_socket_responses(false);
7579 let expected = json!({"autoTier": value, "enableWebSocketResponses": false});
7580 let (create, _) = SessionConfig::default()
7581 .with_model("auto")
7582 .with_capi(capi.clone())
7583 .into_wire(Some(SessionId::from("capi-create")))
7584 .unwrap();
7585 assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected);
7586
7587 let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7588 .with_capi(capi)
7589 .into_wire()
7590 .unwrap();
7591 assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected);
7592 }
7593 }
7594
7595 #[test]
7596 fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() {
7597 for value in ["balanced", "Balance", "unknown"] {
7598 assert_eq!(
7599 serde_json::from_value::<AutoTier>(json!(value)).unwrap(),
7600 AutoTier::Unknown
7601 );
7602 }
7603 let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap();
7604 assert_eq!(capi.auto_tier, None);
7605 }
7606
7607 #[test]
7608 fn session_config_with_capi_serializes() {
7609 let (wire, _) = SessionConfig::default()
7610 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7611 .into_wire(Some(SessionId::from("capi-create")))
7612 .expect("no duplicate handlers");
7613 let json = serde_json::to_value(&wire).unwrap();
7614 assert_eq!(
7615 json["capi"],
7616 serde_json::json!({ "enableWebSocketResponses": false })
7617 );
7618
7619 let (empty_wire, _) = SessionConfig::default()
7620 .into_wire(Some(SessionId::from("capi-create-unset")))
7621 .expect("no duplicate handlers");
7622 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7623 assert!(empty_json.get("capi").is_none());
7624 }
7625
7626 #[test]
7627 fn resume_session_config_with_capi_serializes() {
7628 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7629 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7630 .into_wire()
7631 .expect("no duplicate handlers");
7632 let json = serde_json::to_value(&wire).unwrap();
7633 assert_eq!(
7634 json["capi"],
7635 serde_json::json!({ "enableWebSocketResponses": false })
7636 );
7637
7638 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
7639 .into_wire()
7640 .expect("no duplicate handlers");
7641 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7642 assert!(empty_json.get("capi").is_none());
7643 }
7644
7645 #[test]
7646 fn system_message_config_builder_composes() {
7647 use std::collections::HashMap;
7648
7649 let cfg = SystemMessageConfig::new()
7650 .with_mode("replace")
7651 .with_content("Custom system message.")
7652 .with_sections(HashMap::new());
7653
7654 assert_eq!(cfg.mode.as_deref(), Some("replace"));
7655 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
7656 assert!(cfg.sections.is_some());
7657 }
7658
7659 #[test]
7660 fn delivery_mode_serializes_to_kebab_case_strings() {
7661 assert_eq!(
7662 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
7663 "\"enqueue\""
7664 );
7665 assert_eq!(
7666 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
7667 "\"immediate\""
7668 );
7669 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
7670 assert_eq!(parsed, DeliveryMode::Immediate);
7671 }
7672
7673 #[test]
7674 fn agent_mode_serializes_to_kebab_case_strings() {
7675 assert_eq!(
7676 serde_json::to_string(&AgentMode::Interactive).unwrap(),
7677 "\"interactive\""
7678 );
7679 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
7680 assert_eq!(
7681 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
7682 "\"autopilot\""
7683 );
7684 assert_eq!(
7685 serde_json::to_string(&AgentMode::Shell).unwrap(),
7686 "\"shell\""
7687 );
7688 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
7689 assert_eq!(parsed, AgentMode::Plan);
7690 }
7691
7692 #[test]
7693 fn connection_state_distinguishes_variants() {
7694 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
7697 }
7698
7699 #[test]
7705 fn session_event_round_trips_agent_id_on_envelope() {
7706 let wire = json!({
7707 "id": "evt-1",
7708 "timestamp": "2026-04-30T12:00:00Z",
7709 "parentId": null,
7710 "agentId": "sub-agent-42",
7711 "type": "assistant.message",
7712 "data": { "message": "hi" }
7713 });
7714
7715 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
7716 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7717
7718 let roundtripped = serde_json::to_value(&event).unwrap();
7720 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7721
7722 let main_agent_event: SessionEvent = serde_json::from_value(json!({
7724 "id": "evt-2",
7725 "timestamp": "2026-04-30T12:00:01Z",
7726 "parentId": null,
7727 "type": "session.idle",
7728 "data": {}
7729 }))
7730 .unwrap();
7731 assert!(main_agent_event.agent_id.is_none());
7732 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
7733 assert!(roundtripped.get("agentId").is_none());
7734 }
7735
7736 #[test]
7738 fn typed_session_event_round_trips_agent_id_on_envelope() {
7739 let wire = json!({
7740 "id": "evt-1",
7741 "timestamp": "2026-04-30T12:00:00Z",
7742 "parentId": null,
7743 "agentId": "sub-agent-42",
7744 "type": "session.idle",
7745 "data": {}
7746 });
7747
7748 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
7749 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7750
7751 let roundtripped = serde_json::to_value(&event).unwrap();
7752 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7753 }
7754
7755 #[test]
7756 fn connection_state_variants_compile() {
7757 let _ = ConnectionState::Disconnected;
7761 let _ = ConnectionState::Connecting;
7762 let _ = ConnectionState::Connected;
7763 let _ = ConnectionState::Error;
7764 }
7765
7766 #[test]
7767 fn deserializes_runtime_attachment_variants() {
7768 let attachments: Vec<Attachment> = serde_json::from_value(json!([
7769 {
7770 "type": "file",
7771 "path": "/tmp/file.rs",
7772 "displayName": "file.rs",
7773 "lineRange": { "start": 7, "end": 12 }
7774 },
7775 {
7776 "type": "directory",
7777 "path": "/tmp/project",
7778 "displayName": "project"
7779 },
7780 {
7781 "type": "selection",
7782 "filePath": "/tmp/lib.rs",
7783 "displayName": "lib.rs",
7784 "text": "fn main() {}",
7785 "selection": {
7786 "start": { "line": 1, "character": 2 },
7787 "end": { "line": 3, "character": 4 }
7788 }
7789 },
7790 {
7791 "type": "blob",
7792 "data": "Zm9v",
7793 "mimeType": "image/png",
7794 "displayName": "image.png"
7795 },
7796 {
7797 "type": "github_reference",
7798 "number": 42,
7799 "title": "Fix rendering",
7800 "referenceType": "issue",
7801 "state": "open",
7802 "url": "https://github.com/example/repo/issues/42"
7803 }
7804 ]))
7805 .expect("attachments should deserialize");
7806
7807 assert_eq!(attachments.len(), 5);
7808 assert!(matches!(
7809 &attachments[0],
7810 Attachment::File {
7811 path,
7812 display_name,
7813 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7814 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7815 ));
7816 assert!(matches!(
7817 &attachments[1],
7818 Attachment::Directory { path, display_name }
7819 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7820 ));
7821 assert!(matches!(
7822 &attachments[2],
7823 Attachment::Selection {
7824 file_path,
7825 display_name,
7826 selection:
7827 AttachmentSelectionRange {
7828 start: AttachmentSelectionPosition { line: 1, character: 2 },
7829 end: AttachmentSelectionPosition { line: 3, character: 4 },
7830 },
7831 ..
7832 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
7833 ));
7834 assert!(matches!(
7835 &attachments[3],
7836 Attachment::Blob {
7837 data,
7838 mime_type,
7839 display_name,
7840 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
7841 ));
7842 assert!(matches!(
7843 &attachments[4],
7844 Attachment::GitHubReference {
7845 number: 42,
7846 title,
7847 reference_type: GitHubReferenceType::Issue,
7848 state,
7849 url,
7850 } if title == "Fix rendering"
7851 && state == "open"
7852 && url == "https://github.com/example/repo/issues/42"
7853 ));
7854 }
7855
7856 #[test]
7857 fn ensures_display_names_for_variants_that_support_them() {
7858 let mut attachments = vec![
7859 Attachment::File {
7860 path: PathBuf::from("/tmp/file.rs"),
7861 display_name: None,
7862 line_range: None,
7863 },
7864 Attachment::Selection {
7865 file_path: PathBuf::from("/tmp/src/lib.rs"),
7866 display_name: None,
7867 text: "fn main() {}".to_string(),
7868 selection: AttachmentSelectionRange {
7869 start: AttachmentSelectionPosition {
7870 line: 0,
7871 character: 0,
7872 },
7873 end: AttachmentSelectionPosition {
7874 line: 0,
7875 character: 10,
7876 },
7877 },
7878 },
7879 Attachment::Blob {
7880 data: "Zm9v".to_string(),
7881 mime_type: "image/png".to_string(),
7882 display_name: None,
7883 },
7884 Attachment::GitHubReference {
7885 number: 7,
7886 title: "Track regressions".to_string(),
7887 reference_type: GitHubReferenceType::Issue,
7888 state: "open".to_string(),
7889 url: "https://example.com/issues/7".to_string(),
7890 },
7891 ];
7892
7893 ensure_attachment_display_names(&mut attachments);
7894
7895 assert_eq!(attachments[0].display_name(), Some("file.rs"));
7896 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
7897 assert_eq!(attachments[2].display_name(), Some("attachment"));
7898 assert_eq!(attachments[3].display_name(), None);
7899 assert_eq!(
7900 attachments[3].label(),
7901 Some("Track regressions".to_string())
7902 );
7903 }
7904
7905 #[test]
7906 fn github_anchored_attachment_variants_round_trip() {
7907 let cases = vec![
7908 (
7909 "github_commit",
7910 json!({
7911 "type": "github_commit",
7912 "message": "Fix the thing",
7913 "oid": "abc123",
7914 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
7915 "url": "https://github.com/octocat/repo/commit/abc123"
7916 }),
7917 ),
7918 (
7919 "github_release",
7920 json!({
7921 "type": "github_release",
7922 "name": "v1.2.3",
7923 "repo": { "name": "repo", "owner": "octocat" },
7924 "tagName": "v1.2.3",
7925 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
7926 }),
7927 ),
7928 (
7929 "github_actions_job",
7930 json!({
7931 "type": "github_actions_job",
7932 "conclusion": "failure",
7933 "jobId": 99,
7934 "jobName": "build",
7935 "repo": { "name": "repo", "owner": "octocat" },
7936 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
7937 "workflowName": "CI"
7938 }),
7939 ),
7940 (
7941 "github_repository",
7942 json!({
7943 "type": "github_repository",
7944 "description": "An example repository",
7945 "ref": "main",
7946 "repo": { "name": "repo", "owner": "octocat" },
7947 "url": "https://github.com/octocat/repo"
7948 }),
7949 ),
7950 (
7951 "github_file_diff",
7952 json!({
7953 "type": "github_file_diff",
7954 "base": {
7955 "path": "src/lib.rs",
7956 "ref": "main",
7957 "repo": { "name": "repo", "owner": "octocat" }
7958 },
7959 "head": {
7960 "path": "src/lib.rs",
7961 "ref": "feature",
7962 "repo": { "name": "repo", "owner": "octocat" }
7963 },
7964 "url": "https://github.com/octocat/repo/compare/main...feature"
7965 }),
7966 ),
7967 (
7968 "github_tree_comparison",
7969 json!({
7970 "type": "github_tree_comparison",
7971 "base": {
7972 "repo": { "name": "repo", "owner": "octocat" },
7973 "revision": "main"
7974 },
7975 "head": {
7976 "repo": { "name": "repo", "owner": "octocat" },
7977 "revision": "feature"
7978 },
7979 "url": "https://github.com/octocat/repo/compare/main...feature"
7980 }),
7981 ),
7982 (
7983 "github_url",
7984 json!({
7985 "type": "github_url",
7986 "url": "https://github.com/octocat/repo/wiki"
7987 }),
7988 ),
7989 (
7990 "github_file",
7991 json!({
7992 "type": "github_file",
7993 "path": "src/main.rs",
7994 "ref": "main",
7995 "repo": { "name": "repo", "owner": "octocat" },
7996 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
7997 }),
7998 ),
7999 (
8000 "github_snippet",
8001 json!({
8002 "type": "github_snippet",
8003 "lineRange": { "start": 10, "end": 20 },
8004 "path": "src/main.rs",
8005 "ref": "main",
8006 "repo": { "name": "repo", "owner": "octocat" },
8007 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
8008 }),
8009 ),
8010 ];
8011
8012 for (expected_type, input) in cases {
8013 let attachment: Attachment = serde_json::from_value(input.clone())
8014 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
8015
8016 let serialized_string = serde_json::to_string(&attachment)
8021 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
8022
8023 assert_eq!(
8025 serialized_string.matches("\"type\":").count(),
8026 1,
8027 "{expected_type} must serialize a single `type` key"
8028 );
8029
8030 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
8031 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
8032 assert_eq!(
8033 serialized.get("type").and_then(|value| value.as_str()),
8034 Some(expected_type),
8035 "{expected_type} must serialize the correct discriminator"
8036 );
8037
8038 assert_eq!(
8040 serialized, input,
8041 "{expected_type} should round-trip without data loss"
8042 );
8043 let reparsed: Attachment = serde_json::from_value(serialized)
8044 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
8045 assert_eq!(
8046 reparsed, attachment,
8047 "{expected_type} should re-deserialize to the same value"
8048 );
8049 }
8050 }
8051}
8052
8053#[cfg(test)]
8054mod permission_builder_tests {
8055 use std::sync::Arc;
8056
8057 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
8058 use crate::permission;
8059 use crate::types::{
8060 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
8061 SessionId,
8062 };
8063
8064 fn data() -> PermissionRequestData {
8065 PermissionRequestData {
8066 extra: serde_json::json!({"tool": "shell"}),
8067 ..Default::default()
8068 }
8069 }
8070
8071 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
8074 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
8075 }
8076
8077 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
8078 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
8079 }
8080
8081 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
8082 handler
8083 .handle(SessionId::from("s1"), RequestId::new("1"), data())
8084 .await
8085 }
8086
8087 #[tokio::test]
8088 async fn approve_all_with_handler_present_approves() {
8089 let cfg = SessionConfig::default()
8090 .with_permission_handler(Arc::new(ApproveAllHandler))
8091 .approve_all_permissions();
8092 let h = resolve_create(cfg).expect("policy + handler yields handler");
8093 assert!(matches!(
8094 dispatch(&h).await,
8095 PermissionResult::Decision {
8096 decision: PermissionDecision::ApproveOnce(_),
8097 ..
8098 }
8099 ));
8100 }
8101
8102 #[tokio::test]
8103 async fn approve_all_standalone_produces_handler() {
8104 let cfg = SessionConfig::default().approve_all_permissions();
8105 let h = resolve_create(cfg).expect("policy alone yields handler");
8106 assert!(matches!(
8107 dispatch(&h).await,
8108 PermissionResult::Decision {
8109 decision: PermissionDecision::ApproveOnce(_),
8110 ..
8111 }
8112 ));
8113 }
8114
8115 #[tokio::test]
8118 async fn approve_all_is_order_independent() {
8119 let a = SessionConfig::default()
8120 .with_permission_handler(Arc::new(ApproveAllHandler))
8121 .approve_all_permissions();
8122 let b = SessionConfig::default()
8123 .approve_all_permissions()
8124 .with_permission_handler(Arc::new(ApproveAllHandler));
8125 let ha = resolve_create(a).unwrap();
8126 let hb = resolve_create(b).unwrap();
8127 assert!(matches!(
8128 dispatch(&ha).await,
8129 PermissionResult::Decision {
8130 decision: PermissionDecision::ApproveOnce(_),
8131 ..
8132 }
8133 ));
8134 assert!(matches!(
8135 dispatch(&hb).await,
8136 PermissionResult::Decision {
8137 decision: PermissionDecision::ApproveOnce(_),
8138 ..
8139 }
8140 ));
8141 }
8142
8143 #[tokio::test]
8144 async fn deny_all_is_order_independent() {
8145 let a = SessionConfig::default()
8146 .with_permission_handler(Arc::new(ApproveAllHandler))
8147 .deny_all_permissions();
8148 let b = SessionConfig::default()
8149 .deny_all_permissions()
8150 .with_permission_handler(Arc::new(ApproveAllHandler));
8151 let ha = resolve_create(a).unwrap();
8152 let hb = resolve_create(b).unwrap();
8153 assert!(matches!(
8154 dispatch(&ha).await,
8155 PermissionResult::Decision {
8156 decision: PermissionDecision::Reject(_),
8157 ..
8158 }
8159 ));
8160 assert!(matches!(
8161 dispatch(&hb).await,
8162 PermissionResult::Decision {
8163 decision: PermissionDecision::Reject(_),
8164 ..
8165 }
8166 ));
8167 }
8168
8169 #[tokio::test]
8170 async fn approve_permissions_if_consults_predicate() {
8171 let cfg = SessionConfig::default().approve_permissions_if(|d| {
8172 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
8173 });
8174 let h = resolve_create(cfg).unwrap();
8175 assert!(matches!(
8176 dispatch(&h).await,
8177 PermissionResult::Decision {
8178 decision: PermissionDecision::Reject(_),
8179 ..
8180 }
8181 ));
8182 }
8183
8184 #[tokio::test]
8185 async fn approve_permissions_if_is_order_independent() {
8186 let predicate = |d: &PermissionRequestData| {
8187 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
8188 };
8189 let a = SessionConfig::default()
8190 .with_permission_handler(Arc::new(ApproveAllHandler))
8191 .approve_permissions_if(predicate);
8192 let b = SessionConfig::default()
8193 .approve_permissions_if(predicate)
8194 .with_permission_handler(Arc::new(ApproveAllHandler));
8195 let ha = resolve_create(a).unwrap();
8196 let hb = resolve_create(b).unwrap();
8197 assert!(matches!(
8198 dispatch(&ha).await,
8199 PermissionResult::Decision {
8200 decision: PermissionDecision::Reject(_),
8201 ..
8202 }
8203 ));
8204 assert!(matches!(
8205 dispatch(&hb).await,
8206 PermissionResult::Decision {
8207 decision: PermissionDecision::Reject(_),
8208 ..
8209 }
8210 ));
8211 }
8212
8213 #[tokio::test]
8214 async fn resume_session_config_approve_all_works() {
8215 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
8216 .with_permission_handler(Arc::new(ApproveAllHandler))
8217 .approve_all_permissions();
8218 let h = resolve_resume(cfg).unwrap();
8219 assert!(matches!(
8220 dispatch(&h).await,
8221 PermissionResult::Decision {
8222 decision: PermissionDecision::ApproveOnce(_),
8223 ..
8224 }
8225 ));
8226 }
8227
8228 #[tokio::test]
8229 async fn resume_session_config_approve_all_is_order_independent() {
8230 let a = ResumeSessionConfig::new(SessionId::from("s1"))
8231 .with_permission_handler(Arc::new(ApproveAllHandler))
8232 .approve_all_permissions();
8233 let b = ResumeSessionConfig::new(SessionId::from("s1"))
8234 .approve_all_permissions()
8235 .with_permission_handler(Arc::new(ApproveAllHandler));
8236 let ha = resolve_resume(a).unwrap();
8237 let hb = resolve_resume(b).unwrap();
8238 assert!(matches!(
8239 dispatch(&ha).await,
8240 PermissionResult::Decision {
8241 decision: PermissionDecision::ApproveOnce(_),
8242 ..
8243 }
8244 ));
8245 assert!(matches!(
8246 dispatch(&hb).await,
8247 PermissionResult::Decision {
8248 decision: PermissionDecision::ApproveOnce(_),
8249 ..
8250 }
8251 ));
8252 }
8253
8254 #[test]
8255 fn session_config_enable_experimental_mode_serializes_when_set() {
8256 let cfg = SessionConfig::default().with_enable_experimental_mode(false);
8257 assert_eq!(cfg.enable_experimental_mode, Some(false));
8258
8259 let (wire, _runtime) = cfg
8260 .into_wire(Some(SessionId::from("experimental-mode")))
8261 .expect("enable_experimental_mode config has no duplicate handlers");
8262 assert_eq!(wire.is_experimental_mode, Some(false));
8263
8264 let json = serde_json::to_value(&wire).unwrap();
8265 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
8266 }
8267
8268 #[test]
8269 fn session_config_enable_experimental_mode_omitted_when_none() {
8270 let cfg = SessionConfig::default();
8271 assert_eq!(cfg.enable_experimental_mode, None);
8272
8273 let (wire, _runtime) = cfg
8274 .into_wire(Some(SessionId::from("no-experimental-mode")))
8275 .expect("default config has no duplicate handlers");
8276 assert_eq!(wire.is_experimental_mode, None);
8277
8278 let json = serde_json::to_value(&wire).unwrap();
8279 assert!(json.get("isExperimentalMode").is_none());
8280 }
8281
8282 #[test]
8283 fn resume_session_config_enable_experimental_mode_serializes_when_set() {
8284 let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
8285 .with_enable_experimental_mode(false);
8286 assert_eq!(cfg.enable_experimental_mode, Some(false));
8287
8288 let (wire, _runtime) = cfg
8289 .into_wire()
8290 .expect("resume enable_experimental_mode config has no duplicate handlers");
8291 assert_eq!(wire.is_experimental_mode, Some(false));
8292
8293 let json = serde_json::to_value(&wire).unwrap();
8294 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
8295 }
8296
8297 #[test]
8298 fn resume_session_config_enable_experimental_mode_omitted_when_none() {
8299 let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
8300 assert_eq!(cfg.enable_experimental_mode, None);
8301
8302 let (wire, _runtime) = cfg
8303 .into_wire()
8304 .expect("default resume config has no duplicate handlers");
8305 assert_eq!(wire.is_experimental_mode, None);
8306
8307 let json = serde_json::to_value(&wire).unwrap();
8308 assert!(json.get("isExperimentalMode").is_none());
8309 }
8310}
8311
8312#[cfg(test)]
8313mod is_terminal_tests {
8314 use super::Tool;
8315
8316 #[test]
8317 fn is_terminal_serializes_as_camel_case_when_set() {
8318 let tool = Tool {
8319 name: "clear_context".to_owned(),
8320 is_terminal: true,
8321 ..Default::default()
8322 };
8323 let value = serde_json::to_value(&tool).expect("tool serializes");
8324 assert_eq!(
8325 value.get("isTerminal"),
8326 Some(&serde_json::Value::Bool(true))
8327 );
8328 }
8329
8330 #[test]
8331 fn is_terminal_is_omitted_when_false() {
8332 let tool = Tool {
8333 name: "plain".to_owned(),
8334 ..Default::default()
8335 };
8336 let value = serde_json::to_value(&tool).expect("tool serializes");
8337 assert!(value.get("isTerminal").is_none());
8338 }
8339
8340 #[test]
8343 fn is_terminal_appears_in_debug_output() {
8344 let terminal = Tool {
8345 name: "clear_context".to_owned(),
8346 is_terminal: true,
8347 ..Default::default()
8348 };
8349 assert!(format!("{terminal:?}").contains("is_terminal: true"));
8350
8351 let plain = Tool {
8352 name: "plain".to_owned(),
8353 ..Default::default()
8354 };
8355 assert!(format!("{plain:?}").contains("is_terminal: false"));
8356 }
8357}