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::{
25 DiagnosticLogLevel, DiagnosticSourcesConfiguration, DiagnosticsConfiguration,
26 McpDiagnosticSourceConfiguration,
27};
28pub use crate::generated::api_types::{ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus};
30pub use crate::generated::session_events::AutoTier;
35use crate::generated::session_events::ReasoningSummary;
36pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig};
38use crate::github_token::GitHubTokenProvider;
39use crate::handler::{
40 AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler,
41 PermissionHandler, UserInputHandler,
42};
43use crate::hooks::SessionHooks;
44use crate::provider_token::BearerTokenProvider;
45pub use crate::session_fs::{
46 DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig,
47 SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult,
48 SessionFsSqliteQueryType, SessionFsSqliteTransactionError,
49 SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionStatement,
50};
51pub use crate::trace_context::{TraceContext, TraceContextProvider};
52use crate::transforms::SystemMessageTransform;
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57#[allow(dead_code)]
58#[non_exhaustive]
59pub(crate) enum ConnectionState {
60 Disconnected,
62 Connecting,
64 Connected,
66 Error,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
75#[non_exhaustive]
76pub enum SessionLifecycleEventType {
77 #[serde(rename = "session.created")]
79 Created,
80 #[serde(rename = "session.deleted")]
82 Deleted,
83 #[serde(rename = "session.updated")]
85 Updated,
86 #[serde(rename = "session.foreground")]
88 Foreground,
89 #[serde(rename = "session.background")]
91 Background,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96pub struct SessionLifecycleEventMetadata {
97 #[serde(rename = "startTime")]
99 pub start_time: String,
100 #[serde(rename = "modifiedTime")]
102 pub modified_time: String,
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub summary: Option<String>,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct SessionLifecycleEvent {
112 #[serde(rename = "type")]
114 pub event_type: SessionLifecycleEventType,
115 #[serde(rename = "sessionId")]
117 pub session_id: SessionId,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub metadata: Option<SessionLifecycleEventMetadata>,
121}
122
123#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
129#[serde(transparent)]
130pub struct SessionId(String);
131
132impl SessionId {
133 pub fn new(id: impl Into<String>) -> Self {
135 Self(id.into())
136 }
137
138 pub fn as_str(&self) -> &str {
140 &self.0
141 }
142
143 pub fn into_inner(self) -> String {
145 self.0
146 }
147}
148
149impl std::ops::Deref for SessionId {
150 type Target = str;
151
152 fn deref(&self) -> &str {
153 &self.0
154 }
155}
156
157impl std::fmt::Display for SessionId {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 f.write_str(&self.0)
160 }
161}
162
163impl From<String> for SessionId {
164 fn from(s: String) -> Self {
165 Self(s)
166 }
167}
168
169impl From<&str> for SessionId {
170 fn from(s: &str) -> Self {
171 Self(s.to_owned())
172 }
173}
174
175impl AsRef<str> for SessionId {
176 fn as_ref(&self) -> &str {
177 &self.0
178 }
179}
180
181impl std::borrow::Borrow<str> for SessionId {
182 fn borrow(&self) -> &str {
183 &self.0
184 }
185}
186
187impl From<SessionId> for String {
188 fn from(id: SessionId) -> String {
189 id.0
190 }
191}
192
193impl PartialEq<str> for SessionId {
194 fn eq(&self, other: &str) -> bool {
195 self.0 == other
196 }
197}
198
199impl PartialEq<String> for SessionId {
200 fn eq(&self, other: &String) -> bool {
201 &self.0 == other
202 }
203}
204
205impl PartialEq<SessionId> for String {
206 fn eq(&self, other: &SessionId) -> bool {
207 self == &other.0
208 }
209}
210
211impl PartialEq<&str> for SessionId {
212 fn eq(&self, other: &&str) -> bool {
213 self.0 == *other
214 }
215}
216
217impl PartialEq<&SessionId> for SessionId {
218 fn eq(&self, other: &&SessionId) -> bool {
219 self.0 == other.0
220 }
221}
222
223impl PartialEq<SessionId> for &SessionId {
224 fn eq(&self, other: &SessionId) -> bool {
225 self.0 == other.0
226 }
227}
228
229#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
235#[serde(transparent)]
236pub struct RequestId(String);
237
238impl RequestId {
239 pub fn new(id: impl Into<String>) -> Self {
241 Self(id.into())
242 }
243
244 pub fn into_inner(self) -> String {
246 self.0
247 }
248}
249
250impl std::ops::Deref for RequestId {
251 type Target = str;
252
253 fn deref(&self) -> &str {
254 &self.0
255 }
256}
257
258impl std::fmt::Display for RequestId {
259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260 f.write_str(&self.0)
261 }
262}
263
264impl From<String> for RequestId {
265 fn from(s: String) -> Self {
266 Self(s)
267 }
268}
269
270impl From<&str> for RequestId {
271 fn from(s: &str) -> Self {
272 Self(s.to_owned())
273 }
274}
275
276impl AsRef<str> for RequestId {
277 fn as_ref(&self) -> &str {
278 &self.0
279 }
280}
281
282impl std::borrow::Borrow<str> for RequestId {
283 fn borrow(&self) -> &str {
284 &self.0
285 }
286}
287
288impl From<RequestId> for String {
289 fn from(id: RequestId) -> String {
290 id.0
291 }
292}
293
294impl PartialEq<str> for RequestId {
295 fn eq(&self, other: &str) -> bool {
296 self.0 == other
297 }
298}
299
300impl PartialEq<String> for RequestId {
301 fn eq(&self, other: &String) -> bool {
302 &self.0 == other
303 }
304}
305
306impl PartialEq<RequestId> for String {
307 fn eq(&self, other: &RequestId) -> bool {
308 self == &other.0
309 }
310}
311
312impl PartialEq<&str> for RequestId {
313 fn eq(&self, other: &&str) -> bool {
314 self.0 == *other
315 }
316}
317
318#[derive(Clone, Default, Serialize, Deserialize)]
333#[serde(rename_all = "camelCase")]
334#[non_exhaustive]
335pub struct Tool {
336 pub name: String,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub namespaced_name: Option<String>,
342 #[serde(default)]
344 pub description: String,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub instructions: Option<String>,
348 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
350 pub parameters: IndexMap<String, Value>,
351 #[serde(default, skip_serializing_if = "is_false")]
355 pub overrides_built_in_tool: bool,
356 #[serde(default, skip_serializing_if = "is_false")]
360 pub skip_permission: bool,
361 #[serde(default, skip_serializing_if = "is_false")]
366 pub is_terminal: bool,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub defer: Option<DeferMode>,
374 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
379 pub metadata: IndexMap<String, Value>,
380 #[serde(skip)]
392 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
393}
394
395#[inline]
396fn is_false(b: &bool) -> bool {
397 !*b
398}
399
400#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
403#[serde(rename_all = "lowercase")]
404pub enum DeferMode {
405 Auto,
407 Never,
409}
410
411impl Tool {
412 pub fn new(name: impl Into<String>) -> Self {
432 Self {
433 name: name.into(),
434 ..Default::default()
435 }
436 }
437
438 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
441 self.namespaced_name = Some(namespaced_name.into());
442 self
443 }
444
445 pub fn with_description(mut self, description: impl Into<String>) -> Self {
447 self.description = description.into();
448 self
449 }
450
451 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
453 self.instructions = Some(instructions.into());
454 self
455 }
456
457 pub fn with_parameters(mut self, parameters: Value) -> Self {
471 self.parameters = crate::tool::tool_parameters(parameters);
472 self
473 }
474
475 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
479 self.overrides_built_in_tool = overrides;
480 self
481 }
482
483 pub fn with_skip_permission(mut self, skip: bool) -> Self {
487 self.skip_permission = skip;
488 self
489 }
490
491 #[must_use]
498 pub fn with_is_terminal(mut self, is_terminal: bool) -> Self {
499 self.is_terminal = is_terminal;
500 self
501 }
502
503 pub fn with_defer(mut self, defer: DeferMode) -> Self {
507 self.defer = Some(defer);
508 self
509 }
510
511 pub fn with_metadata(mut self, metadata: IndexMap<String, Value>) -> Self {
514 self.metadata = metadata;
515 self
516 }
517
518 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
522 self.handler = Some(handler);
523 self
524 }
525
526 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
531 self.handler.as_ref()
532 }
533}
534
535impl std::fmt::Debug for Tool {
536 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
537 f.debug_struct("Tool")
538 .field("name", &self.name)
539 .field("namespaced_name", &self.namespaced_name)
540 .field("description", &self.description)
541 .field("instructions", &self.instructions)
542 .field("parameters", &self.parameters)
543 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
544 .field("skip_permission", &self.skip_permission)
545 .field("is_terminal", &self.is_terminal)
546 .field("defer", &self.defer)
547 .field("metadata", &self.metadata)
548 .field(
549 "handler",
550 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
551 )
552 .finish()
553 }
554}
555
556#[non_exhaustive]
559#[derive(Debug, Clone)]
560pub struct CommandContext {
561 pub session_id: SessionId,
563 pub command: String,
565 pub command_name: String,
567 pub args: String,
569}
570
571#[async_trait::async_trait]
577pub trait CommandHandler: Send + Sync {
578 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
580}
581
582#[non_exhaustive]
588#[derive(Clone)]
589pub struct CommandDefinition {
590 pub name: String,
592 pub description: Option<String>,
594 pub handler: Arc<dyn CommandHandler>,
596}
597
598impl CommandDefinition {
599 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
602 Self {
603 name: name.into(),
604 description: None,
605 handler,
606 }
607 }
608
609 pub fn with_description(mut self, description: impl Into<String>) -> Self {
611 self.description = Some(description.into());
612 self
613 }
614}
615
616impl std::fmt::Debug for CommandDefinition {
617 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
618 f.debug_struct("CommandDefinition")
619 .field("name", &self.name)
620 .field("description", &self.description)
621 .field("handler", &"<set>")
622 .finish()
623 }
624}
625
626impl Serialize for CommandDefinition {
627 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
628 use serde::ser::SerializeStruct;
629 let mut state = serializer.serialize_struct("CommandDefinition", 2)?;
630 state.serialize_field("name", &self.name)?;
631 state.serialize_field("description", self.description.as_deref().unwrap_or(""))?;
632 state.end()
633 }
634}
635
636#[derive(Debug, Clone, Default, Serialize, Deserialize)]
643#[serde(rename_all = "camelCase")]
644#[non_exhaustive]
645pub struct CustomAgentConfig {
646 pub name: String,
648 #[serde(default, skip_serializing_if = "Option::is_none")]
650 pub display_name: Option<String>,
651 #[serde(default, skip_serializing_if = "Option::is_none")]
653 pub description: Option<String>,
654 #[serde(default, skip_serializing_if = "Option::is_none")]
656 pub tools: Option<Vec<String>>,
657 pub prompt: String,
659 #[serde(default, skip_serializing_if = "Option::is_none")]
661 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
662 #[serde(default, skip_serializing_if = "Option::is_none")]
664 pub infer: Option<bool>,
665 #[serde(default, skip_serializing_if = "Option::is_none")]
667 pub skills: Option<Vec<String>>,
668 #[serde(default, skip_serializing_if = "Option::is_none")]
673 pub model: Option<String>,
674 #[serde(default, skip_serializing_if = "Option::is_none")]
679 pub reasoning_effort: Option<String>,
680}
681
682impl CustomAgentConfig {
683 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
690 Self {
691 name: name.into(),
692 prompt: prompt.into(),
693 ..Self::default()
694 }
695 }
696
697 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
699 self.display_name = Some(display_name.into());
700 self
701 }
702
703 pub fn with_description(mut self, description: impl Into<String>) -> Self {
705 self.description = Some(description.into());
706 self
707 }
708
709 pub fn with_tools<I, S>(mut self, tools: I) -> Self
712 where
713 I: IntoIterator<Item = S>,
714 S: Into<String>,
715 {
716 self.tools = Some(tools.into_iter().map(Into::into).collect());
717 self
718 }
719
720 pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
722 self.mcp_servers = Some(mcp_servers);
723 self
724 }
725
726 pub fn with_infer(mut self, infer: bool) -> Self {
728 self.infer = Some(infer);
729 self
730 }
731
732 pub fn with_skills<I, S>(mut self, skills: I) -> Self
734 where
735 I: IntoIterator<Item = S>,
736 S: Into<String>,
737 {
738 self.skills = Some(skills.into_iter().map(Into::into).collect());
739 self
740 }
741
742 pub fn with_model(mut self, model: impl Into<String>) -> Self {
744 self.model = Some(model.into());
745 self
746 }
747
748 pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into<String>) -> Self {
750 self.reasoning_effort = Some(reasoning_effort.into());
751 self
752 }
753}
754
755#[derive(Debug, Clone, Default, Serialize, Deserialize)]
762#[serde(rename_all = "camelCase")]
763pub struct DefaultAgentConfig {
764 #[serde(default, skip_serializing_if = "Option::is_none")]
766 pub excluded_tools: Option<Vec<String>>,
767}
768
769#[derive(Debug, Clone, Default, Serialize, Deserialize)]
775#[serde(rename_all = "camelCase")]
776#[non_exhaustive]
777pub struct LargeToolOutputConfig {
778 #[serde(default, skip_serializing_if = "Option::is_none")]
780 pub enabled: Option<bool>,
781 #[serde(default, skip_serializing_if = "Option::is_none")]
784 pub max_size_bytes: Option<u64>,
785 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
788 pub output_directory: Option<PathBuf>,
789}
790
791impl LargeToolOutputConfig {
792 pub fn new() -> Self {
795 Self::default()
796 }
797
798 pub fn with_enabled(mut self, enabled: bool) -> Self {
800 self.enabled = Some(enabled);
801 self
802 }
803
804 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
806 self.max_size_bytes = Some(max_size_bytes);
807 self
808 }
809
810 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
812 self.output_directory = Some(output_directory.into());
813 self
814 }
815}
816
817#[derive(Debug, Clone, Default, Serialize, Deserialize)]
823#[serde(rename_all = "camelCase")]
824#[non_exhaustive]
825pub struct ToolSearchConfig {
826 #[serde(default, skip_serializing_if = "Option::is_none")]
828 pub enabled: Option<bool>,
829 #[serde(default, skip_serializing_if = "Option::is_none")]
832 pub defer_threshold: Option<u32>,
833}
834
835impl ToolSearchConfig {
836 pub fn new() -> Self {
839 Self::default()
840 }
841
842 pub fn with_enabled(mut self, enabled: bool) -> Self {
844 self.enabled = Some(enabled);
845 self
846 }
847
848 pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
851 self.defer_threshold = Some(defer_threshold);
852 self
853 }
854}
855
856#[derive(Debug, Clone, Default, Serialize, Deserialize)]
861#[serde(rename_all = "camelCase")]
862#[non_exhaustive]
863pub struct GitHubMcpToolConfig {
864 #[serde(default, skip_serializing_if = "Option::is_none")]
866 pub enable_all_tools: Option<bool>,
867 #[serde(default, skip_serializing_if = "Option::is_none")]
869 pub additional_toolsets: Option<Vec<String>>,
870 #[serde(default, skip_serializing_if = "Option::is_none")]
872 pub additional_tools: Option<Vec<String>>,
873 #[serde(default, skip_serializing_if = "Option::is_none")]
875 pub enable_insiders_mode: Option<bool>,
876 #[serde(default, skip_serializing_if = "Option::is_none")]
880 pub disable_form_deferral: Option<bool>,
881}
882
883impl GitHubMcpToolConfig {
884 pub fn new() -> Self {
886 Self::default()
887 }
888
889 pub fn with_enable_all_tools(mut self, value: bool) -> Self {
891 self.enable_all_tools = Some(value);
892 self
893 }
894
895 pub fn with_additional_toolsets<I, S>(mut self, values: I) -> Self
897 where
898 I: IntoIterator<Item = S>,
899 S: Into<String>,
900 {
901 self.additional_toolsets = Some(values.into_iter().map(Into::into).collect());
902 self
903 }
904
905 pub fn with_additional_tools<I, S>(mut self, values: I) -> Self
907 where
908 I: IntoIterator<Item = S>,
909 S: Into<String>,
910 {
911 self.additional_tools = Some(values.into_iter().map(Into::into).collect());
912 self
913 }
914
915 pub fn with_enable_insiders_mode(mut self, value: bool) -> Self {
917 self.enable_insiders_mode = Some(value);
918 self
919 }
920
921 pub fn with_disable_form_deferral(mut self, value: bool) -> Self {
925 self.disable_form_deferral = Some(value);
926 self
927 }
928}
929
930#[derive(Debug, Clone, Default, Serialize, Deserialize)]
937#[serde(rename_all = "camelCase")]
938#[non_exhaustive]
939pub struct InfiniteSessionConfig {
940 #[serde(default, skip_serializing_if = "Option::is_none")]
942 pub enabled: Option<bool>,
943 #[serde(default, skip_serializing_if = "Option::is_none")]
946 pub background_compaction_threshold: Option<f64>,
947 #[serde(default, skip_serializing_if = "Option::is_none")]
950 pub buffer_exhaustion_threshold: Option<f64>,
951}
952
953impl InfiniteSessionConfig {
954 pub fn new() -> Self {
957 Self::default()
958 }
959
960 pub fn with_enabled(mut self, enabled: bool) -> Self {
963 self.enabled = Some(enabled);
964 self
965 }
966
967 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
970 self.background_compaction_threshold = Some(threshold);
971 self
972 }
973
974 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
977 self.buffer_exhaustion_threshold = Some(threshold);
978 self
979 }
980}
981
982#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
993#[serde(rename_all = "camelCase")]
994#[non_exhaustive]
995pub struct MemoryConfiguration {
996 pub enabled: bool,
998}
999
1000impl MemoryConfiguration {
1001 pub fn enabled() -> Self {
1003 Self { enabled: true }
1004 }
1005
1006 pub fn disabled() -> Self {
1008 Self { enabled: false }
1009 }
1010
1011 pub fn with_enabled(mut self, enabled: bool) -> Self {
1013 self.enabled = enabled;
1014 self
1015 }
1016}
1017
1018#[derive(Debug, Clone, Serialize, Deserialize)]
1020#[serde(rename_all = "camelCase")]
1021#[non_exhaustive]
1022pub struct CloudSessionRepository {
1023 pub owner: String,
1025 pub name: String,
1027 #[serde(skip_serializing_if = "Option::is_none")]
1029 pub branch: Option<String>,
1030}
1031
1032impl CloudSessionRepository {
1033 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
1035 Self {
1036 owner: owner.into(),
1037 name: name.into(),
1038 branch: None,
1039 }
1040 }
1041
1042 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
1044 self.branch = Some(branch.into());
1045 self
1046 }
1047}
1048
1049#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1051#[serde(rename_all = "camelCase")]
1052#[non_exhaustive]
1053pub struct CloudSessionOptions {
1054 #[serde(skip_serializing_if = "Option::is_none")]
1056 pub repository: Option<CloudSessionRepository>,
1057}
1058
1059impl CloudSessionOptions {
1060 pub fn with_repository(repository: CloudSessionRepository) -> Self {
1062 Self {
1063 repository: Some(repository),
1064 }
1065 }
1066}
1067
1068#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1070#[serde(rename_all = "camelCase")]
1071pub struct ExtensionInfo {
1072 pub source: String,
1074 pub name: String,
1076}
1077
1078impl ExtensionInfo {
1079 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
1081 Self {
1082 source: source.into(),
1083 name: name.into(),
1084 }
1085 }
1086}
1087
1088#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1099#[serde(rename_all = "camelCase")]
1100pub struct CanvasProviderIdentity {
1101 pub id: String,
1103 #[serde(skip_serializing_if = "Option::is_none")]
1105 pub name: Option<String>,
1106}
1107
1108impl CanvasProviderIdentity {
1109 pub fn new(id: impl Into<String>) -> Self {
1111 Self {
1112 id: id.into(),
1113 name: None,
1114 }
1115 }
1116
1117 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1119 self.name = Some(name.into());
1120 self
1121 }
1122}
1123
1124#[derive(Debug, Clone, Serialize, Deserialize)]
1158#[serde(tag = "type", rename_all = "lowercase")]
1159#[non_exhaustive]
1160pub enum McpServerConfig {
1161 #[serde(alias = "local")]
1165 Stdio(McpStdioServerConfig),
1166 Http(McpHttpServerConfig),
1168 Sse(McpHttpServerConfig),
1170}
1171
1172#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1176#[serde(rename_all = "camelCase")]
1177pub struct McpStdioServerConfig {
1178 #[serde(default, skip_serializing_if = "Option::is_none")]
1184 pub tools: Option<Vec<String>>,
1185 #[serde(default, skip_serializing_if = "Option::is_none")]
1187 pub timeout: Option<i64>,
1188 pub command: String,
1190 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1192 pub args: Vec<String>,
1193 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1196 pub env: HashMap<String, String>,
1197 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1199 pub working_directory: Option<String>,
1200}
1201
1202#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1206#[serde(rename_all = "camelCase")]
1207pub struct McpHttpServerConfig {
1208 #[serde(default, skip_serializing_if = "Option::is_none")]
1214 pub tools: Option<Vec<String>>,
1215 #[serde(default, skip_serializing_if = "Option::is_none")]
1217 pub timeout: Option<i64>,
1218 pub url: String,
1220 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1222 pub headers: HashMap<String, String>,
1223}
1224
1225#[derive(Clone, Default, Serialize, Deserialize)]
1231#[serde(rename_all = "camelCase")]
1232#[non_exhaustive]
1233pub struct ProviderConfig {
1234 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1237 pub provider_type: Option<String>,
1238 #[serde(default, skip_serializing_if = "Option::is_none")]
1241 pub wire_api: Option<String>,
1242 #[serde(default, skip_serializing_if = "Option::is_none")]
1247 pub transport: Option<String>,
1248 pub base_url: String,
1250 #[serde(default, skip_serializing_if = "Option::is_none")]
1252 pub api_key: Option<String>,
1253 #[serde(default, skip_serializing_if = "Option::is_none")]
1257 pub bearer_token: Option<String>,
1258 #[serde(skip)]
1261 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1262 #[serde(default, skip_serializing_if = "Option::is_none")]
1263 pub(crate) has_bearer_token_provider: Option<bool>,
1264 #[serde(default, skip_serializing_if = "Option::is_none")]
1266 pub azure: Option<AzureProviderOptions>,
1267 #[serde(default, skip_serializing_if = "Option::is_none")]
1269 pub headers: Option<HashMap<String, String>>,
1270 #[serde(default, skip_serializing_if = "Option::is_none")]
1274 pub model_id: Option<String>,
1275 #[serde(default, skip_serializing_if = "Option::is_none")]
1282 pub wire_model: Option<String>,
1283 #[serde(default, skip_serializing_if = "Option::is_none")]
1288 pub max_prompt_tokens: Option<i64>,
1289 #[serde(default, skip_serializing_if = "Option::is_none")]
1292 pub max_output_tokens: Option<i64>,
1293}
1294
1295impl std::fmt::Debug for ProviderConfig {
1296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1297 f.debug_struct("ProviderConfig")
1298 .field("provider_type", &self.provider_type)
1299 .field("wire_api", &self.wire_api)
1300 .field("transport", &self.transport)
1301 .field("base_url", &self.base_url)
1302 .field("api_key", &self.api_key)
1303 .field("bearer_token", &self.bearer_token)
1304 .field(
1305 "bearer_token_provider",
1306 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1307 )
1308 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1309 .field("azure", &self.azure)
1310 .field("headers", &self.headers)
1311 .field("model_id", &self.model_id)
1312 .field("wire_model", &self.wire_model)
1313 .field("max_prompt_tokens", &self.max_prompt_tokens)
1314 .field("max_output_tokens", &self.max_output_tokens)
1315 .finish()
1316 }
1317}
1318
1319impl ProviderConfig {
1320 pub fn new(base_url: impl Into<String>) -> Self {
1323 Self {
1324 base_url: base_url.into(),
1325 ..Self::default()
1326 }
1327 }
1328
1329 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1331 self.provider_type = Some(provider_type.into());
1332 self
1333 }
1334
1335 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1337 self.wire_api = Some(wire_api.into());
1338 self
1339 }
1340
1341 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1344 self.transport = Some(transport.into());
1345 self
1346 }
1347
1348 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1350 self.api_key = Some(api_key.into());
1351 self
1352 }
1353
1354 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1357 self.bearer_token = Some(bearer_token.into());
1358 self
1359 }
1360
1361 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1367 self.bearer_token_provider = Some(provider);
1368 self
1369 }
1370
1371 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1373 self.azure = Some(azure);
1374 self
1375 }
1376
1377 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1379 self.headers = Some(headers);
1380 self
1381 }
1382
1383 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1386 self.model_id = Some(model_id.into());
1387 self
1388 }
1389
1390 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1395 self.wire_model = Some(wire_model.into());
1396 self
1397 }
1398
1399 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1403 self.max_prompt_tokens = Some(max);
1404 self
1405 }
1406
1407 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1410 self.max_output_tokens = Some(max);
1411 self
1412 }
1413}
1414
1415#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1428#[serde(rename_all = "camelCase")]
1429#[non_exhaustive]
1430pub struct CapiSessionOptions {
1431 #[serde(default, skip_serializing_if = "Option::is_none")]
1442 pub auto_tier: Option<AutoTier>,
1443
1444 #[serde(default, skip_serializing_if = "Option::is_none")]
1450 pub enable_web_socket_responses: Option<bool>,
1451}
1452
1453impl CapiSessionOptions {
1454 pub fn new() -> Self {
1456 Self::default()
1457 }
1458
1459 pub fn with_auto_tier(mut self, auto_tier: AutoTier) -> Self {
1461 self.auto_tier = Some(auto_tier);
1462 self
1463 }
1464
1465 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1467 self.enable_web_socket_responses = Some(enable);
1468 self
1469 }
1470}
1471
1472#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1474#[serde(rename_all = "camelCase")]
1475pub struct AzureProviderOptions {
1476 #[serde(default, skip_serializing_if = "Option::is_none")]
1478 pub api_version: Option<String>,
1479}
1480
1481#[derive(Clone, Default, Serialize, Deserialize)]
1492#[serde(rename_all = "camelCase")]
1493#[non_exhaustive]
1494pub struct NamedProviderConfig {
1495 pub name: String,
1498 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1501 pub provider_type: Option<String>,
1502 #[serde(default, skip_serializing_if = "Option::is_none")]
1505 pub wire_api: Option<String>,
1506 pub base_url: String,
1508 #[serde(default, skip_serializing_if = "Option::is_none")]
1510 pub api_key: Option<String>,
1511 #[serde(default, skip_serializing_if = "Option::is_none")]
1514 pub bearer_token: Option<String>,
1515 #[serde(skip)]
1518 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1519 #[serde(default, skip_serializing_if = "Option::is_none")]
1520 pub(crate) has_bearer_token_provider: Option<bool>,
1521 #[serde(default, skip_serializing_if = "Option::is_none")]
1523 pub azure: Option<AzureProviderOptions>,
1524 #[serde(default, skip_serializing_if = "Option::is_none")]
1526 pub headers: Option<HashMap<String, String>>,
1527}
1528
1529impl std::fmt::Debug for NamedProviderConfig {
1530 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1531 f.debug_struct("NamedProviderConfig")
1532 .field("name", &self.name)
1533 .field("provider_type", &self.provider_type)
1534 .field("wire_api", &self.wire_api)
1535 .field("base_url", &self.base_url)
1536 .field("api_key", &self.api_key)
1537 .field("bearer_token", &self.bearer_token)
1538 .field(
1539 "bearer_token_provider",
1540 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1541 )
1542 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1543 .field("azure", &self.azure)
1544 .field("headers", &self.headers)
1545 .finish()
1546 }
1547}
1548
1549impl NamedProviderConfig {
1550 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1553 Self {
1554 name: name.into(),
1555 base_url: base_url.into(),
1556 ..Self::default()
1557 }
1558 }
1559
1560 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1562 self.provider_type = Some(provider_type.into());
1563 self
1564 }
1565
1566 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1568 self.wire_api = Some(wire_api.into());
1569 self
1570 }
1571
1572 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1574 self.api_key = Some(api_key.into());
1575 self
1576 }
1577
1578 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1581 self.bearer_token = Some(bearer_token.into());
1582 self
1583 }
1584
1585 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1591 self.bearer_token_provider = Some(provider);
1592 self
1593 }
1594
1595 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1597 self.azure = Some(azure);
1598 self
1599 }
1600
1601 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1603 self.headers = Some(headers);
1604 self
1605 }
1606}
1607
1608fn prepare_bearer_token_providers(
1609 provider: &mut Option<ProviderConfig>,
1610 providers: &mut Option<Vec<NamedProviderConfig>>,
1611) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1612 let mut bearer_token_providers = HashMap::new();
1613
1614 if let Some(provider) = provider.as_mut()
1615 && let Some(token_provider) = provider.bearer_token_provider.take()
1616 {
1617 provider.has_bearer_token_provider = Some(true);
1618 bearer_token_providers.insert("default".to_string(), token_provider);
1619 }
1620
1621 if let Some(providers) = providers.as_mut() {
1622 for provider in providers {
1623 if let Some(token_provider) = provider.bearer_token_provider.take() {
1624 provider.has_bearer_token_provider = Some(true);
1625 bearer_token_providers.insert(provider.name.clone(), token_provider);
1626 }
1627 }
1628 }
1629
1630 bearer_token_providers
1631}
1632
1633#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1641#[serde(rename_all = "camelCase")]
1642#[non_exhaustive]
1643pub struct ProviderModelConfig {
1644 pub id: String,
1647 pub provider: String,
1649 #[serde(default, skip_serializing_if = "Option::is_none")]
1652 pub wire_model: Option<String>,
1653 #[serde(default, skip_serializing_if = "Option::is_none")]
1656 pub model_id: Option<String>,
1657 #[serde(default, skip_serializing_if = "Option::is_none")]
1659 pub name: Option<String>,
1660 #[serde(default, skip_serializing_if = "Option::is_none")]
1662 pub max_prompt_tokens: Option<i64>,
1663 #[serde(default, skip_serializing_if = "Option::is_none")]
1665 pub max_context_window_tokens: Option<i64>,
1666 #[serde(default, skip_serializing_if = "Option::is_none")]
1668 pub max_output_tokens: Option<i64>,
1669 #[serde(default, skip_serializing_if = "Option::is_none")]
1672 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1673}
1674
1675impl ProviderModelConfig {
1676 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1679 Self {
1680 id: id.into(),
1681 provider: provider.into(),
1682 ..Self::default()
1683 }
1684 }
1685
1686 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1688 self.wire_model = Some(wire_model.into());
1689 self
1690 }
1691
1692 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1695 self.model_id = Some(model_id.into());
1696 self
1697 }
1698
1699 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1701 self.name = Some(name.into());
1702 self
1703 }
1704
1705 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1707 self.max_prompt_tokens = Some(max);
1708 self
1709 }
1710
1711 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1713 self.max_context_window_tokens = Some(max);
1714 self
1715 }
1716
1717 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1719 self.max_output_tokens = Some(max);
1720 self
1721 }
1722
1723 pub fn with_capabilities(
1725 mut self,
1726 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1727 ) -> Self {
1728 self.capabilities = Some(capabilities);
1729 self
1730 }
1731}
1732
1733#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1737#[serde(untagged)]
1738pub enum ExpFlagValue {
1739 Bool(bool),
1741 Integer(i64),
1743 Float(f64),
1745 String(String),
1747 Null,
1749}
1750
1751#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1755#[serde(rename_all = "PascalCase")]
1756pub struct ExpConfigEntry {
1757 pub id: String,
1759 pub parameters: HashMap<String, ExpFlagValue>,
1761}
1762
1763#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1769#[serde(rename_all = "PascalCase")]
1770pub struct CopilotExpAssignmentResponse {
1771 #[serde(default)]
1773 pub features: Vec<String>,
1774 #[serde(default)]
1776 pub flights: HashMap<String, String>,
1777 #[serde(default)]
1779 pub configs: Vec<ExpConfigEntry>,
1780 #[serde(default, skip_serializing_if = "Option::is_none")]
1782 pub parameter_groups: Option<Value>,
1783 #[serde(default, skip_serializing_if = "Option::is_none")]
1785 pub flighting_version: Option<i64>,
1786 #[serde(default, skip_serializing_if = "Option::is_none")]
1788 pub impression_id: Option<String>,
1789 #[serde(default)]
1791 pub assignment_context: String,
1792}
1793
1794pub struct DisableBypassPermissionsModes;
1796
1797impl DisableBypassPermissionsModes {
1798 pub const ALLOW_AUTO_ONLY: &'static str = "allow-auto-only";
1800 pub const DISABLE: &'static str = "disable";
1802}
1803
1804#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1813#[serde(rename_all = "camelCase")]
1814#[non_exhaustive]
1815pub struct ManagedSettingsPermissions {
1816 #[serde(default, skip_serializing_if = "Option::is_none")]
1820 pub disable_bypass_permissions_mode: Option<String>,
1821 #[serde(default, skip_serializing_if = "Option::is_none")]
1823 pub deny: Option<Vec<String>>,
1824 #[serde(default, skip_serializing_if = "Option::is_none")]
1826 pub ask: Option<Vec<String>>,
1827 #[serde(default, skip_serializing_if = "Option::is_none")]
1829 pub allow: Option<Vec<String>>,
1830}
1831
1832impl ManagedSettingsPermissions {
1833 pub fn with_disable_bypass_permissions_mode(mut self, value: impl Into<String>) -> Self {
1835 self.disable_bypass_permissions_mode = Some(value.into());
1836 self
1837 }
1838
1839 pub fn with_deny(mut self, rules: Vec<String>) -> Self {
1841 self.deny = Some(rules);
1842 self
1843 }
1844
1845 pub fn with_ask(mut self, rules: Vec<String>) -> Self {
1847 self.ask = Some(rules);
1848 self
1849 }
1850
1851 pub fn with_allow(mut self, rules: Vec<String>) -> Self {
1853 self.allow = Some(rules);
1854 self
1855 }
1856}
1857
1858#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1868#[serde(rename_all = "camelCase")]
1869#[non_exhaustive]
1870pub struct ManagedSettings {
1871 #[serde(default, skip_serializing_if = "Option::is_none")]
1873 pub permissions: Option<ManagedSettingsPermissions>,
1874}
1875
1876impl ManagedSettings {
1877 pub fn with_permissions(mut self, permissions: ManagedSettingsPermissions) -> Self {
1879 self.permissions = Some(permissions);
1880 self
1881 }
1882}
1883
1884#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1886#[serde(rename_all = "lowercase")]
1887#[non_exhaustive]
1888pub enum AskUserVariant {
1889 #[default]
1891 Legacy,
1892 Elicitation,
1894}
1895
1896#[derive(Clone)]
1948#[non_exhaustive]
1949pub struct SessionConfig {
1950 pub session_id: Option<SessionId>,
1952 pub model: Option<String>,
1954 pub allowed_models: Option<Vec<String>>,
1959 pub client_name: Option<String>,
1961 pub reasoning_effort: Option<String>,
1963 pub reasoning_summary: Option<ReasoningSummary>,
1967 pub context_tier: Option<String>,
1970 pub streaming: Option<bool>,
1972 pub system_message: Option<SystemMessageConfig>,
1974 pub ask_user_variant: Option<AskUserVariant>,
1979 pub tools: Option<Vec<Tool>>,
1981 pub canvases: Option<Vec<CanvasDeclaration>>,
1983 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1988 pub request_canvas_renderer: Option<bool>,
1990 pub request_extensions: Option<bool>,
1992 pub extension_sdk_path: Option<String>,
1996 pub extension_info: Option<ExtensionInfo>,
1998 pub canvas_provider: Option<CanvasProviderIdentity>,
2001 pub available_tools: Option<Vec<String>>,
2003 pub excluded_tools: Option<Vec<String>>,
2005 pub excluded_builtin_agents: Option<Vec<String>>,
2011 pub included_builtin_skills: Option<Vec<String>>,
2015 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
2017 pub diagnostics: Option<DiagnosticsConfiguration>,
2022 pub mcp_oauth_token_storage: Option<String>,
2031 pub auth_client_id_metadata_url: Option<String>,
2038 pub enable_config_discovery: Option<bool>,
2041 pub skip_embedding_retrieval: Option<bool>,
2043 pub embedding_cache_storage: Option<String>,
2046 pub organization_custom_instructions: Option<String>,
2048 pub refresh_custom_instructions: Option<bool>,
2055 pub enable_on_demand_instruction_discovery: Option<bool>,
2057 pub enable_file_hooks: Option<bool>,
2059 pub enable_host_git_operations: Option<bool>,
2061 pub enable_session_store: Option<bool>,
2063 pub enable_skills: Option<bool>,
2065 pub enable_mcp_apps: Option<bool>,
2092 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
2097 pub skill_directories: Option<Vec<PathBuf>>,
2099 pub instruction_directories: Option<Vec<PathBuf>>,
2102 pub plugin_directories: Option<Vec<PathBuf>>,
2104 pub large_output: Option<LargeToolOutputConfig>,
2106 pub tool_search: Option<ToolSearchConfig>,
2110 pub disabled_skills: Option<Vec<String>>,
2113 pub disabled_mcp_servers: Option<Vec<String>>,
2117 pub hooks: Option<bool>,
2121 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2123 pub default_agent: Option<DefaultAgentConfig>,
2127 pub agent: Option<String>,
2130 pub infinite_sessions: Option<InfiniteSessionConfig>,
2133 pub provider: Option<ProviderConfig>,
2137 pub capi: Option<CapiSessionOptions>,
2143 pub providers: Option<Vec<NamedProviderConfig>>,
2150 pub models: Option<Vec<ProviderModelConfig>>,
2156 pub enable_session_telemetry: Option<bool>,
2164 pub enable_citations: Option<bool>,
2166 pub enable_file_change_tracking: Option<bool>,
2169 pub session_limits: Option<SessionLimitsConfig>,
2171 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2174 pub memory: Option<MemoryConfiguration>,
2176 pub config_directory: Option<PathBuf>,
2179 pub working_directory: Option<PathBuf>,
2182 pub additional_directories: Option<Vec<PathBuf>>,
2186 pub github_token: Option<String>,
2192 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2198 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2204 pub cloud: Option<CloudSessionOptions>,
2207 pub include_sub_agent_streaming_events: Option<bool>,
2211 pub commands: Option<Vec<CommandDefinition>>,
2215 pub feature_flags: Option<HashMap<String, bool>>,
2221 #[doc(hidden)]
2228 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
2229 pub enable_managed_settings: Option<bool>,
2237 pub managed_settings: Option<ManagedSettings>,
2246 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2251 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2255 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2258 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2261 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2265 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2268 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2271 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2275 pub(crate) permission_policy: Option<crate::permission::Policy>,
2279 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2284 pub skip_custom_instructions: Option<bool>,
2288 pub custom_agents_local_only: Option<bool>,
2292 pub enable_experimental_mode: Option<bool>,
2297 pub coauthor_enabled: Option<bool>,
2301 pub manage_schedule_enabled: Option<bool>,
2305 pub event_buffer_capacity: Option<usize>,
2322}
2323
2324impl std::fmt::Debug for SessionConfig {
2325 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2326 f.debug_struct("SessionConfig")
2327 .field("session_id", &self.session_id)
2328 .field("model", &self.model)
2329 .field("allowed_models", &self.allowed_models)
2330 .field("client_name", &self.client_name)
2331 .field("reasoning_effort", &self.reasoning_effort)
2332 .field("reasoning_summary", &self.reasoning_summary)
2333 .field("context_tier", &self.context_tier)
2334 .field("streaming", &self.streaming)
2335 .field("system_message", &self.system_message)
2336 .field("ask_user_variant", &self.ask_user_variant)
2337 .field("tools", &self.tools)
2338 .field("canvases", &self.canvases)
2339 .field(
2340 "canvas_handler",
2341 &self.canvas_handler.as_ref().map(|_| "<set>"),
2342 )
2343 .field("request_canvas_renderer", &self.request_canvas_renderer)
2344 .field("request_extensions", &self.request_extensions)
2345 .field("extension_sdk_path", &self.extension_sdk_path)
2346 .field("extension_info", &self.extension_info)
2347 .field("canvas_provider", &self.canvas_provider)
2348 .field("available_tools", &self.available_tools)
2349 .field("excluded_tools", &self.excluded_tools)
2350 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2351 .field("included_builtin_skills", &self.included_builtin_skills)
2352 .field("mcp_servers", &self.mcp_servers)
2353 .field("diagnostics", &self.diagnostics)
2354 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2355 .field(
2356 "auth_client_id_metadata_url",
2357 &self.auth_client_id_metadata_url,
2358 )
2359 .field("embedding_cache_storage", &self.embedding_cache_storage)
2360 .field("enable_config_discovery", &self.enable_config_discovery)
2361 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2362 .field(
2363 "organization_custom_instructions",
2364 &self
2365 .organization_custom_instructions
2366 .as_ref()
2367 .map(|_| "<redacted>"),
2368 )
2369 .field(
2370 "refresh_custom_instructions",
2371 &self.refresh_custom_instructions,
2372 )
2373 .field(
2374 "enable_on_demand_instruction_discovery",
2375 &self.enable_on_demand_instruction_discovery,
2376 )
2377 .field("enable_file_hooks", &self.enable_file_hooks)
2378 .field(
2379 "enable_host_git_operations",
2380 &self.enable_host_git_operations,
2381 )
2382 .field("enable_session_store", &self.enable_session_store)
2383 .field("enable_skills", &self.enable_skills)
2384 .field("enable_mcp_apps", &self.enable_mcp_apps)
2385 .field("skill_directories", &self.skill_directories)
2386 .field("instruction_directories", &self.instruction_directories)
2387 .field("plugin_directories", &self.plugin_directories)
2388 .field("large_output", &self.large_output)
2389 .field("tool_search", &self.tool_search)
2390 .field("disabled_skills", &self.disabled_skills)
2391 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
2392 .field("hooks", &self.hooks)
2393 .field("custom_agents", &self.custom_agents)
2394 .field("default_agent", &self.default_agent)
2395 .field("agent", &self.agent)
2396 .field("infinite_sessions", &self.infinite_sessions)
2397 .field("provider", &self.provider)
2398 .field("capi", &self.capi)
2399 .field("enable_session_telemetry", &self.enable_session_telemetry)
2400 .field("enable_citations", &self.enable_citations)
2401 .field(
2402 "enable_file_change_tracking",
2403 &self.enable_file_change_tracking,
2404 )
2405 .field("session_limits", &self.session_limits)
2406 .field("model_capabilities", &self.model_capabilities)
2407 .field("memory", &self.memory)
2408 .field("config_directory", &self.config_directory)
2409 .field("working_directory", &self.working_directory)
2410 .field("additional_directories", &self.additional_directories)
2411 .field(
2412 "github_token",
2413 &self.github_token.as_ref().map(|_| "<redacted>"),
2414 )
2415 .field(
2416 "github_token_provider",
2417 &self.github_token_provider.as_ref().map(|_| "<set>"),
2418 )
2419 .field("remote_session", &self.remote_session)
2420 .field("cloud", &self.cloud)
2421 .field(
2422 "include_sub_agent_streaming_events",
2423 &self.include_sub_agent_streaming_events,
2424 )
2425 .field("commands", &self.commands)
2426 .field("feature_flags", &self.feature_flags)
2427 .field("exp_assignments", &self.exp_assignments)
2428 .field("enable_managed_settings", &self.enable_managed_settings)
2429 .field("enable_experimental_mode", &self.enable_experimental_mode)
2430 .field("managed_settings", &self.managed_settings)
2431 .field(
2432 "session_fs_provider",
2433 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2434 )
2435 .field(
2436 "permission_handler",
2437 &self.permission_handler.as_ref().map(|_| "<set>"),
2438 )
2439 .field(
2440 "elicitation_handler",
2441 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2442 )
2443 .field(
2444 "mcp_auth_handler",
2445 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2446 )
2447 .field(
2448 "user_input_handler",
2449 &self.user_input_handler.as_ref().map(|_| "<set>"),
2450 )
2451 .field(
2452 "exit_plan_mode_handler",
2453 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2454 )
2455 .field(
2456 "auto_mode_switch_handler",
2457 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2458 )
2459 .field(
2460 "hooks_handler",
2461 &self.hooks_handler.as_ref().map(|_| "<set>"),
2462 )
2463 .field(
2464 "system_message_transform",
2465 &self.system_message_transform.as_ref().map(|_| "<set>"),
2466 )
2467 .field("event_buffer_capacity", &self.event_buffer_capacity)
2468 .finish()
2469 }
2470}
2471
2472impl Default for SessionConfig {
2473 fn default() -> Self {
2479 Self {
2480 session_id: None,
2481 model: None,
2482 allowed_models: None,
2483 client_name: None,
2484 reasoning_effort: None,
2485 reasoning_summary: None,
2486 context_tier: None,
2487 streaming: None,
2488 system_message: None,
2489 ask_user_variant: None,
2490 tools: None,
2491 canvases: None,
2492 canvas_handler: None,
2493 request_canvas_renderer: None,
2494 request_extensions: None,
2495 extension_sdk_path: None,
2496 extension_info: None,
2497 canvas_provider: None,
2498 available_tools: None,
2499 excluded_tools: None,
2500 excluded_builtin_agents: None,
2501 included_builtin_skills: None,
2502 mcp_servers: None,
2503 diagnostics: None,
2504 mcp_oauth_token_storage: None,
2505 auth_client_id_metadata_url: None,
2506 enable_config_discovery: None,
2507 skip_embedding_retrieval: None,
2508 organization_custom_instructions: None,
2509 refresh_custom_instructions: None,
2510 enable_on_demand_instruction_discovery: None,
2511 enable_file_hooks: None,
2512 enable_host_git_operations: None,
2513 enable_session_store: None,
2514 enable_skills: None,
2515 embedding_cache_storage: None,
2516 enable_mcp_apps: None,
2517 github_mcp_tool_config: None,
2518 skill_directories: None,
2519 instruction_directories: None,
2520 plugin_directories: None,
2521 large_output: None,
2522 tool_search: None,
2523 disabled_skills: None,
2524 disabled_mcp_servers: None,
2525 hooks: None,
2526 custom_agents: None,
2527 default_agent: None,
2528 agent: None,
2529 infinite_sessions: None,
2530 provider: None,
2531 capi: None,
2532 providers: None,
2533 models: None,
2534 enable_session_telemetry: None,
2535 enable_citations: None,
2536 enable_file_change_tracking: None,
2537 session_limits: None,
2538 model_capabilities: None,
2539 memory: None,
2540 config_directory: None,
2541 working_directory: None,
2542 additional_directories: None,
2543 github_token: None,
2544 github_token_provider: None,
2545 remote_session: None,
2546 cloud: None,
2547 include_sub_agent_streaming_events: None,
2548 commands: None,
2549 feature_flags: None,
2550 exp_assignments: None,
2551 enable_managed_settings: None,
2552 managed_settings: None,
2553 session_fs_provider: None,
2554 permission_handler: None,
2555 elicitation_handler: None,
2556 mcp_auth_handler: None,
2557 user_input_handler: None,
2558 exit_plan_mode_handler: None,
2559 auto_mode_switch_handler: None,
2560 hooks_handler: None,
2561 permission_policy: None,
2562 system_message_transform: None,
2563 skip_custom_instructions: None,
2564 custom_agents_local_only: None,
2565 enable_experimental_mode: None,
2566 coauthor_enabled: None,
2567 manage_schedule_enabled: None,
2568 event_buffer_capacity: None,
2569 }
2570 }
2571}
2572
2573pub(crate) struct SessionConfigRuntime {
2579 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2580 pub permission_policy: Option<crate::permission::Policy>,
2581 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2582 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2583 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2584 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2585 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2586 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2587 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2588 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2589 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2590 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2591 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2592 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2593 pub commands: Option<Vec<CommandDefinition>>,
2594}
2595
2596impl SessionConfig {
2597 pub(crate) fn into_wire(
2609 mut self,
2610 session_id: Option<SessionId>,
2611 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2612 if self.github_token.is_some() && self.github_token_provider.is_some() {
2613 return Err(crate::Error::with_message(
2614 crate::ErrorKind::InvalidConfig,
2615 "github_token and github_token_provider are mutually exclusive",
2616 ));
2617 }
2618 let permission_active =
2619 self.permission_handler.is_some() || self.permission_policy.is_some();
2620 let request_user_input = self.user_input_handler.is_some();
2621 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2622 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2623 let request_elicitation = self.elicitation_handler.is_some();
2624 let hooks_flag = self.hooks_handler.is_some();
2625
2626 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2627 if let Some(tools) = self.tools.as_mut() {
2628 for tool in tools.iter_mut() {
2629 if let Some(handler) = tool.handler.take()
2630 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2631 {
2632 return Err(crate::Error::with_message(
2633 crate::ErrorKind::InvalidConfig,
2634 format!("duplicate tool handler registered for name {:?}", tool.name),
2635 ));
2636 }
2637 }
2638 }
2639
2640 let wire_commands = self.commands.as_ref().map(|cmds| {
2641 cmds.iter()
2642 .map(|c| crate::wire::CommandWireDefinition {
2643 name: c.name.clone(),
2644 description: c.description.clone().unwrap_or_default(),
2645 })
2646 .collect()
2647 });
2648 let wire_canvases = self.canvases.clone();
2649 let canvas_handler = self.canvas_handler.clone();
2650 let bearer_token_providers =
2651 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2652
2653 let wire = crate::wire::SessionCreateWire {
2654 session_id,
2655 model: self.model,
2656 allowed_models: self.allowed_models,
2657 client_name: self.client_name,
2658 reasoning_effort: self.reasoning_effort,
2659 reasoning_summary: self.reasoning_summary,
2660 context_tier: self.context_tier,
2661 streaming: self.streaming,
2662 system_message: self.system_message,
2663 ask_user_variant: self.ask_user_variant,
2664 tools: self.tools,
2665 canvases: wire_canvases,
2666 request_canvas_renderer: self.request_canvas_renderer,
2667 request_extensions: self.request_extensions,
2668 extension_sdk_path: self.extension_sdk_path,
2669 extension_info: self.extension_info,
2670 canvas_provider: self.canvas_provider,
2671 available_tools: self.available_tools,
2672 excluded_tools: self.excluded_tools,
2673 excluded_builtin_agents: self.excluded_builtin_agents,
2674 tool_filter_precedence: "excluded",
2675 mcp_servers: self.mcp_servers,
2676 diagnostics: self.diagnostics,
2677 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2678 auth_client_id_metadata_url: self.auth_client_id_metadata_url,
2679 embedding_cache_storage: self.embedding_cache_storage,
2680 env_value_mode: "direct",
2681 enable_config_discovery: self.enable_config_discovery,
2682 skip_embedding_retrieval: self.skip_embedding_retrieval,
2683 organization_custom_instructions: self.organization_custom_instructions,
2684 refresh_custom_instructions: self.refresh_custom_instructions,
2685 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2686 enable_file_hooks: self.enable_file_hooks,
2687 enable_host_git_operations: self.enable_host_git_operations,
2688 enable_session_store: self.enable_session_store,
2689 enable_skills: self.enable_skills,
2690 request_user_input,
2691 request_permission: permission_active,
2692 request_exit_plan_mode,
2693 request_auto_mode_switch,
2694 request_elicitation,
2695 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2696 github_mcp_tool_config: self.github_mcp_tool_config,
2697 hooks: hooks_flag,
2698 skill_directories: self.skill_directories,
2699 instruction_directories: self.instruction_directories,
2700 plugin_directories: self.plugin_directories,
2701 large_output: self.large_output,
2702 tool_search: self.tool_search,
2703 disabled_skills: self.disabled_skills,
2704 disabled_mcp_servers: self.disabled_mcp_servers,
2705 custom_agents: self.custom_agents,
2706 custom_agents_local_only: self.custom_agents_local_only,
2707 default_agent: self.default_agent,
2708 agent: self.agent,
2709 infinite_sessions: self.infinite_sessions,
2710 provider: self.provider,
2711 capi: self.capi,
2712 providers: self.providers,
2713 models: self.models,
2714 enable_session_telemetry: self.enable_session_telemetry,
2715 enable_citations: self.enable_citations,
2716 enable_file_change_tracking: self.enable_file_change_tracking,
2717 session_limits: self.session_limits,
2718 model_capabilities: self.model_capabilities,
2719 memory: self.memory,
2720 config_dir: self.config_directory,
2721 working_directory: self.working_directory,
2722 additional_directories: self.additional_directories,
2723 github_token: self.github_token,
2724 github_token_provider_registration_id: None,
2725 remote_session: self.remote_session,
2726 cloud: self.cloud,
2727 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2728 enable_github_telemetry_forwarding: None,
2729 commands: wire_commands,
2730 feature_flags: self.feature_flags,
2731 exp_assignments: self.exp_assignments,
2732 enable_managed_settings: self.enable_managed_settings,
2733 is_experimental_mode: self.enable_experimental_mode,
2734 managed_settings: self.managed_settings,
2735 };
2736
2737 let runtime = SessionConfigRuntime {
2738 permission_handler: self.permission_handler,
2739 permission_policy: self.permission_policy,
2740 elicitation_handler: self.elicitation_handler,
2741 mcp_auth_handler: self.mcp_auth_handler,
2742 user_input_handler: self.user_input_handler,
2743 exit_plan_mode_handler: self.exit_plan_mode_handler,
2744 auto_mode_switch_handler: self.auto_mode_switch_handler,
2745 hooks_handler: self.hooks_handler,
2746 system_message_transform: self.system_message_transform,
2747 tool_handlers,
2748 canvas_handler,
2749 session_fs_provider: self.session_fs_provider,
2750 bearer_token_providers,
2751 github_token_provider: self.github_token_provider,
2752 commands: self.commands,
2753 };
2754
2755 Ok((wire, runtime))
2756 }
2757
2758 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2762 self.permission_handler = Some(handler);
2763 self
2764 }
2765
2766 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2769 self.elicitation_handler = Some(handler);
2770 self
2771 }
2772
2773 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2775 self.mcp_auth_handler = Some(handler);
2776 self
2777 }
2778
2779 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2782 self.user_input_handler = Some(handler);
2783 self
2784 }
2785
2786 pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self {
2788 self.ask_user_variant = Some(variant);
2789 self
2790 }
2791
2792 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2794 self.exit_plan_mode_handler = Some(handler);
2795 self
2796 }
2797
2798 pub fn with_auto_mode_switch_handler(
2800 mut self,
2801 handler: Arc<dyn AutoModeSwitchHandler>,
2802 ) -> Self {
2803 self.auto_mode_switch_handler = Some(handler);
2804 self
2805 }
2806
2807 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2812 self.commands = Some(commands);
2813 self
2814 }
2815
2816 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2820 self.session_fs_provider = Some(provider);
2821 self
2822 }
2823
2824 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2827 self.hooks_handler = Some(hooks);
2828 self
2829 }
2830
2831 pub fn with_system_message_transform(
2835 mut self,
2836 transform: Arc<dyn SystemMessageTransform>,
2837 ) -> Self {
2838 self.system_message_transform = Some(transform);
2839 self
2840 }
2841
2842 pub fn approve_all_permissions(mut self) -> Self {
2848 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2849 self
2850 }
2851
2852 pub fn deny_all_permissions(mut self) -> Self {
2855 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2856 self
2857 }
2858
2859 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2864 where
2865 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2866 {
2867 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2868 self
2869 }
2870
2871 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2873 self.session_id = Some(id.into());
2874 self
2875 }
2876
2877 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2879 self.model = Some(model.into());
2880 self
2881 }
2882
2883 pub fn with_allowed_models<I, S>(mut self, models: I) -> Self
2888 where
2889 I: IntoIterator<Item = S>,
2890 S: Into<String>,
2891 {
2892 self.allowed_models = Some(models.into_iter().map(Into::into).collect());
2893 self
2894 }
2895
2896 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2898 self.client_name = Some(name.into());
2899 self
2900 }
2901
2902 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2904 self.reasoning_effort = Some(effort.into());
2905 self
2906 }
2907
2908 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2910 self.reasoning_summary = Some(summary);
2911 self
2912 }
2913
2914 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2916 self.context_tier = Some(tier.into());
2917 self
2918 }
2919
2920 pub fn with_streaming(mut self, streaming: bool) -> Self {
2922 self.streaming = Some(streaming);
2923 self
2924 }
2925
2926 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2928 self.system_message = Some(system_message);
2929 self
2930 }
2931
2932 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2934 self.tools = Some(tools.into_iter().collect());
2935 self
2936 }
2937
2938 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2943 self.canvases = Some(canvases.into_iter().collect());
2944 self
2945 }
2946
2947 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2949 self.canvas_handler = Some(handler);
2950 self
2951 }
2952
2953 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2955 self.request_canvas_renderer = Some(request);
2956 self
2957 }
2958
2959 pub fn with_request_extensions(mut self, request: bool) -> Self {
2961 self.request_extensions = Some(request);
2962 self
2963 }
2964
2965 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2969 self.extension_sdk_path = Some(path.into());
2970 self
2971 }
2972
2973 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2975 self.extension_info = Some(extension_info);
2976 self
2977 }
2978
2979 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2982 self.canvas_provider = Some(canvas_provider);
2983 self
2984 }
2985
2986 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2988 where
2989 I: IntoIterator<Item = S>,
2990 S: Into<String>,
2991 {
2992 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2993 self
2994 }
2995
2996 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2998 where
2999 I: IntoIterator<Item = S>,
3000 S: Into<String>,
3001 {
3002 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
3003 self
3004 }
3005
3006 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
3008 where
3009 I: IntoIterator<Item = S>,
3010 S: Into<String>,
3011 {
3012 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
3013 self
3014 }
3015
3016 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
3018 self.mcp_servers = Some(servers);
3019 self
3020 }
3021
3022 pub fn with_diagnostics(mut self, diagnostics: DiagnosticsConfiguration) -> Self {
3024 self.diagnostics = Some(diagnostics);
3025 self
3026 }
3027
3028 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3036 self.mcp_oauth_token_storage = Some(mode.into());
3037 self
3038 }
3039
3040 pub fn with_auth_client_id_metadata_url(mut self, url: impl Into<String>) -> Self {
3042 self.auth_client_id_metadata_url = Some(url.into());
3043 self
3044 }
3045
3046 pub fn with_embedding_cache_storage(
3048 mut self,
3049 embedding_cache_storage: impl Into<String>,
3050 ) -> Self {
3051 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3052 self
3053 }
3054
3055 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3058 self.enable_config_discovery = Some(enable);
3059 self
3060 }
3061
3062 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3064 self.skip_embedding_retrieval = Some(value);
3065 self
3066 }
3067
3068 pub fn with_organization_custom_instructions(
3070 mut self,
3071 instructions: impl Into<String>,
3072 ) -> Self {
3073 self.organization_custom_instructions = Some(instructions.into());
3074 self
3075 }
3076
3077 pub fn with_refresh_custom_instructions(mut self, refresh: bool) -> Self {
3079 self.refresh_custom_instructions = Some(refresh);
3080 self
3081 }
3082
3083 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3085 self.enable_on_demand_instruction_discovery = Some(value);
3086 self
3087 }
3088
3089 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3091 self.enable_file_hooks = Some(value);
3092 self
3093 }
3094
3095 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3097 self.enable_host_git_operations = Some(value);
3098 self
3099 }
3100
3101 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3103 self.enable_session_store = Some(value);
3104 self
3105 }
3106
3107 pub fn with_enable_skills(mut self, value: bool) -> Self {
3109 self.enable_skills = Some(value);
3110 self
3111 }
3112
3113 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3119 self.enable_mcp_apps = Some(enable);
3120 self
3121 }
3122
3123 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
3125 self.github_mcp_tool_config = Some(config);
3126 self
3127 }
3128
3129 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3131 where
3132 I: IntoIterator<Item = P>,
3133 P: Into<PathBuf>,
3134 {
3135 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3136 self
3137 }
3138
3139 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
3141 where
3142 I: IntoIterator<Item = S>,
3143 S: Into<String>,
3144 {
3145 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
3146 self
3147 }
3148
3149 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3153 where
3154 I: IntoIterator<Item = P>,
3155 P: Into<PathBuf>,
3156 {
3157 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3158 self
3159 }
3160
3161 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3163 where
3164 I: IntoIterator<Item = P>,
3165 P: Into<PathBuf>,
3166 {
3167 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3168 self
3169 }
3170
3171 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3173 self.large_output = Some(config);
3174 self
3175 }
3176
3177 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
3180 self.tool_search = Some(config);
3181 self
3182 }
3183
3184 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3186 where
3187 I: IntoIterator<Item = S>,
3188 S: Into<String>,
3189 {
3190 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3191 self
3192 }
3193
3194 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
3196 where
3197 I: IntoIterator<Item = S>,
3198 S: Into<String>,
3199 {
3200 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
3201 self
3202 }
3203
3204 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3206 mut self,
3207 agents: I,
3208 ) -> Self {
3209 self.custom_agents = Some(agents.into_iter().collect());
3210 self
3211 }
3212
3213 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3215 self.default_agent = Some(agent);
3216 self
3217 }
3218
3219 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3222 self.agent = Some(name.into());
3223 self
3224 }
3225
3226 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3229 self.infinite_sessions = Some(config);
3230 self
3231 }
3232
3233 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3235 self.provider = Some(provider);
3236 self
3237 }
3238
3239 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3241 self.capi = Some(capi);
3242 self
3243 }
3244
3245 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3251 self.providers = Some(providers);
3252 self
3253 }
3254
3255 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3261 self.models = Some(models);
3262 self
3263 }
3264
3265 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3269 self.enable_session_telemetry = Some(enable);
3270 self
3271 }
3272
3273 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3275 self.enable_citations = Some(enable);
3276 self
3277 }
3278
3279 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
3282 self.enable_file_change_tracking = Some(enable);
3283 self
3284 }
3285
3286 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3288 self.session_limits = Some(limits);
3289 self
3290 }
3291
3292 pub fn with_model_capabilities(
3294 mut self,
3295 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3296 ) -> Self {
3297 self.model_capabilities = Some(capabilities);
3298 self
3299 }
3300
3301 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3303 self.memory = Some(memory);
3304 self
3305 }
3306
3307 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3309 self.config_directory = Some(dir.into());
3310 self
3311 }
3312
3313 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3316 self.working_directory = Some(dir.into());
3317 self
3318 }
3319
3320 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
3322 where
3323 I: IntoIterator<Item = P>,
3324 P: Into<PathBuf>,
3325 {
3326 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
3327 self
3328 }
3329
3330 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3335 self.github_token = Some(token.into());
3336 self
3337 }
3338
3339 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
3345 self.github_token_provider = Some(provider);
3346 self
3347 }
3348
3349 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3352 self.include_sub_agent_streaming_events = Some(include);
3353 self
3354 }
3355
3356 pub fn with_remote_session(
3358 mut self,
3359 mode: crate::generated::api_types::RemoteSessionMode,
3360 ) -> Self {
3361 self.remote_session = Some(mode);
3362 self
3363 }
3364
3365 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
3367 self.cloud = Some(cloud);
3368 self
3369 }
3370
3371 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3373 self.skip_custom_instructions = Some(value);
3374 self
3375 }
3376
3377 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3379 self.custom_agents_local_only = Some(value);
3380 self
3381 }
3382
3383 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
3385 self.enable_experimental_mode = Some(enable_experimental_mode);
3386 self
3387 }
3388
3389 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3391 self.coauthor_enabled = Some(value);
3392 self
3393 }
3394
3395 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3397 self.manage_schedule_enabled = Some(value);
3398 self
3399 }
3400
3401 pub fn with_feature_flags(mut self, feature_flags: HashMap<String, bool>) -> Self {
3403 self.feature_flags = Some(feature_flags);
3404 self
3405 }
3406
3407 pub fn with_event_buffer_capacity(mut self, capacity: usize) -> Self {
3415 self.event_buffer_capacity = Some(capacity);
3416 self
3417 }
3418
3419 #[doc(hidden)]
3427 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
3428 self.exp_assignments = Some(assignments);
3429 self
3430 }
3431
3432 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3439 self.enable_managed_settings = Some(enabled);
3440 self
3441 }
3442
3443 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
3448 self.managed_settings = Some(managed_settings);
3449 self
3450 }
3451}
3452#[derive(Clone)]
3459#[non_exhaustive]
3460pub struct ResumeSessionConfig {
3461 pub session_id: SessionId,
3463 pub model: Option<String>,
3466 pub allowed_models: Option<Vec<String>>,
3471 pub client_name: Option<String>,
3473 pub reasoning_effort: Option<String>,
3475 pub reasoning_summary: Option<ReasoningSummary>,
3479 pub context_tier: Option<String>,
3482 pub streaming: Option<bool>,
3484 pub system_message: Option<SystemMessageConfig>,
3487 pub ask_user_variant: Option<AskUserVariant>,
3492 pub tools: Option<Vec<Tool>>,
3494 pub canvases: Option<Vec<CanvasDeclaration>>,
3496 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
3499 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3501 pub request_canvas_renderer: Option<bool>,
3503 pub request_extensions: Option<bool>,
3505 pub extension_sdk_path: Option<String>,
3509 pub extension_info: Option<ExtensionInfo>,
3511 pub canvas_provider: Option<CanvasProviderIdentity>,
3514 pub available_tools: Option<Vec<String>>,
3516 pub excluded_tools: Option<Vec<String>>,
3518 pub excluded_builtin_agents: Option<Vec<String>>,
3524 pub included_builtin_skills: Option<Vec<String>>,
3528 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3530 pub diagnostics: Option<DiagnosticsConfiguration>,
3533 pub mcp_oauth_token_storage: Option<String>,
3536 pub auth_client_id_metadata_url: Option<String>,
3542 pub enable_config_discovery: Option<bool>,
3545 pub skip_embedding_retrieval: Option<bool>,
3547 pub embedding_cache_storage: Option<String>,
3549 pub organization_custom_instructions: Option<String>,
3551 pub enable_on_demand_instruction_discovery: Option<bool>,
3553 pub enable_file_hooks: Option<bool>,
3555 pub enable_host_git_operations: Option<bool>,
3557 pub enable_session_store: Option<bool>,
3559 pub enable_skills: Option<bool>,
3561 pub enable_mcp_apps: Option<bool>,
3567 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
3572 pub skill_directories: Option<Vec<PathBuf>>,
3574 pub instruction_directories: Option<Vec<PathBuf>>,
3577 pub plugin_directories: Option<Vec<PathBuf>>,
3579 pub large_output: Option<LargeToolOutputConfig>,
3581 pub tool_search: Option<ToolSearchConfig>,
3584 pub disabled_skills: Option<Vec<String>>,
3586 pub disabled_mcp_servers: Option<Vec<String>>,
3589 pub hooks: Option<bool>,
3591 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3593 pub default_agent: Option<DefaultAgentConfig>,
3595 pub agent: Option<String>,
3597 pub infinite_sessions: Option<InfiniteSessionConfig>,
3599 pub provider: Option<ProviderConfig>,
3601 pub capi: Option<CapiSessionOptions>,
3607 pub providers: Option<Vec<NamedProviderConfig>>,
3613 pub models: Option<Vec<ProviderModelConfig>>,
3619 pub enable_session_telemetry: Option<bool>,
3627 pub enable_citations: Option<bool>,
3629 pub enable_file_change_tracking: Option<bool>,
3633 pub session_limits: Option<SessionLimitsConfig>,
3635 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3637 pub memory: Option<MemoryConfiguration>,
3639 pub config_directory: Option<PathBuf>,
3641 pub working_directory: Option<PathBuf>,
3643 pub additional_directories: Option<Vec<PathBuf>>,
3646 pub github_token: Option<String>,
3649 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
3652 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3655 pub include_sub_agent_streaming_events: Option<bool>,
3657 pub commands: Option<Vec<CommandDefinition>>,
3661 pub feature_flags: Option<HashMap<String, bool>>,
3665 #[doc(hidden)]
3670 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3671 pub enable_managed_settings: Option<bool>,
3677 pub managed_settings: Option<ManagedSettings>,
3683 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3688 pub suppress_resume_event: Option<bool>,
3691 pub continue_pending_work: Option<bool>,
3699 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3702 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3705 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3707 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3710 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3713 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3716 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3718 pub(crate) permission_policy: Option<crate::permission::Policy>,
3720 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3722 pub skip_custom_instructions: Option<bool>,
3724 pub custom_agents_local_only: Option<bool>,
3726 pub enable_experimental_mode: Option<bool>,
3731 pub coauthor_enabled: Option<bool>,
3733 pub manage_schedule_enabled: Option<bool>,
3735 pub event_buffer_capacity: Option<usize>,
3737}
3738
3739impl std::fmt::Debug for ResumeSessionConfig {
3740 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3741 f.debug_struct("ResumeSessionConfig")
3742 .field("session_id", &self.session_id)
3743 .field("model", &self.model)
3744 .field("allowed_models", &self.allowed_models)
3745 .field("client_name", &self.client_name)
3746 .field("reasoning_effort", &self.reasoning_effort)
3747 .field("reasoning_summary", &self.reasoning_summary)
3748 .field("context_tier", &self.context_tier)
3749 .field("streaming", &self.streaming)
3750 .field("system_message", &self.system_message)
3751 .field("ask_user_variant", &self.ask_user_variant)
3752 .field("tools", &self.tools)
3753 .field("canvases", &self.canvases)
3754 .field(
3755 "canvas_handler",
3756 &self.canvas_handler.as_ref().map(|_| "<set>"),
3757 )
3758 .field("open_canvases", &self.open_canvases)
3759 .field("request_canvas_renderer", &self.request_canvas_renderer)
3760 .field("request_extensions", &self.request_extensions)
3761 .field("extension_sdk_path", &self.extension_sdk_path)
3762 .field("extension_info", &self.extension_info)
3763 .field("canvas_provider", &self.canvas_provider)
3764 .field("available_tools", &self.available_tools)
3765 .field("excluded_tools", &self.excluded_tools)
3766 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3767 .field("included_builtin_skills", &self.included_builtin_skills)
3768 .field("mcp_servers", &self.mcp_servers)
3769 .field("diagnostics", &self.diagnostics)
3770 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3771 .field(
3772 "auth_client_id_metadata_url",
3773 &self.auth_client_id_metadata_url,
3774 )
3775 .field("embedding_cache_storage", &self.embedding_cache_storage)
3776 .field("enable_config_discovery", &self.enable_config_discovery)
3777 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3778 .field(
3779 "organization_custom_instructions",
3780 &self
3781 .organization_custom_instructions
3782 .as_ref()
3783 .map(|_| "<redacted>"),
3784 )
3785 .field(
3786 "enable_on_demand_instruction_discovery",
3787 &self.enable_on_demand_instruction_discovery,
3788 )
3789 .field("enable_file_hooks", &self.enable_file_hooks)
3790 .field(
3791 "enable_host_git_operations",
3792 &self.enable_host_git_operations,
3793 )
3794 .field("enable_session_store", &self.enable_session_store)
3795 .field("enable_skills", &self.enable_skills)
3796 .field("enable_mcp_apps", &self.enable_mcp_apps)
3797 .field("skill_directories", &self.skill_directories)
3798 .field("instruction_directories", &self.instruction_directories)
3799 .field("plugin_directories", &self.plugin_directories)
3800 .field("large_output", &self.large_output)
3801 .field("tool_search", &self.tool_search)
3802 .field("disabled_skills", &self.disabled_skills)
3803 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
3804 .field("hooks", &self.hooks)
3805 .field("custom_agents", &self.custom_agents)
3806 .field("default_agent", &self.default_agent)
3807 .field("agent", &self.agent)
3808 .field("infinite_sessions", &self.infinite_sessions)
3809 .field("provider", &self.provider)
3810 .field("capi", &self.capi)
3811 .field("enable_session_telemetry", &self.enable_session_telemetry)
3812 .field("enable_citations", &self.enable_citations)
3813 .field(
3814 "enable_file_change_tracking",
3815 &self.enable_file_change_tracking,
3816 )
3817 .field("session_limits", &self.session_limits)
3818 .field("model_capabilities", &self.model_capabilities)
3819 .field("memory", &self.memory)
3820 .field("config_directory", &self.config_directory)
3821 .field("working_directory", &self.working_directory)
3822 .field("additional_directories", &self.additional_directories)
3823 .field(
3824 "github_token",
3825 &self.github_token.as_ref().map(|_| "<redacted>"),
3826 )
3827 .field(
3828 "github_token_provider",
3829 &self.github_token_provider.as_ref().map(|_| "<set>"),
3830 )
3831 .field("remote_session", &self.remote_session)
3832 .field(
3833 "include_sub_agent_streaming_events",
3834 &self.include_sub_agent_streaming_events,
3835 )
3836 .field("commands", &self.commands)
3837 .field("feature_flags", &self.feature_flags)
3838 .field("exp_assignments", &self.exp_assignments)
3839 .field("enable_managed_settings", &self.enable_managed_settings)
3840 .field("enable_experimental_mode", &self.enable_experimental_mode)
3841 .field("managed_settings", &self.managed_settings)
3842 .field(
3843 "session_fs_provider",
3844 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3845 )
3846 .field(
3847 "permission_handler",
3848 &self.permission_handler.as_ref().map(|_| "<set>"),
3849 )
3850 .field(
3851 "elicitation_handler",
3852 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3853 )
3854 .field(
3855 "user_input_handler",
3856 &self.user_input_handler.as_ref().map(|_| "<set>"),
3857 )
3858 .field(
3859 "exit_plan_mode_handler",
3860 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3861 )
3862 .field(
3863 "auto_mode_switch_handler",
3864 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3865 )
3866 .field(
3867 "hooks_handler",
3868 &self.hooks_handler.as_ref().map(|_| "<set>"),
3869 )
3870 .field(
3871 "system_message_transform",
3872 &self.system_message_transform.as_ref().map(|_| "<set>"),
3873 )
3874 .field("suppress_resume_event", &self.suppress_resume_event)
3875 .field("continue_pending_work", &self.continue_pending_work)
3876 .field("event_buffer_capacity", &self.event_buffer_capacity)
3877 .finish()
3878 }
3879}
3880
3881impl ResumeSessionConfig {
3882 pub(crate) fn into_wire(
3890 mut self,
3891 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3892 if self.github_token.is_some() && self.github_token_provider.is_some() {
3893 return Err(crate::Error::with_message(
3894 crate::ErrorKind::InvalidConfig,
3895 "github_token and github_token_provider are mutually exclusive",
3896 ));
3897 }
3898 let permission_active =
3899 self.permission_handler.is_some() || self.permission_policy.is_some();
3900 let request_user_input = self.user_input_handler.is_some();
3901 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3902 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3903 let request_elicitation = self.elicitation_handler.is_some();
3904 let hooks_flag = self.hooks_handler.is_some();
3905
3906 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3907 if let Some(tools) = self.tools.as_mut() {
3908 for tool in tools.iter_mut() {
3909 if let Some(handler) = tool.handler.take()
3910 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3911 {
3912 return Err(crate::Error::with_message(
3913 crate::ErrorKind::InvalidConfig,
3914 format!("duplicate tool handler registered for name {:?}", tool.name),
3915 ));
3916 }
3917 }
3918 }
3919
3920 let wire_commands = self.commands.as_ref().map(|cmds| {
3921 cmds.iter()
3922 .map(|c| crate::wire::CommandWireDefinition {
3923 name: c.name.clone(),
3924 description: c.description.clone().unwrap_or_default(),
3925 })
3926 .collect()
3927 });
3928 let wire_canvases = self.canvases.clone();
3929 let canvas_handler = self.canvas_handler.clone();
3930 let bearer_token_providers =
3931 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3932
3933 let wire = crate::wire::SessionResumeWire {
3934 session_id: self.session_id,
3935 model: self.model,
3936 allowed_models: self.allowed_models,
3937 client_name: self.client_name,
3938 reasoning_effort: self.reasoning_effort,
3939 reasoning_summary: self.reasoning_summary,
3940 context_tier: self.context_tier,
3941 streaming: self.streaming,
3942 system_message: self.system_message,
3943 ask_user_variant: self.ask_user_variant,
3944 tools: self.tools,
3945 canvases: wire_canvases,
3946 open_canvases: self.open_canvases,
3947 request_canvas_renderer: self.request_canvas_renderer,
3948 request_extensions: self.request_extensions,
3949 extension_sdk_path: self.extension_sdk_path,
3950 extension_info: self.extension_info,
3951 canvas_provider: self.canvas_provider,
3952 available_tools: self.available_tools,
3953 excluded_tools: self.excluded_tools,
3954 excluded_builtin_agents: self.excluded_builtin_agents,
3955 tool_filter_precedence: "excluded",
3956 mcp_servers: self.mcp_servers,
3957 diagnostics: self.diagnostics,
3958 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3959 auth_client_id_metadata_url: self.auth_client_id_metadata_url,
3960 embedding_cache_storage: self.embedding_cache_storage,
3961 env_value_mode: "direct",
3962 enable_config_discovery: self.enable_config_discovery,
3963 skip_embedding_retrieval: self.skip_embedding_retrieval,
3964 organization_custom_instructions: self.organization_custom_instructions,
3965 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3966 enable_file_hooks: self.enable_file_hooks,
3967 enable_host_git_operations: self.enable_host_git_operations,
3968 enable_session_store: self.enable_session_store,
3969 enable_skills: self.enable_skills,
3970 request_user_input,
3971 request_permission: permission_active,
3972 request_exit_plan_mode,
3973 request_auto_mode_switch,
3974 request_elicitation,
3975 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3976 github_mcp_tool_config: self.github_mcp_tool_config,
3977 hooks: hooks_flag,
3978 skill_directories: self.skill_directories,
3979 instruction_directories: self.instruction_directories,
3980 plugin_directories: self.plugin_directories,
3981 large_output: self.large_output,
3982 tool_search: self.tool_search,
3983 disabled_skills: self.disabled_skills,
3984 disabled_mcp_servers: self.disabled_mcp_servers,
3985 custom_agents: self.custom_agents,
3986 custom_agents_local_only: self.custom_agents_local_only,
3987 default_agent: self.default_agent,
3988 agent: self.agent,
3989 infinite_sessions: self.infinite_sessions,
3990 provider: self.provider,
3991 capi: self.capi,
3992 providers: self.providers,
3993 models: self.models,
3994 enable_session_telemetry: self.enable_session_telemetry,
3995 enable_citations: self.enable_citations,
3996 enable_file_change_tracking: self.enable_file_change_tracking,
3997 session_limits: self.session_limits,
3998 model_capabilities: self.model_capabilities,
3999 memory: self.memory,
4000 config_dir: self.config_directory,
4001 working_directory: self.working_directory,
4002 additional_directories: self.additional_directories,
4003 github_token: self.github_token,
4004 github_token_provider_registration_id: None,
4005 remote_session: self.remote_session,
4006 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
4007 enable_github_telemetry_forwarding: None,
4008 commands: wire_commands,
4009 feature_flags: self.feature_flags,
4010 exp_assignments: self.exp_assignments,
4011 enable_managed_settings: self.enable_managed_settings,
4012 is_experimental_mode: self.enable_experimental_mode,
4013 managed_settings: self.managed_settings,
4014 suppress_resume_event: self.suppress_resume_event,
4015 continue_pending_work: self.continue_pending_work,
4016 };
4017
4018 let runtime = SessionConfigRuntime {
4019 permission_handler: self.permission_handler,
4020 permission_policy: self.permission_policy,
4021 elicitation_handler: self.elicitation_handler,
4022 mcp_auth_handler: self.mcp_auth_handler,
4023 user_input_handler: self.user_input_handler,
4024 exit_plan_mode_handler: self.exit_plan_mode_handler,
4025 auto_mode_switch_handler: self.auto_mode_switch_handler,
4026 hooks_handler: self.hooks_handler,
4027 system_message_transform: self.system_message_transform,
4028 tool_handlers,
4029 canvas_handler,
4030 session_fs_provider: self.session_fs_provider,
4031 bearer_token_providers,
4032 github_token_provider: self.github_token_provider,
4033 commands: self.commands,
4034 };
4035
4036 Ok((wire, runtime))
4037 }
4038
4039 pub fn new(session_id: SessionId) -> Self {
4044 Self {
4045 session_id,
4046 model: None,
4047 allowed_models: None,
4048 client_name: None,
4049 reasoning_effort: None,
4050 reasoning_summary: None,
4051 context_tier: None,
4052 streaming: None,
4053 system_message: None,
4054 ask_user_variant: None,
4055 tools: None,
4056 canvases: None,
4057 canvas_handler: None,
4058 open_canvases: None,
4059 request_canvas_renderer: None,
4060 request_extensions: None,
4061 extension_sdk_path: None,
4062 extension_info: None,
4063 canvas_provider: None,
4064 available_tools: None,
4065 excluded_tools: None,
4066 excluded_builtin_agents: None,
4067 included_builtin_skills: None,
4068 mcp_servers: None,
4069 diagnostics: None,
4070 mcp_oauth_token_storage: None,
4071 auth_client_id_metadata_url: None,
4072 enable_config_discovery: None,
4073 skip_embedding_retrieval: None,
4074 organization_custom_instructions: None,
4075 enable_on_demand_instruction_discovery: None,
4076 enable_file_hooks: None,
4077 enable_host_git_operations: None,
4078 enable_session_store: None,
4079 enable_skills: None,
4080 embedding_cache_storage: None,
4081 enable_mcp_apps: None,
4082 github_mcp_tool_config: None,
4083 skill_directories: None,
4084 instruction_directories: None,
4085 plugin_directories: None,
4086 large_output: None,
4087 tool_search: None,
4088 disabled_skills: None,
4089 disabled_mcp_servers: None,
4090 hooks: None,
4091 custom_agents: None,
4092 default_agent: None,
4093 agent: None,
4094 infinite_sessions: None,
4095 provider: None,
4096 capi: None,
4097 providers: None,
4098 models: None,
4099 enable_session_telemetry: None,
4100 enable_citations: None,
4101 enable_file_change_tracking: None,
4102 session_limits: None,
4103 model_capabilities: None,
4104 memory: None,
4105 config_directory: None,
4106 working_directory: None,
4107 additional_directories: None,
4108 github_token: None,
4109 github_token_provider: None,
4110 remote_session: None,
4111 include_sub_agent_streaming_events: None,
4112 commands: None,
4113 feature_flags: None,
4114 exp_assignments: None,
4115 enable_managed_settings: None,
4116 managed_settings: None,
4117 session_fs_provider: None,
4118 suppress_resume_event: None,
4119 continue_pending_work: None,
4120 permission_handler: None,
4121 elicitation_handler: None,
4122 mcp_auth_handler: None,
4123 user_input_handler: None,
4124 exit_plan_mode_handler: None,
4125 auto_mode_switch_handler: None,
4126 hooks_handler: None,
4127 permission_policy: None,
4128 system_message_transform: None,
4129 skip_custom_instructions: None,
4130 custom_agents_local_only: None,
4131 enable_experimental_mode: None,
4132 coauthor_enabled: None,
4133 manage_schedule_enabled: None,
4134 event_buffer_capacity: None,
4135 }
4136 }
4137
4138 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
4140 self.permission_handler = Some(handler);
4141 self
4142 }
4143
4144 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
4146 self.elicitation_handler = Some(handler);
4147 self
4148 }
4149
4150 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
4152 self.mcp_auth_handler = Some(handler);
4153 self
4154 }
4155
4156 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
4158 self.user_input_handler = Some(handler);
4159 self
4160 }
4161
4162 pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self {
4164 self.ask_user_variant = Some(variant);
4165 self
4166 }
4167
4168 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
4170 self.exit_plan_mode_handler = Some(handler);
4171 self
4172 }
4173
4174 pub fn with_auto_mode_switch_handler(
4176 mut self,
4177 handler: Arc<dyn AutoModeSwitchHandler>,
4178 ) -> Self {
4179 self.auto_mode_switch_handler = Some(handler);
4180 self
4181 }
4182
4183 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
4186 self.hooks_handler = Some(hooks);
4187 self
4188 }
4189
4190 pub fn with_system_message_transform(
4192 mut self,
4193 transform: Arc<dyn SystemMessageTransform>,
4194 ) -> Self {
4195 self.system_message_transform = Some(transform);
4196 self
4197 }
4198
4199 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
4203 self.commands = Some(commands);
4204 self
4205 }
4206
4207 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
4210 self.session_fs_provider = Some(provider);
4211 self
4212 }
4213
4214 pub fn approve_all_permissions(mut self) -> Self {
4217 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
4218 self
4219 }
4220
4221 pub fn deny_all_permissions(mut self) -> Self {
4224 self.permission_policy = Some(crate::permission::Policy::DenyAll);
4225 self
4226 }
4227
4228 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
4231 where
4232 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
4233 {
4234 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
4235 self
4236 }
4237
4238 pub fn with_model(mut self, model: impl Into<String>) -> Self {
4240 self.model = Some(model.into());
4241 self
4242 }
4243
4244 pub fn with_allowed_models<I, S>(mut self, models: I) -> Self
4249 where
4250 I: IntoIterator<Item = S>,
4251 S: Into<String>,
4252 {
4253 self.allowed_models = Some(models.into_iter().map(Into::into).collect());
4254 self
4255 }
4256
4257 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
4259 self.client_name = Some(name.into());
4260 self
4261 }
4262
4263 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4265 self.reasoning_effort = Some(effort.into());
4266 self
4267 }
4268
4269 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4271 self.reasoning_summary = Some(summary);
4272 self
4273 }
4274
4275 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
4278 self.context_tier = Some(tier.into());
4279 self
4280 }
4281
4282 pub fn with_streaming(mut self, streaming: bool) -> Self {
4284 self.streaming = Some(streaming);
4285 self
4286 }
4287
4288 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
4291 self.system_message = Some(system_message);
4292 self
4293 }
4294
4295 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
4297 self.tools = Some(tools.into_iter().collect());
4298 self
4299 }
4300
4301 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
4303 self.canvases = Some(canvases.into_iter().collect());
4304 self
4305 }
4306
4307 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
4309 self.canvas_handler = Some(handler);
4310 self
4311 }
4312
4313 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
4315 mut self,
4316 open_canvases: I,
4317 ) -> Self {
4318 self.open_canvases = Some(open_canvases.into_iter().collect());
4319 self
4320 }
4321
4322 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
4324 self.request_canvas_renderer = Some(request);
4325 self
4326 }
4327
4328 pub fn with_request_extensions(mut self, request: bool) -> Self {
4330 self.request_extensions = Some(request);
4331 self
4332 }
4333
4334 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
4338 self.extension_sdk_path = Some(path.into());
4339 self
4340 }
4341
4342 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
4344 self.extension_info = Some(extension_info);
4345 self
4346 }
4347
4348 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
4351 self.canvas_provider = Some(canvas_provider);
4352 self
4353 }
4354
4355 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
4357 where
4358 I: IntoIterator<Item = S>,
4359 S: Into<String>,
4360 {
4361 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
4362 self
4363 }
4364
4365 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
4367 where
4368 I: IntoIterator<Item = S>,
4369 S: Into<String>,
4370 {
4371 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
4372 self
4373 }
4374
4375 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
4377 where
4378 I: IntoIterator<Item = S>,
4379 S: Into<String>,
4380 {
4381 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
4382 self
4383 }
4384
4385 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
4387 self.mcp_servers = Some(servers);
4388 self
4389 }
4390
4391 pub fn with_diagnostics(mut self, diagnostics: DiagnosticsConfiguration) -> Self {
4393 self.diagnostics = Some(diagnostics);
4394 self
4395 }
4396
4397 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
4400 self.mcp_oauth_token_storage = Some(mode.into());
4401 self
4402 }
4403
4404 pub fn with_auth_client_id_metadata_url(mut self, url: impl Into<String>) -> Self {
4406 self.auth_client_id_metadata_url = Some(url.into());
4407 self
4408 }
4409
4410 pub fn with_embedding_cache_storage(
4412 mut self,
4413 embedding_cache_storage: impl Into<String>,
4414 ) -> Self {
4415 self.embedding_cache_storage = Some(embedding_cache_storage.into());
4416 self
4417 }
4418
4419 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
4422 self.enable_config_discovery = Some(enable);
4423 self
4424 }
4425
4426 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
4428 self.skip_embedding_retrieval = Some(value);
4429 self
4430 }
4431
4432 pub fn with_organization_custom_instructions(
4434 mut self,
4435 instructions: impl Into<String>,
4436 ) -> Self {
4437 self.organization_custom_instructions = Some(instructions.into());
4438 self
4439 }
4440
4441 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
4443 self.enable_on_demand_instruction_discovery = Some(value);
4444 self
4445 }
4446
4447 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
4449 self.enable_file_hooks = Some(value);
4450 self
4451 }
4452
4453 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
4455 self.enable_host_git_operations = Some(value);
4456 self
4457 }
4458
4459 pub fn with_enable_session_store(mut self, value: bool) -> Self {
4461 self.enable_session_store = Some(value);
4462 self
4463 }
4464
4465 pub fn with_enable_skills(mut self, value: bool) -> Self {
4467 self.enable_skills = Some(value);
4468 self
4469 }
4470
4471 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
4477 self.enable_mcp_apps = Some(enable);
4478 self
4479 }
4480
4481 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
4483 self.github_mcp_tool_config = Some(config);
4484 self
4485 }
4486
4487 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
4489 where
4490 I: IntoIterator<Item = P>,
4491 P: Into<PathBuf>,
4492 {
4493 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
4494 self
4495 }
4496
4497 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
4499 where
4500 I: IntoIterator<Item = S>,
4501 S: Into<String>,
4502 {
4503 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
4504 self
4505 }
4506
4507 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
4511 where
4512 I: IntoIterator<Item = P>,
4513 P: Into<PathBuf>,
4514 {
4515 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
4516 self
4517 }
4518
4519 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
4521 where
4522 I: IntoIterator<Item = P>,
4523 P: Into<PathBuf>,
4524 {
4525 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
4526 self
4527 }
4528
4529 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
4531 self.large_output = Some(config);
4532 self
4533 }
4534
4535 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
4538 self.tool_search = Some(config);
4539 self
4540 }
4541
4542 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
4544 where
4545 I: IntoIterator<Item = S>,
4546 S: Into<String>,
4547 {
4548 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
4549 self
4550 }
4551
4552 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
4554 where
4555 I: IntoIterator<Item = S>,
4556 S: Into<String>,
4557 {
4558 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
4559 self
4560 }
4561
4562 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
4564 mut self,
4565 agents: I,
4566 ) -> Self {
4567 self.custom_agents = Some(agents.into_iter().collect());
4568 self
4569 }
4570
4571 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
4573 self.default_agent = Some(agent);
4574 self
4575 }
4576
4577 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
4579 self.agent = Some(name.into());
4580 self
4581 }
4582
4583 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
4585 self.infinite_sessions = Some(config);
4586 self
4587 }
4588
4589 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
4591 self.provider = Some(provider);
4592 self
4593 }
4594
4595 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
4597 self.capi = Some(capi);
4598 self
4599 }
4600
4601 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
4607 self.providers = Some(providers);
4608 self
4609 }
4610
4611 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
4617 self.models = Some(models);
4618 self
4619 }
4620
4621 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
4625 self.enable_session_telemetry = Some(enable);
4626 self
4627 }
4628
4629 pub fn with_enable_citations(mut self, enable: bool) -> Self {
4631 self.enable_citations = Some(enable);
4632 self
4633 }
4634
4635 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
4638 self.enable_file_change_tracking = Some(enable);
4639 self
4640 }
4641
4642 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
4644 self.session_limits = Some(limits);
4645 self
4646 }
4647
4648 pub fn with_model_capabilities(
4650 mut self,
4651 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
4652 ) -> Self {
4653 self.model_capabilities = Some(capabilities);
4654 self
4655 }
4656
4657 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
4659 self.memory = Some(memory);
4660 self
4661 }
4662
4663 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4665 self.config_directory = Some(dir.into());
4666 self
4667 }
4668
4669 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4671 self.working_directory = Some(dir.into());
4672 self
4673 }
4674
4675 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
4677 where
4678 I: IntoIterator<Item = P>,
4679 P: Into<PathBuf>,
4680 {
4681 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
4682 self
4683 }
4684
4685 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
4689 self.github_token = Some(token.into());
4690 self
4691 }
4692
4693 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
4699 self.github_token_provider = Some(provider);
4700 self
4701 }
4702
4703 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4705 self.include_sub_agent_streaming_events = Some(include);
4706 self
4707 }
4708
4709 pub fn with_remote_session(
4711 mut self,
4712 mode: crate::generated::api_types::RemoteSessionMode,
4713 ) -> Self {
4714 self.remote_session = Some(mode);
4715 self
4716 }
4717
4718 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4721 self.suppress_resume_event = Some(suppress);
4722 self
4723 }
4724
4725 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4731 self.continue_pending_work = Some(continue_pending);
4732 self
4733 }
4734
4735 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4737 self.skip_custom_instructions = Some(value);
4738 self
4739 }
4740
4741 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4743 self.custom_agents_local_only = Some(value);
4744 self
4745 }
4746
4747 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
4749 self.enable_experimental_mode = Some(enable_experimental_mode);
4750 self
4751 }
4752
4753 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4755 self.coauthor_enabled = Some(value);
4756 self
4757 }
4758
4759 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4761 self.manage_schedule_enabled = Some(value);
4762 self
4763 }
4764
4765 pub fn with_feature_flags(mut self, feature_flags: HashMap<String, bool>) -> Self {
4767 self.feature_flags = Some(feature_flags);
4768 self
4769 }
4770
4771 pub fn with_event_buffer_capacity(mut self, capacity: usize) -> Self {
4779 self.event_buffer_capacity = Some(capacity);
4780 self
4781 }
4782
4783 #[doc(hidden)]
4787 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4788 self.exp_assignments = Some(assignments);
4789 self
4790 }
4791
4792 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4795 self.enable_managed_settings = Some(enabled);
4796 self
4797 }
4798
4799 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
4803 self.managed_settings = Some(managed_settings);
4804 self
4805 }
4806}
4807
4808#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4814#[serde(rename_all = "camelCase")]
4815#[non_exhaustive]
4816pub struct SystemMessageConfig {
4817 #[serde(skip_serializing_if = "Option::is_none")]
4819 pub mode: Option<String>,
4820 #[serde(skip_serializing_if = "Option::is_none")]
4822 pub content: Option<String>,
4823 #[serde(skip_serializing_if = "Option::is_none")]
4825 pub sections: Option<HashMap<String, SectionOverride>>,
4826}
4827
4828impl SystemMessageConfig {
4829 pub fn new() -> Self {
4832 Self::default()
4833 }
4834
4835 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4838 self.mode = Some(mode.into());
4839 self
4840 }
4841
4842 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4845 self.content = Some(content.into());
4846 self
4847 }
4848
4849 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4851 self.sections = Some(sections);
4852 self
4853 }
4854}
4855
4856#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4862#[serde(rename_all = "camelCase")]
4863pub struct SectionOverride {
4864 #[serde(skip_serializing_if = "Option::is_none")]
4867 pub action: Option<String>,
4868 #[serde(skip_serializing_if = "Option::is_none")]
4870 pub content: Option<String>,
4871}
4872
4873#[derive(Debug, Clone, Serialize, Deserialize)]
4875#[serde(rename_all = "camelCase")]
4876pub struct CreateSessionResult {
4877 pub session_id: SessionId,
4879 #[serde(skip_serializing_if = "Option::is_none")]
4881 pub workspace_path: Option<PathBuf>,
4882 #[serde(default, alias = "remote_url")]
4884 pub remote_url: Option<String>,
4885 #[serde(skip_serializing_if = "Option::is_none")]
4887 pub capabilities: Option<SessionCapabilities>,
4888}
4889
4890#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4892#[serde(rename_all = "camelCase")]
4893pub(crate) struct ResumeSessionResult {
4894 #[serde(default)]
4896 pub session_id: Option<SessionId>,
4897 #[serde(default, skip_serializing_if = "Option::is_none")]
4899 pub workspace_path: Option<PathBuf>,
4900 #[serde(default, alias = "remote_url")]
4902 pub remote_url: Option<String>,
4903 #[serde(default, skip_serializing_if = "Option::is_none")]
4905 pub capabilities: Option<SessionCapabilities>,
4906 #[serde(
4908 default,
4909 alias = "openCanvasInstances",
4910 skip_serializing_if = "Option::is_none"
4911 )]
4912 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4913}
4914
4915#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4917#[serde(rename_all = "lowercase")]
4918pub enum LogLevel {
4919 #[default]
4921 Info,
4922 Warning,
4924 Error,
4926}
4927
4928#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4933#[serde(rename_all = "camelCase")]
4934pub struct LogOptions {
4935 #[serde(skip_serializing_if = "Option::is_none")]
4937 pub level: Option<LogLevel>,
4938 #[serde(skip_serializing_if = "Option::is_none")]
4941 pub ephemeral: Option<bool>,
4942}
4943
4944impl LogOptions {
4945 pub fn with_level(mut self, level: LogLevel) -> Self {
4947 self.level = Some(level);
4948 self
4949 }
4950
4951 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4953 self.ephemeral = Some(ephemeral);
4954 self
4955 }
4956}
4957
4958#[derive(Debug, Clone, Default)]
4962pub struct SetModelOptions {
4963 pub reasoning_effort: Option<String>,
4966 pub reasoning_summary: Option<ReasoningSummary>,
4970 pub context_tier: Option<ContextTier>,
4973 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4977 pub auto_tier: Option<AutoTierPreference>,
4985}
4986
4987#[derive(Debug, Clone, PartialEq, Eq)]
4996pub enum AutoTierPreference {
4997 Tier(AutoTier),
4999 Reset,
5001}
5002
5003impl SetModelOptions {
5004 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
5006 self.reasoning_effort = Some(effort.into());
5007 self
5008 }
5009
5010 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
5012 self.reasoning_summary = Some(summary);
5013 self
5014 }
5015
5016 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
5018 self.context_tier = Some(tier);
5019 self
5020 }
5021
5022 pub fn with_model_capabilities(
5024 mut self,
5025 caps: crate::generated::api_types::ModelCapabilitiesOverride,
5026 ) -> Self {
5027 self.model_capabilities = Some(caps);
5028 self
5029 }
5030
5031 pub fn with_auto_tier(mut self, tier: AutoTier) -> Self {
5033 self.auto_tier = Some(AutoTierPreference::Tier(tier));
5034 self
5035 }
5036
5037 pub fn with_reset_auto_tier(mut self) -> Self {
5040 self.auto_tier = Some(AutoTierPreference::Reset);
5041 self
5042 }
5043}
5044
5045#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
5052#[serde(rename_all = "camelCase")]
5053pub struct PingResponse {
5054 #[serde(default)]
5056 pub message: String,
5057 #[serde(default)]
5059 pub timestamp: String,
5060 #[serde(skip_serializing_if = "Option::is_none")]
5062 pub protocol_version: Option<u32>,
5063}
5064
5065#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5067#[serde(rename_all = "camelCase")]
5068pub struct AttachmentLineRange {
5069 pub start: u32,
5071 pub end: u32,
5073}
5074
5075#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5077#[serde(rename_all = "camelCase")]
5078pub struct AttachmentSelectionPosition {
5079 pub line: u32,
5081 pub character: u32,
5083}
5084
5085#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5087#[serde(rename_all = "camelCase")]
5088pub struct AttachmentSelectionRange {
5089 pub start: AttachmentSelectionPosition,
5091 pub end: AttachmentSelectionPosition,
5093}
5094
5095#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5097#[serde(rename_all = "snake_case")]
5098#[non_exhaustive]
5099pub enum GitHubReferenceType {
5100 Issue,
5102 Pr,
5104 Discussion,
5106}
5107
5108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5114#[serde(rename_all = "camelCase")]
5115pub struct GitHubRepoPointer {
5116 #[serde(skip_serializing_if = "Option::is_none")]
5118 pub id: Option<i64>,
5119 pub name: String,
5121 pub owner: String,
5123}
5124
5125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5127#[serde(rename_all = "camelCase")]
5128pub struct GitHubFileDiffSide {
5129 pub path: String,
5131 pub r#ref: String,
5133 pub repo: GitHubRepoPointer,
5135}
5136
5137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5139#[serde(rename_all = "camelCase")]
5140pub struct GitHubTreeComparisonSide {
5141 pub repo: GitHubRepoPointer,
5143 pub revision: String,
5145}
5146
5147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5149#[serde(rename_all = "camelCase")]
5150pub struct GitHubSnippetLineRange {
5151 pub start: i64,
5153 pub end: i64,
5155}
5156
5157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5159#[serde(
5160 tag = "type",
5161 rename_all = "camelCase",
5162 rename_all_fields = "camelCase"
5163)]
5164#[non_exhaustive]
5165pub enum Attachment {
5166 File {
5168 path: PathBuf,
5170 #[serde(skip_serializing_if = "Option::is_none")]
5172 display_name: Option<String>,
5173 #[serde(skip_serializing_if = "Option::is_none")]
5175 line_range: Option<AttachmentLineRange>,
5176 },
5177 Directory {
5179 path: PathBuf,
5181 #[serde(skip_serializing_if = "Option::is_none")]
5183 display_name: Option<String>,
5184 },
5185 Selection {
5187 file_path: PathBuf,
5189 text: String,
5191 #[serde(skip_serializing_if = "Option::is_none")]
5193 display_name: Option<String>,
5194 selection: AttachmentSelectionRange,
5196 },
5197 Blob {
5199 data: String,
5201 mime_type: String,
5203 #[serde(skip_serializing_if = "Option::is_none")]
5205 display_name: Option<String>,
5206 },
5207 #[serde(rename = "extension_context")]
5209 ExtensionContext {
5210 captured_at: String,
5212 extension_id: String,
5214 #[serde(skip_serializing_if = "Option::is_none")]
5216 canvas_id: Option<String>,
5217 #[serde(skip_serializing_if = "Option::is_none")]
5219 instance_id: Option<String>,
5220 title: String,
5222 #[serde(skip_serializing_if = "Option::is_none")]
5224 payload: Option<Value>,
5225 },
5226 #[serde(rename = "github_reference")]
5228 GitHubReference {
5229 number: u64,
5231 title: String,
5233 reference_type: GitHubReferenceType,
5235 state: String,
5237 url: String,
5239 },
5240 #[serde(rename = "github_commit")]
5242 GitHubCommit {
5243 message: String,
5245 oid: String,
5247 repo: GitHubRepoPointer,
5249 url: String,
5251 },
5252 #[serde(rename = "github_release")]
5254 GitHubRelease {
5255 name: String,
5257 repo: GitHubRepoPointer,
5259 tag_name: String,
5261 url: String,
5263 },
5264 #[serde(rename = "github_actions_job")]
5266 GitHubActionsJob {
5267 #[serde(skip_serializing_if = "Option::is_none")]
5270 conclusion: Option<String>,
5271 job_id: i64,
5273 job_name: String,
5275 repo: GitHubRepoPointer,
5277 url: String,
5279 workflow_name: String,
5281 },
5282 #[serde(rename = "github_repository")]
5284 GitHubRepository {
5285 #[serde(skip_serializing_if = "Option::is_none")]
5287 description: Option<String>,
5288 #[serde(skip_serializing_if = "Option::is_none")]
5291 r#ref: Option<String>,
5292 repo: GitHubRepoPointer,
5294 url: String,
5296 },
5297 #[serde(rename = "github_file_diff")]
5299 GitHubFileDiff {
5300 #[serde(skip_serializing_if = "Option::is_none")]
5302 base: Option<GitHubFileDiffSide>,
5303 #[serde(skip_serializing_if = "Option::is_none")]
5305 head: Option<GitHubFileDiffSide>,
5306 url: String,
5308 },
5309 #[serde(rename = "github_tree_comparison")]
5311 GitHubTreeComparison {
5312 base: GitHubTreeComparisonSide,
5314 head: GitHubTreeComparisonSide,
5316 url: String,
5318 },
5319 #[serde(rename = "github_url")]
5321 GitHubUrl {
5322 url: String,
5324 },
5325 #[serde(rename = "github_file")]
5327 GitHubFile {
5328 path: String,
5330 r#ref: String,
5332 repo: GitHubRepoPointer,
5334 url: String,
5336 },
5337 #[serde(rename = "github_snippet")]
5339 GitHubSnippet {
5340 line_range: GitHubSnippetLineRange,
5342 path: String,
5344 r#ref: String,
5346 repo: GitHubRepoPointer,
5348 url: String,
5350 },
5351}
5352
5353impl Attachment {
5354 pub fn display_name(&self) -> Option<&str> {
5356 match self {
5357 Self::File { display_name, .. }
5358 | Self::Directory { display_name, .. }
5359 | Self::Selection { display_name, .. }
5360 | Self::Blob { display_name, .. } => display_name.as_deref(),
5361 Self::GitHubReference { .. }
5362 | Self::GitHubCommit { .. }
5363 | Self::GitHubRelease { .. }
5364 | Self::GitHubActionsJob { .. }
5365 | Self::GitHubRepository { .. }
5366 | Self::GitHubFileDiff { .. }
5367 | Self::GitHubTreeComparison { .. }
5368 | Self::GitHubUrl { .. }
5369 | Self::GitHubFile { .. }
5370 | Self::GitHubSnippet { .. }
5371 | Self::ExtensionContext { .. } => None,
5372 }
5373 }
5374
5375 pub fn label(&self) -> Option<String> {
5377 if let Some(display_name) = self
5378 .display_name()
5379 .map(str::trim)
5380 .filter(|name| !name.is_empty())
5381 {
5382 return Some(display_name.to_string());
5383 }
5384
5385 match self {
5386 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
5387 format!("#{}", number)
5388 } else {
5389 title.trim().to_string()
5390 }),
5391 Self::ExtensionContext { title, .. } if !title.trim().is_empty() => {
5392 Some(title.trim().to_string())
5393 }
5394 _ => self.derived_display_name(),
5395 }
5396 }
5397
5398 pub fn ensure_display_name(&mut self) {
5400 if self
5401 .display_name()
5402 .map(str::trim)
5403 .is_some_and(|name| !name.is_empty())
5404 {
5405 return;
5406 }
5407
5408 let Some(derived_display_name) = self.derived_display_name() else {
5409 return;
5410 };
5411
5412 match self {
5413 Self::File { display_name, .. }
5414 | Self::Directory { display_name, .. }
5415 | Self::Selection { display_name, .. }
5416 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
5417 Self::GitHubReference { .. }
5418 | Self::GitHubCommit { .. }
5419 | Self::GitHubRelease { .. }
5420 | Self::GitHubActionsJob { .. }
5421 | Self::GitHubRepository { .. }
5422 | Self::GitHubFileDiff { .. }
5423 | Self::GitHubTreeComparison { .. }
5424 | Self::GitHubUrl { .. }
5425 | Self::GitHubFile { .. }
5426 | Self::GitHubSnippet { .. }
5427 | Self::ExtensionContext { .. } => {}
5428 }
5429 }
5430
5431 fn derived_display_name(&self) -> Option<String> {
5432 match self {
5433 Self::File { path, .. } | Self::Directory { path, .. } => {
5434 Some(attachment_name_from_path(path))
5435 }
5436 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
5437 Self::Blob { .. } => Some("attachment".to_string()),
5438 Self::GitHubReference { .. }
5439 | Self::GitHubCommit { .. }
5440 | Self::GitHubRelease { .. }
5441 | Self::GitHubActionsJob { .. }
5442 | Self::GitHubRepository { .. }
5443 | Self::GitHubFileDiff { .. }
5444 | Self::GitHubTreeComparison { .. }
5445 | Self::GitHubUrl { .. }
5446 | Self::GitHubFile { .. }
5447 | Self::GitHubSnippet { .. }
5448 | Self::ExtensionContext { .. } => None,
5449 }
5450 }
5451}
5452
5453fn attachment_name_from_path(path: &Path) -> String {
5454 path.file_name()
5455 .map(|name| name.to_string_lossy().into_owned())
5456 .filter(|name| !name.is_empty())
5457 .unwrap_or_else(|| {
5458 let full = path.to_string_lossy();
5459 if full.is_empty() {
5460 "attachment".to_string()
5461 } else {
5462 full.into_owned()
5463 }
5464 })
5465}
5466
5467pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
5469 for attachment in attachments {
5470 attachment.ensure_display_name();
5471 }
5472}
5473
5474#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5479#[non_exhaustive]
5480pub enum MessageSource {
5481 User,
5483 System,
5485 Agent(String),
5487}
5488
5489impl std::fmt::Display for MessageSource {
5490 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5491 match self {
5492 Self::User => f.write_str("user"),
5493 Self::System => f.write_str("system"),
5494 Self::Agent(id) => write!(f, "agent-{id}"),
5495 }
5496 }
5497}
5498
5499impl Serialize for MessageSource {
5500 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
5501 serializer.collect_str(self)
5502 }
5503}
5504
5505impl<'de> Deserialize<'de> for MessageSource {
5506 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
5507 let value = String::deserialize(deserializer)?;
5508 match value.as_str() {
5509 "user" => Ok(Self::User),
5510 "system" => Ok(Self::System),
5511 value => value
5512 .strip_prefix("agent-")
5513 .map(|id| Self::Agent(id.to_owned()))
5514 .ok_or_else(|| serde::de::Error::custom("expected user, system, or agent-<id>")),
5515 }
5516 }
5517}
5518
5519#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5524#[serde(rename_all = "lowercase")]
5525#[non_exhaustive]
5526pub enum DeliveryMode {
5527 Enqueue,
5529 Immediate,
5531}
5532
5533#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5538#[serde(rename_all = "lowercase")]
5539#[non_exhaustive]
5540pub enum AgentMode {
5541 Interactive,
5543 Plan,
5545 Autopilot,
5547 Shell,
5549}
5550
5551#[derive(Debug, Clone)]
5580#[non_exhaustive]
5581pub struct MessageOptions {
5582 pub response_schema: Option<Value>,
5585 pub prompt: String,
5587 pub source: Option<MessageSource>,
5590 pub mode: Option<DeliveryMode>,
5596 pub agent_mode: Option<AgentMode>,
5600 pub attachments: Option<Vec<Attachment>>,
5602 pub wait_timeout: Option<Duration>,
5605 pub request_headers: Option<HashMap<String, String>>,
5609 pub traceparent: Option<String>,
5616 pub tracestate: Option<String>,
5620 pub display_prompt: Option<String>,
5622}
5623
5624impl MessageOptions {
5625 pub fn new(prompt: impl Into<String>) -> Self {
5627 Self {
5628 prompt: prompt.into(),
5629 response_schema: None,
5630 source: None,
5631 mode: None,
5632 agent_mode: None,
5633 attachments: None,
5634 wait_timeout: None,
5635 request_headers: None,
5636 traceparent: None,
5637 tracestate: None,
5638 display_prompt: None,
5639 }
5640 }
5641
5642 pub fn with_source(mut self, source: MessageSource) -> Self {
5644 self.source = Some(source);
5645 self
5646 }
5647
5648 pub fn with_response_schema(mut self, schema: Value) -> Self {
5650 self.response_schema = Some(schema);
5651 self
5652 }
5653
5654 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
5660 self.mode = Some(mode);
5661 self
5662 }
5663
5664 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
5668 self.agent_mode = Some(agent_mode);
5669 self
5670 }
5671
5672 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
5674 self.attachments = Some(attachments);
5675 self
5676 }
5677
5678 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
5680 self.wait_timeout = Some(timeout);
5681 self
5682 }
5683
5684 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
5686 self.request_headers = Some(headers);
5687 self
5688 }
5689
5690 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
5695 self.traceparent = ctx.traceparent;
5696 self.tracestate = ctx.tracestate;
5697 self
5698 }
5699
5700 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5702 self.traceparent = Some(traceparent.into());
5703 self
5704 }
5705
5706 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5708 self.tracestate = Some(tracestate.into());
5709 self
5710 }
5711
5712 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5714 self.display_prompt = Some(display_prompt.into());
5715 self
5716 }
5717}
5718
5719impl From<&str> for MessageOptions {
5720 fn from(prompt: &str) -> Self {
5721 Self::new(prompt)
5722 }
5723}
5724
5725impl From<String> for MessageOptions {
5726 fn from(prompt: String) -> Self {
5727 Self::new(prompt)
5728 }
5729}
5730
5731impl From<&String> for MessageOptions {
5732 fn from(prompt: &String) -> Self {
5733 Self::new(prompt.clone())
5734 }
5735}
5736
5737#[derive(Debug, Clone, Serialize, Deserialize)]
5739#[serde(rename_all = "camelCase")]
5740#[non_exhaustive]
5741pub struct GetStatusResponse {
5742 pub version: String,
5744 pub protocol_version: u32,
5746}
5747
5748#[derive(Debug, Clone, Serialize, Deserialize)]
5750#[serde(rename_all = "camelCase")]
5751#[non_exhaustive]
5752pub struct GetAuthStatusResponse {
5753 pub is_authenticated: bool,
5755 #[serde(skip_serializing_if = "Option::is_none")]
5758 pub auth_type: Option<String>,
5759 #[serde(skip_serializing_if = "Option::is_none")]
5761 pub host: Option<String>,
5762 #[serde(skip_serializing_if = "Option::is_none")]
5764 pub login: Option<String>,
5765 #[serde(skip_serializing_if = "Option::is_none")]
5767 pub status_message: Option<String>,
5768}
5769
5770#[derive(Debug, Clone, Serialize, Deserialize)]
5774#[serde(rename_all = "camelCase")]
5775pub struct SessionEventNotification {
5776 pub session_id: SessionId,
5778 pub event: SessionEvent,
5780}
5781
5782#[derive(Debug, Clone, Serialize, Deserialize)]
5789#[serde(rename_all = "camelCase")]
5790pub struct SessionEvent {
5791 pub id: String,
5793 pub timestamp: String,
5795 pub parent_id: Option<String>,
5797 #[serde(skip_serializing_if = "Option::is_none")]
5799 pub ephemeral: Option<bool>,
5800 #[serde(skip_serializing_if = "Option::is_none")]
5803 pub agent_id: Option<String>,
5804 #[serde(skip_serializing_if = "Option::is_none")]
5806 pub debug_cli_received_at_ms: Option<i64>,
5807 #[serde(skip_serializing_if = "Option::is_none")]
5809 pub debug_ws_forwarded_at_ms: Option<i64>,
5810 #[serde(rename = "type")]
5812 pub event_type: String,
5813 pub data: Value,
5815}
5816
5817impl SessionEvent {
5818 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5823 use serde::de::IntoDeserializer;
5824 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5825 self.event_type.as_str().into_deserializer();
5826 crate::generated::SessionEventType::deserialize(deserializer)
5827 .unwrap_or(crate::generated::SessionEventType::Unknown)
5828 }
5829
5830 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5836 serde_json::from_value(self.data.clone()).ok()
5837 }
5838
5839 pub fn is_transient_error(&self) -> bool {
5843 self.event_type == "session.error"
5844 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5845 }
5846}
5847
5848#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5853#[serde(rename_all = "camelCase")]
5854#[non_exhaustive]
5855pub struct ToolInvocation {
5856 pub session_id: SessionId,
5858 pub tool_call_id: String,
5860 pub tool_name: String,
5862 pub arguments: Value,
5864 #[serde(skip)]
5872 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5873 #[serde(default, skip_serializing_if = "Option::is_none")]
5878 pub traceparent: Option<String>,
5879 #[serde(default, skip_serializing_if = "Option::is_none")]
5882 pub tracestate: Option<String>,
5883}
5884
5885impl ToolInvocation {
5886 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5907 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5908 }
5909
5910 pub fn trace_context(&self) -> TraceContext {
5913 TraceContext {
5914 traceparent: self.traceparent.clone(),
5915 tracestate: self.tracestate.clone(),
5916 }
5917 }
5918}
5919
5920#[derive(Debug, Clone, Serialize, Deserialize)]
5922#[serde(rename_all = "camelCase")]
5923pub struct ToolBinaryResult {
5924 pub data: String,
5926 pub mime_type: String,
5928 pub r#type: String,
5930 #[serde(default, skip_serializing_if = "Option::is_none")]
5932 pub description: Option<String>,
5933}
5934
5935#[derive(Debug, Clone, Serialize, Deserialize)]
5942#[serde(rename_all = "camelCase")]
5943#[non_exhaustive]
5944pub struct ToolResultExpanded {
5945 pub text_result_for_llm: String,
5947 pub result_type: String,
5949 #[serde(default, skip_serializing_if = "Option::is_none")]
5951 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5952 #[serde(skip_serializing_if = "Option::is_none")]
5954 pub session_log: Option<String>,
5955 #[serde(skip_serializing_if = "Option::is_none")]
5957 pub error: Option<String>,
5958 #[serde(default, skip_serializing_if = "Option::is_none")]
5960 pub tool_telemetry: Option<HashMap<String, Value>>,
5961 #[serde(default, skip_serializing_if = "Option::is_none")]
5963 pub tool_references: Option<Vec<String>>,
5964}
5965
5966impl ToolResultExpanded {
5967 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5971 Self {
5972 text_result_for_llm: text_result_for_llm.into(),
5973 result_type: result_type.into(),
5974 binary_results_for_llm: None,
5975 session_log: None,
5976 error: None,
5977 tool_telemetry: None,
5978 tool_references: None,
5979 }
5980 }
5981
5982 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5984 self.binary_results_for_llm = Some(results);
5985 self
5986 }
5987
5988 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5990 self.session_log = Some(session_log.into());
5991 self
5992 }
5993
5994 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5996 self.error = Some(error.into());
5997 self
5998 }
5999
6000 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
6002 self.tool_telemetry = Some(telemetry);
6003 self
6004 }
6005
6006 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
6008 where
6009 I: IntoIterator<Item = S>,
6010 S: Into<String>,
6011 {
6012 self.tool_references = Some(references.into_iter().map(Into::into).collect());
6013 self
6014 }
6015}
6016
6017#[derive(Debug, Clone, Serialize, Deserialize)]
6019#[serde(untagged)]
6020#[non_exhaustive]
6021pub enum ToolResult {
6022 Text(String),
6024 Expanded(ToolResultExpanded),
6026}
6027
6028#[derive(Debug, Clone, Serialize, Deserialize)]
6030#[serde(rename_all = "camelCase")]
6031pub struct ToolResultResponse {
6032 pub result: ToolResult,
6034}
6035
6036#[derive(Debug, Clone, Serialize, Deserialize)]
6038#[serde(rename_all = "camelCase")]
6039pub struct SessionMetadata {
6040 pub session_id: SessionId,
6042 pub start_time: String,
6044 pub modified_time: String,
6046 #[serde(skip_serializing_if = "Option::is_none")]
6048 pub summary: Option<String>,
6049 pub is_remote: bool,
6051}
6052
6053#[derive(Debug, Clone, Serialize, Deserialize)]
6055#[serde(rename_all = "camelCase")]
6056pub struct ListSessionsResponse {
6057 pub sessions: Vec<SessionMetadata>,
6059}
6060
6061#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6065#[serde(rename_all = "camelCase")]
6066pub struct SessionListFilter {
6067 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
6069 pub working_directory: Option<String>,
6070 #[serde(default, skip_serializing_if = "Option::is_none")]
6072 pub git_root: Option<String>,
6073 #[serde(default, skip_serializing_if = "Option::is_none")]
6075 pub repository: Option<String>,
6076 #[serde(default, skip_serializing_if = "Option::is_none")]
6078 pub branch: Option<String>,
6079}
6080
6081#[derive(Debug, Clone, Serialize, Deserialize)]
6083#[serde(rename_all = "camelCase")]
6084pub struct GetSessionMetadataResponse {
6085 #[serde(skip_serializing_if = "Option::is_none")]
6087 pub session: Option<SessionMetadata>,
6088}
6089
6090#[derive(Debug, Clone, Serialize, Deserialize)]
6092#[serde(rename_all = "camelCase")]
6093pub struct GetLastSessionIdResponse {
6094 #[serde(skip_serializing_if = "Option::is_none")]
6096 pub session_id: Option<SessionId>,
6097}
6098
6099#[derive(Debug, Clone, Serialize, Deserialize)]
6101#[serde(rename_all = "camelCase")]
6102pub struct GetForegroundSessionResponse {
6103 #[serde(skip_serializing_if = "Option::is_none")]
6105 pub session_id: Option<SessionId>,
6106}
6107
6108#[derive(Debug, Clone, Serialize, Deserialize)]
6110#[serde(rename_all = "camelCase")]
6111pub struct GetMessagesResponse {
6112 pub events: Vec<SessionEvent>,
6114}
6115
6116#[derive(Debug, Clone, Serialize, Deserialize)]
6118#[serde(rename_all = "camelCase")]
6119pub struct ElicitationResult {
6120 pub action: String,
6122 #[serde(skip_serializing_if = "Option::is_none")]
6124 pub content: Option<Value>,
6125}
6126
6127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6133#[serde(rename_all = "camelCase")]
6134#[non_exhaustive]
6135pub enum ElicitationMode {
6136 Form,
6138 Url,
6140 #[serde(other)]
6142 Unknown,
6143}
6144
6145#[derive(Debug, Clone, Serialize, Deserialize)]
6152#[serde(rename_all = "camelCase")]
6153pub struct ElicitationRequest {
6154 pub message: String,
6156 #[serde(skip_serializing_if = "Option::is_none")]
6158 pub requested_schema: Option<Value>,
6159 #[serde(skip_serializing_if = "Option::is_none")]
6161 pub mode: Option<ElicitationMode>,
6162 #[serde(skip_serializing_if = "Option::is_none")]
6164 pub elicitation_source: Option<String>,
6165 #[serde(skip_serializing_if = "Option::is_none")]
6167 pub url: Option<String>,
6168}
6169
6170#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6175#[serde(rename_all = "camelCase")]
6176pub struct SessionCapabilities {
6177 #[serde(skip_serializing_if = "Option::is_none")]
6179 pub ui: Option<UiCapabilities>,
6180}
6181
6182#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6184#[serde(rename_all = "camelCase")]
6185pub struct UiCapabilities {
6186 #[serde(skip_serializing_if = "Option::is_none")]
6188 pub elicitation: Option<bool>,
6189 #[serde(skip_serializing_if = "Option::is_none")]
6200 pub mcp_apps: Option<bool>,
6201 #[serde(skip_serializing_if = "Option::is_none")]
6203 pub canvases: Option<bool>,
6204}
6205
6206#[derive(Debug, Clone, Default)]
6208pub struct UiInputOptions<'a> {
6209 pub title: Option<&'a str>,
6211 pub description: Option<&'a str>,
6213 pub min_length: Option<u64>,
6215 pub max_length: Option<u64>,
6217 pub format: Option<InputFormat>,
6219 pub default: Option<&'a str>,
6221}
6222
6223#[derive(Debug, Clone, Copy)]
6225#[non_exhaustive]
6226pub enum InputFormat {
6227 Email,
6229 Uri,
6231 Date,
6233 DateTime,
6235}
6236
6237impl InputFormat {
6238 pub fn as_str(&self) -> &'static str {
6240 match self {
6241 Self::Email => "email",
6242 Self::Uri => "uri",
6243 Self::Date => "date",
6244 Self::DateTime => "date-time",
6245 }
6246 }
6247}
6248
6249pub use crate::generated::api_types::{
6254 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
6255 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
6256 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
6257 PermissionDecisionApproveOnce, PermissionDecisionContext, PermissionDecisionOutcome,
6258 PermissionDecisionReject, PermissionDecisionSource, PermissionDecisionSurface,
6259 PermissionDecisionUserNotAvailable, PermissionResponseCapability,
6260};
6261
6262#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6268#[serde(rename_all = "kebab-case")]
6269#[non_exhaustive]
6270pub enum PermissionRequestKind {
6271 Shell,
6273 Write,
6275 Read,
6277 Url,
6279 Mcp,
6281 CustomTool,
6283 Memory,
6285 Hook,
6287 #[serde(other)]
6290 Unknown,
6291}
6292
6293#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6299#[serde(rename_all = "camelCase")]
6300pub struct PermissionRequestData {
6301 #[serde(default, skip_serializing_if = "Option::is_none")]
6305 pub kind: Option<PermissionRequestKind>,
6306 #[serde(default, skip_serializing_if = "Option::is_none")]
6309 pub tool_call_id: Option<String>,
6310 #[serde(default, skip_serializing_if = "Option::is_none")]
6312 pub managed_approval_required: Option<bool>,
6313 #[serde(default, skip_serializing_if = "is_false")]
6315 pub managed_settings_enabled: bool,
6316 #[serde(flatten)]
6320 pub extra: Value,
6321}
6322
6323#[derive(Debug, Clone, Serialize, Deserialize)]
6325#[serde(rename_all = "camelCase")]
6326pub struct ExitPlanModeData {
6327 #[serde(default)]
6329 pub summary: String,
6330 #[serde(default, skip_serializing_if = "Option::is_none")]
6332 pub plan_content: Option<String>,
6333 #[serde(default)]
6335 pub actions: Vec<String>,
6336 #[serde(default = "default_recommended_action")]
6338 pub recommended_action: String,
6339}
6340
6341fn default_recommended_action() -> String {
6342 "autopilot".to_string()
6343}
6344
6345impl Default for ExitPlanModeData {
6346 fn default() -> Self {
6347 Self {
6348 summary: String::new(),
6349 plan_content: None,
6350 actions: Vec::new(),
6351 recommended_action: default_recommended_action(),
6352 }
6353 }
6354}
6355
6356#[cfg(test)]
6357mod tests {
6358 use std::collections::HashMap;
6359 use std::path::PathBuf;
6360
6361 use serde_json::json;
6362
6363 use super::{
6364 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
6365 AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions,
6366 ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode,
6367 ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
6368 InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
6369 MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig,
6370 ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
6371 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
6372 ToolResultResponse, ensure_attachment_display_names,
6373 };
6374 use crate::generated::session_events::TypedSessionEvent;
6375
6376 #[test]
6377 fn permission_response_capability_is_publicly_exported() {
6378 assert_eq!(
6379 serde_json::to_value(PermissionResponseCapability::Interactive).unwrap(),
6380 json!("interactive")
6381 );
6382 }
6383
6384 #[test]
6385 fn tool_builder_composes() {
6386 let tool = Tool::new("greet")
6387 .with_description("Say hello")
6388 .with_namespaced_name("hello/greet")
6389 .with_instructions("Pass the user's name")
6390 .with_parameters(json!({
6391 "type": "object",
6392 "properties": { "name": { "type": "string" } },
6393 "required": ["name"]
6394 }))
6395 .with_overrides_built_in_tool(true)
6396 .with_skip_permission(true);
6397 assert_eq!(tool.name, "greet");
6398 assert_eq!(tool.description, "Say hello");
6399 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
6400 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
6401 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
6402 assert!(tool.overrides_built_in_tool);
6403 assert!(tool.skip_permission);
6404 }
6405
6406 #[test]
6407 fn tool_defer_serialization() {
6408 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
6409 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
6410 let value = serde_json::to_value(&tool).unwrap();
6411 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
6412
6413 let plain = Tool::new("plain");
6414 let value = serde_json::to_value(&plain).unwrap();
6415 assert!(value.get("defer").is_none());
6416 }
6417
6418 #[test]
6419 fn tool_metadata_serialization() {
6420 use indexmap::IndexMap;
6421
6422 let mut metadata = IndexMap::new();
6423 metadata.insert(
6424 "github.com/copilot:safeForTelemetry".to_string(),
6425 json!({ "name": true, "inputsNames": false }),
6426 );
6427 let tool = Tool::new("lookup").with_metadata(metadata);
6428 let value = serde_json::to_value(&tool).unwrap();
6429 assert_eq!(
6430 value
6431 .get("metadata")
6432 .unwrap()
6433 .get("github.com/copilot:safeForTelemetry")
6434 .unwrap(),
6435 &json!({ "name": true, "inputsNames": false })
6436 );
6437
6438 let plain = Tool::new("plain");
6440 let value = serde_json::to_value(&plain).unwrap();
6441 assert!(value.get("metadata").is_none());
6442 }
6443
6444 #[test]
6445 fn custom_agent_config_builder_with_model() {
6446 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
6447 .with_model("claude-haiku-4.5")
6448 .with_display_name("My Agent");
6449 assert_eq!(agent.name, "my-agent");
6450 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
6451 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
6452 }
6453
6454 #[test]
6455 fn custom_agent_config_serializes_model() {
6456 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
6457 let wire = serde_json::to_value(&agent).unwrap();
6458 assert_eq!(wire["model"], "claude-haiku-4.5");
6459 assert_eq!(wire["name"], "model-agent");
6460 }
6461
6462 #[test]
6463 fn custom_agent_config_omits_model_when_none() {
6464 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
6465 let wire = serde_json::to_value(&agent).unwrap();
6466 assert!(wire.get("model").is_none());
6467 }
6468
6469 #[test]
6470 fn custom_agent_config_builder_with_reasoning_effort() {
6471 let agent =
6472 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6473 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
6474 }
6475
6476 #[test]
6477 fn custom_agent_config_serializes_reasoning_effort() {
6478 let agent =
6479 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6480 let wire = serde_json::to_value(&agent).unwrap();
6481 assert_eq!(wire["reasoningEffort"], "high");
6482 }
6483
6484 #[test]
6485 fn custom_agent_config_omits_reasoning_effort_when_none() {
6486 let agent = CustomAgentConfig::new("default-agent", "prompt");
6487 let wire = serde_json::to_value(&agent).unwrap();
6488 assert!(wire.get("reasoningEffort").is_none());
6489 }
6490
6491 #[test]
6492 #[should_panic(expected = "tool parameter schema must be a JSON object")]
6493 fn tool_with_parameters_panics_on_non_object_value() {
6494 let _ = Tool::new("noop").with_parameters(json!(null));
6495 }
6496
6497 #[test]
6498 fn tool_result_expanded_serializes_binary_results_for_llm() {
6499 let response = ToolResultResponse {
6500 result: ToolResult::Expanded(ToolResultExpanded {
6501 text_result_for_llm: "rendered chart".to_string(),
6502 result_type: "success".to_string(),
6503 binary_results_for_llm: Some(vec![ToolBinaryResult {
6504 data: "aW1n".to_string(),
6505 mime_type: "image/png".to_string(),
6506 r#type: "image".to_string(),
6507 description: Some("chart preview".to_string()),
6508 }]),
6509 session_log: None,
6510 error: None,
6511 tool_telemetry: None,
6512 tool_references: None,
6513 }),
6514 };
6515
6516 let wire = serde_json::to_value(&response).unwrap();
6517
6518 assert_eq!(
6519 wire,
6520 json!({
6521 "result": {
6522 "textResultForLlm": "rendered chart",
6523 "resultType": "success",
6524 "binaryResultsForLlm": [
6525 {
6526 "data": "aW1n",
6527 "mimeType": "image/png",
6528 "type": "image",
6529 "description": "chart preview"
6530 }
6531 ]
6532 }
6533 })
6534 );
6535 }
6536
6537 #[test]
6538 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
6539 let response = ToolResultResponse {
6540 result: ToolResult::Expanded(ToolResultExpanded {
6541 text_result_for_llm: "ok".to_string(),
6542 result_type: "success".to_string(),
6543 binary_results_for_llm: None,
6544 session_log: None,
6545 error: None,
6546 tool_telemetry: None,
6547 tool_references: None,
6548 }),
6549 };
6550
6551 let wire = serde_json::to_value(&response).unwrap();
6552
6553 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6554 assert!(wire["result"].get("binaryResultsForLlm").is_none());
6555 }
6556
6557 #[test]
6558 fn tool_result_expanded_serializes_tool_references() {
6559 let response = ToolResultResponse {
6560 result: ToolResult::Expanded(
6561 ToolResultExpanded::new("found 2 tools", "success")
6562 .with_tool_references(["get_weather", "check_status"]),
6563 ),
6564 };
6565
6566 let wire = serde_json::to_value(&response).unwrap();
6567
6568 assert_eq!(
6569 wire,
6570 json!({
6571 "result": {
6572 "textResultForLlm": "found 2 tools",
6573 "resultType": "success",
6574 "toolReferences": ["get_weather", "check_status"]
6575 }
6576 })
6577 );
6578 }
6579
6580 #[test]
6581 fn tool_result_expanded_omits_tool_references_when_none() {
6582 let response = ToolResultResponse {
6583 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
6584 };
6585
6586 let wire = serde_json::to_value(&response).unwrap();
6587
6588 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6589 assert!(wire["result"].get("toolReferences").is_none());
6590 }
6591
6592 #[test]
6593 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
6594 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
6597 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
6598
6599 assert_eq!(
6600 expanded.tool_references.as_deref(),
6601 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6602 );
6603 }
6604
6605 #[test]
6606 fn tool_result_expanded_deserializes_tool_references() {
6607 let wire = json!({
6608 "textResultForLlm": "found tools",
6609 "resultType": "success",
6610 "toolReferences": ["alpha", "beta"]
6611 });
6612
6613 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
6614
6615 assert_eq!(
6616 expanded.tool_references.as_deref(),
6617 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6618 );
6619 }
6620
6621 #[test]
6622 fn session_config_default_wire_flags_off_without_handlers() {
6623 let cfg = SessionConfig::default();
6624 assert_eq!(cfg.mcp_oauth_token_storage, None);
6625 assert_eq!(cfg.allowed_models, None);
6626 let (wire, _runtime) = cfg
6630 .into_wire(Some(SessionId::from("default-flags")))
6631 .expect("default config has no duplicate handlers");
6632 assert!(!wire.request_user_input);
6633 assert!(!wire.request_permission);
6634 assert!(!wire.request_elicitation);
6635 assert!(!wire.request_exit_plan_mode);
6636 assert!(!wire.request_auto_mode_switch);
6637 assert!(!wire.hooks);
6638 assert!(!wire.request_mcp_apps);
6639 let json = serde_json::to_value(&wire).unwrap();
6640 assert!(json.get("askUserVariant").is_none());
6641 assert!(json.get("allowedModels").is_none());
6642 }
6643
6644 #[test]
6645 fn resume_session_config_new_wire_flags_off_without_handlers() {
6646 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
6647 assert_eq!(cfg.mcp_oauth_token_storage, None);
6648 assert_eq!(cfg.allowed_models, None);
6649 let (wire, _runtime) = cfg
6650 .into_wire()
6651 .expect("default resume config has no duplicate handlers");
6652 assert!(!wire.request_user_input);
6653 assert!(!wire.request_permission);
6654 assert!(!wire.request_elicitation);
6655 assert!(!wire.request_exit_plan_mode);
6656 assert!(!wire.request_auto_mode_switch);
6657 assert!(!wire.hooks);
6658 assert!(!wire.request_mcp_apps);
6659 let json = serde_json::to_value(&wire).unwrap();
6660 assert!(json.get("askUserVariant").is_none());
6661 assert!(json.get("allowedModels").is_none());
6662 }
6663
6664 #[test]
6665 fn session_configs_build_debug_and_serialize_allowed_models() {
6666 let create = SessionConfig::default().with_allowed_models(["gpt-5.4", "claude-sonnet-4"]);
6667 assert_eq!(
6668 create.allowed_models.as_deref(),
6669 Some(&["gpt-5.4".to_string(), "claude-sonnet-4".to_string()][..])
6670 );
6671 assert!(format!("{create:?}").contains("allowed_models"));
6672
6673 let (create_wire, _) = create
6674 .into_wire(Some(SessionId::from("create-allowed-models")))
6675 .expect("allowed model config has no duplicate handlers");
6676 let create_json = serde_json::to_value(&create_wire).unwrap();
6677 assert_eq!(
6678 create_json["allowedModels"],
6679 json!(["gpt-5.4", "claude-sonnet-4"])
6680 );
6681
6682 let resume = ResumeSessionConfig::new(SessionId::from("resume-allowed-models"))
6683 .with_allowed_models(vec!["gpt-5.4".to_string(), "gpt-5-mini".to_string()]);
6684 assert_eq!(
6685 resume.allowed_models.as_deref(),
6686 Some(&["gpt-5.4".to_string(), "gpt-5-mini".to_string()][..])
6687 );
6688 assert!(format!("{resume:?}").contains("allowed_models"));
6689
6690 let (resume_wire, _) = resume
6691 .into_wire()
6692 .expect("resume allowed model config has no duplicate handlers");
6693 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6694 assert_eq!(
6695 resume_json["allowedModels"],
6696 json!(["gpt-5.4", "gpt-5-mini"])
6697 );
6698 }
6699
6700 #[test]
6701 fn custom_agents_local_only_serializes_on_create_and_resume() {
6702 let (create_wire, _) = SessionConfig::default()
6703 .with_custom_agents_local_only(false)
6704 .into_wire(Some(SessionId::from("create-locality")))
6705 .expect("create config has no duplicate handlers");
6706 let create_json = serde_json::to_value(&create_wire).unwrap();
6707 assert_eq!(create_json["customAgentsLocalOnly"], false);
6708
6709 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
6710 .with_custom_agents_local_only(false)
6711 .into_wire()
6712 .expect("resume config has no duplicate handlers");
6713 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6714 assert_eq!(resume_json["customAgentsLocalOnly"], false);
6715
6716 let (unset_create_wire, _) = SessionConfig::default()
6717 .into_wire(Some(SessionId::from("create-unset")))
6718 .expect("create config has no duplicate handlers");
6719 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6720 assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
6721
6722 let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
6723 .into_wire()
6724 .expect("resume config has no duplicate handlers");
6725 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6726 assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
6727 }
6728
6729 #[test]
6730 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6731 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
6732 assert_eq!(cfg.enable_mcp_apps, Some(true));
6733
6734 let (wire, _runtime) = cfg
6735 .into_wire(Some(SessionId::from("enable-mcp-apps")))
6736 .expect("enable_mcp_apps config has no duplicate handlers");
6737 assert!(wire.request_mcp_apps);
6738
6739 let json = serde_json::to_value(&wire).unwrap();
6740 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6741 }
6742
6743 #[test]
6744 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6745 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
6746 .with_enable_mcp_apps(true);
6747 assert_eq!(cfg.enable_mcp_apps, Some(true));
6748
6749 let (wire, _runtime) = cfg
6750 .into_wire()
6751 .expect("resume enable_mcp_apps config has no duplicate handlers");
6752 assert!(wire.request_mcp_apps);
6753
6754 let json = serde_json::to_value(&wire).unwrap();
6755 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6756 }
6757
6758 #[test]
6759 fn github_mcp_tool_config_serializes_for_create_and_resume() {
6760 let github_config = GitHubMcpToolConfig::new()
6761 .with_enable_all_tools(true)
6762 .with_additional_toolsets(["repos"])
6763 .with_additional_tools(["get_issue"])
6764 .with_enable_insiders_mode(true)
6765 .with_disable_form_deferral(true);
6766
6767 let (create_wire, _) = SessionConfig::default()
6768 .with_github_mcp_tool_config(github_config.clone())
6769 .into_wire(Some(SessionId::from("github-mcp")))
6770 .expect("create config has no duplicate handlers");
6771 assert_eq!(
6772 serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6773 serde_json::json!({
6774 "enableAllTools": true,
6775 "additionalToolsets": ["repos"],
6776 "additionalTools": ["get_issue"],
6777 "enableInsidersMode": true,
6778 "disableFormDeferral": true,
6779 })
6780 );
6781
6782 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6783 .with_github_mcp_tool_config(github_config)
6784 .into_wire()
6785 .expect("resume config has no duplicate handlers");
6786 assert!(resume_wire.github_mcp_tool_config.is_some());
6787
6788 let (unset_wire, _) = SessionConfig::default()
6789 .into_wire(Some(SessionId::from("github-mcp-unset")))
6790 .expect("default config has no duplicate handlers");
6791 assert!(
6792 serde_json::to_value(&unset_wire)
6793 .unwrap()
6794 .get("githubMcpToolConfig")
6795 .is_none()
6796 );
6797 }
6798
6799 #[test]
6800 fn memory_configuration_constructors_and_serde() {
6801 assert!(MemoryConfiguration::enabled().enabled);
6802 assert!(!MemoryConfiguration::disabled().enabled);
6803 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6804
6805 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6806 assert_eq!(json, serde_json::json!({ "enabled": true }));
6807 }
6808
6809 #[test]
6810 fn session_config_with_memory_serializes() {
6811 let (wire, _runtime) = SessionConfig::default()
6812 .with_memory(MemoryConfiguration::enabled())
6813 .into_wire(Some(SessionId::from("memory-on")))
6814 .expect("no duplicate handlers");
6815 let json = serde_json::to_value(&wire).unwrap();
6816 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6817
6818 let (wire_off, _) = SessionConfig::default()
6819 .with_memory(MemoryConfiguration::disabled())
6820 .into_wire(Some(SessionId::from("memory-off")))
6821 .expect("no duplicate handlers");
6822 let json_off = serde_json::to_value(&wire_off).unwrap();
6823 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6824
6825 let (empty_wire, _) = SessionConfig::default()
6827 .into_wire(Some(SessionId::from("memory-unset")))
6828 .expect("no duplicate handlers");
6829 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6830 assert!(empty_json.get("memory").is_none());
6831 }
6832
6833 #[test]
6834 fn resume_session_config_with_memory_serializes() {
6835 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6836 .with_memory(MemoryConfiguration::enabled())
6837 .into_wire()
6838 .expect("no duplicate handlers");
6839 let json = serde_json::to_value(&wire).unwrap();
6840 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6841
6842 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6844 .into_wire()
6845 .expect("no duplicate handlers");
6846 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6847 assert!(empty_json.get("memory").is_none());
6848 }
6849
6850 #[test]
6851 fn feature_flags_serialize_on_create_and_resume() {
6852 let feature_flags = HashMap::from([
6853 ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true),
6854 ("DISABLED_TEST_FLAG".to_string(), false),
6855 ]);
6856 let expected = serde_json::json!({
6857 "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true,
6858 "DISABLED_TEST_FLAG": false,
6859 });
6860
6861 let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone());
6862 assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags));
6863 let (create_wire, _) = create_config
6864 .into_wire(Some(SessionId::from("feature-flags-create")))
6865 .expect("no duplicate handlers");
6866 let create_json = serde_json::to_value(&create_wire).unwrap();
6867 assert_eq!(create_json["featureFlags"], expected);
6868
6869 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume"))
6870 .with_feature_flags(feature_flags)
6871 .into_wire()
6872 .expect("no duplicate handlers");
6873 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6874 assert_eq!(resume_json["featureFlags"], expected);
6875
6876 let (unset_create_wire, _) = SessionConfig::default()
6877 .into_wire(Some(SessionId::from("feature-flags-create-unset")))
6878 .expect("no duplicate handlers");
6879 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6880 assert!(unset_create_json.get("featureFlags").is_none());
6881
6882 let (unset_resume_wire, _) =
6883 ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset"))
6884 .into_wire()
6885 .expect("no duplicate handlers");
6886 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6887 assert!(unset_resume_json.get("featureFlags").is_none());
6888 }
6889
6890 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6891 CopilotExpAssignmentResponse {
6892 features: vec!["copilot_exp_flag".to_string()],
6893 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6894 configs: vec![ExpConfigEntry {
6895 id: "cfg-1".to_string(),
6896 parameters: HashMap::from([
6897 ("threshold".to_string(), ExpFlagValue::Integer(5)),
6898 ("enabled".to_string(), ExpFlagValue::Bool(true)),
6899 ]),
6900 }],
6901 assignment_context: context.to_string(),
6902 ..Default::default()
6903 }
6904 }
6905
6906 #[test]
6907 fn exp_flag_value_round_trips_all_variants() {
6908 let values = serde_json::json!({
6909 "s": "text",
6910 "i": 7,
6911 "f": 1.5,
6912 "b": true,
6913 "n": null,
6914 });
6915 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6916 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6917 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6918 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6919 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6920 assert_eq!(parsed["n"], ExpFlagValue::Null);
6921 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6922 }
6923
6924 #[test]
6925 fn session_config_with_exp_assignments_serializes() {
6926 let assignments = sample_exp_assignments("ctx-123");
6927 let expected = serde_json::to_value(&assignments).unwrap();
6928 let (wire, _runtime) = SessionConfig::default()
6929 .with_exp_assignments(assignments)
6930 .into_wire(Some(SessionId::from("exp-on")))
6931 .expect("no duplicate handlers");
6932 let json = serde_json::to_value(&wire).unwrap();
6933 assert_eq!(json["expAssignments"], expected);
6934 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6935 assert_eq!(
6936 json["expAssignments"]["Flights"]["copilot_exp_flag"],
6937 "treatment"
6938 );
6939
6940 let (empty_wire, _) = SessionConfig::default()
6942 .into_wire(Some(SessionId::from("exp-unset")))
6943 .expect("no duplicate handlers");
6944 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6945 assert!(empty_json.get("expAssignments").is_none());
6946 }
6947
6948 #[test]
6949 fn resume_session_config_with_exp_assignments_serializes() {
6950 let assignments = sample_exp_assignments("ctx-456");
6951 let expected = serde_json::to_value(&assignments).unwrap();
6952 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6953 .with_exp_assignments(assignments)
6954 .into_wire()
6955 .expect("no duplicate handlers");
6956 let json = serde_json::to_value(&wire).unwrap();
6957 assert_eq!(json["expAssignments"], expected);
6958
6959 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6961 .into_wire()
6962 .expect("no duplicate handlers");
6963 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6964 assert!(empty_json.get("expAssignments").is_none());
6965 }
6966
6967 #[test]
6968 fn session_config_clone_preserves_exp_assignments() {
6969 let assignments = sample_exp_assignments("ctx-clone");
6970 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6971 let cloned = config.clone();
6972
6973 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6974
6975 let (wire, _runtime) = cloned
6976 .into_wire(Some(SessionId::from("exp-clone")))
6977 .expect("no duplicate handlers");
6978 let json = serde_json::to_value(&wire).unwrap();
6979 assert_eq!(
6980 json["expAssignments"],
6981 serde_json::to_value(&assignments).unwrap()
6982 );
6983 }
6984
6985 #[test]
6986 fn resume_session_config_clone_preserves_exp_assignments() {
6987 let assignments = sample_exp_assignments("ctx-clone-resume");
6988 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6989 .with_exp_assignments(assignments.clone());
6990 let cloned = config.clone();
6991
6992 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6993
6994 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6995 let json = serde_json::to_value(&wire).unwrap();
6996 assert_eq!(
6997 json["expAssignments"],
6998 serde_json::to_value(&assignments).unwrap()
6999 );
7000 }
7001
7002 #[test]
7003 #[allow(clippy::field_reassign_with_default)]
7004 fn session_config_into_wire_serializes_bucket_b_fields() {
7005 use std::path::PathBuf;
7006
7007 use super::{CloudSessionOptions, CloudSessionRepository};
7008
7009 let mut cfg = SessionConfig::default();
7010 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
7011 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
7012 cfg.github_token = Some("ghs_secret".to_string());
7013 cfg.include_sub_agent_streaming_events = Some(false);
7014 cfg.enable_session_telemetry = Some(false);
7015 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
7016 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
7017 cfg.enable_on_demand_instruction_discovery = Some(false);
7018 cfg.cloud = Some(CloudSessionOptions::with_repository(
7019 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
7020 ));
7021
7022 let (wire, _runtime) = cfg
7023 .into_wire(Some(SessionId::from("custom-id")))
7024 .expect("no duplicate handlers");
7025 let wire_json = serde_json::to_value(&wire).unwrap();
7026 assert_eq!(wire_json["sessionId"], "custom-id");
7027 assert_eq!(wire_json["configDir"], "/tmp/cfg");
7028 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
7029 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
7030 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
7031 assert_eq!(wire_json["enableSessionTelemetry"], false);
7032 assert_eq!(wire_json["reasoningSummary"], "concise");
7033 assert_eq!(wire_json["remoteSession"], "export");
7034 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
7035 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
7036 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
7037 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
7038
7039 let (empty_wire, _) = SessionConfig::default()
7041 .into_wire(Some(SessionId::from("empty")))
7042 .expect("default has no duplicate handlers");
7043 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7044 assert!(empty_json.get("gitHubToken").is_none());
7045 assert!(empty_json.get("enableSessionTelemetry").is_none());
7046 assert!(empty_json.get("reasoningSummary").is_none());
7047 assert!(empty_json.get("remoteSession").is_none());
7048 assert!(
7049 empty_json
7050 .get("enableOnDemandInstructionDiscovery")
7051 .is_none()
7052 );
7053 assert!(empty_json.get("cloud").is_none());
7054 }
7055
7056 #[test]
7057 fn session_config_into_wire_serializes_named_providers_and_models() {
7058 let cfg = SessionConfig::default()
7059 .with_providers(vec![
7060 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
7061 .with_provider_type("openai")
7062 .with_wire_api("responses")
7063 .with_api_key("sk-test"),
7064 ])
7065 .with_models(vec![
7066 ProviderModelConfig::new("gpt-x", "my-openai")
7067 .with_wire_model("gpt-x-2025")
7068 .with_max_output_tokens(2048),
7069 ]);
7070
7071 let (wire, _) = cfg
7072 .into_wire(Some(SessionId::from("sess-providers")))
7073 .expect("no duplicate handlers");
7074 let wire_json = serde_json::to_value(&wire).unwrap();
7075 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
7076 assert_eq!(
7077 wire_json["providers"][0]["baseUrl"],
7078 "https://api.example.com/v1"
7079 );
7080 assert_eq!(wire_json["providers"][0]["type"], "openai");
7081 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
7082 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
7083 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
7084 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
7085 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
7086 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
7087
7088 let (empty_wire, _) = SessionConfig::default()
7089 .into_wire(Some(SessionId::from("empty")))
7090 .expect("default has no duplicate handlers");
7091 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7092 assert!(empty_json.get("providers").is_none());
7093 assert!(empty_json.get("models").is_none());
7094 }
7095
7096 #[test]
7097 fn resume_config_into_wire_serializes_named_providers_and_models() {
7098 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
7099 .with_providers(vec![
7100 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
7101 .with_provider_type("azure")
7102 .with_azure(AzureProviderOptions {
7103 api_version: Some("2024-10-21".to_string()),
7104 }),
7105 ])
7106 .with_models(vec![
7107 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
7108 ]);
7109
7110 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7111 let wire_json = serde_json::to_value(&wire).unwrap();
7112 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
7113 assert_eq!(wire_json["providers"][0]["type"], "azure");
7114 assert_eq!(
7115 wire_json["providers"][0]["azure"]["apiVersion"],
7116 "2024-10-21"
7117 );
7118 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
7119 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
7120 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
7121
7122 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
7123 .into_wire()
7124 .expect("default has no duplicate handlers");
7125 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7126 assert!(empty_json.get("providers").is_none());
7127 assert!(empty_json.get("models").is_none());
7128 }
7129
7130 #[test]
7131 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
7132 use std::path::PathBuf;
7133
7134 let cfg = SessionConfig {
7135 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
7136 disabled_mcp_servers: Some(vec![
7137 "local-files".to_string(),
7138 "remote-github".to_string(),
7139 ]),
7140 large_output: Some(
7141 LargeToolOutputConfig::new()
7142 .with_enabled(true)
7143 .with_max_size_bytes(1024)
7144 .with_output_directory(PathBuf::from("/tmp/large-output")),
7145 ),
7146 ..Default::default()
7147 };
7148
7149 let (wire, _) = cfg
7150 .into_wire(Some(SessionId::from("sess-1")))
7151 .expect("no duplicate handlers");
7152 let wire_json = serde_json::to_value(&wire).unwrap();
7153 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
7154 assert_eq!(
7155 wire_json["disabledMcpServers"],
7156 serde_json::json!(["local-files", "remote-github"])
7157 );
7158 assert_eq!(wire_json["largeOutput"]["enabled"], true);
7159 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
7160 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
7161
7162 let (empty_wire, _) = SessionConfig::default()
7163 .into_wire(Some(SessionId::from("empty")))
7164 .expect("default has no duplicate handlers");
7165 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7166 assert!(empty_json.get("pluginDirectories").is_none());
7167 assert!(empty_json.get("disabledMcpServers").is_none());
7168 assert!(empty_json.get("largeOutput").is_none());
7169 }
7170
7171 #[test]
7172 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
7173 use std::path::PathBuf;
7174
7175 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
7176 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
7177 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
7178 cfg.github_token = Some("ghs_secret".to_string());
7179 cfg.include_sub_agent_streaming_events = Some(true);
7180 cfg.enable_session_telemetry = Some(false);
7181 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
7182 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
7183 cfg.enable_on_demand_instruction_discovery = Some(false);
7184
7185 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7186 let wire_json = serde_json::to_value(&wire).unwrap();
7187 assert_eq!(wire_json["sessionId"], "sess-1");
7188 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
7189 assert_eq!(wire_json["configDir"], "/tmp/cfg");
7190 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
7191 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
7192 assert_eq!(wire_json["enableSessionTelemetry"], false);
7193 assert_eq!(wire_json["reasoningSummary"], "detailed");
7194 assert_eq!(wire_json["remoteSession"], "on");
7195 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
7196
7197 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7199 .into_wire()
7200 .expect("default resume has no duplicate handlers");
7201 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7202 assert!(empty_json.get("reasoningSummary").is_none());
7203 assert!(empty_json.get("remoteSession").is_none());
7204 assert!(
7205 empty_json
7206 .get("enableOnDemandInstructionDiscovery")
7207 .is_none()
7208 );
7209 }
7210
7211 #[test]
7212 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
7213 use std::path::PathBuf;
7214
7215 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
7216 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
7217 cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]);
7218 cfg.large_output = Some(
7219 LargeToolOutputConfig::new()
7220 .with_enabled(false)
7221 .with_max_size_bytes(2048)
7222 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
7223 );
7224
7225 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7226 let wire_json = serde_json::to_value(&wire).unwrap();
7227 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
7228 assert_eq!(
7229 wire_json["disabledMcpServers"],
7230 serde_json::json!(["local-files-r"])
7231 );
7232 assert_eq!(wire_json["largeOutput"]["enabled"], false);
7233 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
7234 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
7235
7236 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7237 .into_wire()
7238 .expect("default resume has no duplicate handlers");
7239 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7240 assert!(empty_json.get("pluginDirectories").is_none());
7241 assert!(empty_json.get("disabledMcpServers").is_none());
7242 assert!(empty_json.get("largeOutput").is_none());
7243 }
7244
7245 #[test]
7246 fn auth_client_id_metadata_url_reaches_create_and_resume_wire_payloads() {
7247 let url = "https://example.com/oauth/client-metadata.json";
7248
7249 let (create_wire, _) = SessionConfig::default()
7250 .with_auth_client_id_metadata_url(url)
7251 .into_wire(None)
7252 .expect("default create has no duplicate handlers");
7253 let create_json = serde_json::to_value(&create_wire).unwrap();
7254 assert_eq!(create_json["authClientIdMetadataUrl"], url);
7255
7256 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-1"))
7257 .with_auth_client_id_metadata_url(url)
7258 .into_wire()
7259 .expect("default resume has no duplicate handlers");
7260 let resume_json = serde_json::to_value(&resume_wire).unwrap();
7261 assert_eq!(resume_json["authClientIdMetadataUrl"], url);
7262
7263 let (empty_create_wire, _) = SessionConfig::default()
7264 .into_wire(None)
7265 .expect("default create has no duplicate handlers");
7266 let empty_create_json = serde_json::to_value(&empty_create_wire).unwrap();
7267 assert!(empty_create_json.get("authClientIdMetadataUrl").is_none());
7268
7269 let (empty_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7270 .into_wire()
7271 .expect("default resume has no duplicate handlers");
7272 let empty_resume_json = serde_json::to_value(&empty_resume_wire).unwrap();
7273 assert!(empty_resume_json.get("authClientIdMetadataUrl").is_none());
7274 }
7275
7276 #[test]
7277 fn session_config_clones_disabled_mcp_servers() {
7278 let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]);
7279 let mut create_clone = create.clone();
7280 create_clone
7281 .disabled_mcp_servers
7282 .as_mut()
7283 .expect("configured disabled MCP servers")
7284 .push("remote-github".to_string());
7285 assert_eq!(
7286 create.disabled_mcp_servers.as_deref(),
7287 Some(&["local-files".to_string()][..])
7288 );
7289
7290 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
7291 .with_disabled_mcp_servers(["local-files"]);
7292 let mut resume_clone = resume.clone();
7293 resume_clone
7294 .disabled_mcp_servers
7295 .as_mut()
7296 .expect("configured disabled MCP servers")
7297 .push("remote-github".to_string());
7298 assert_eq!(
7299 resume.disabled_mcp_servers.as_deref(),
7300 Some(&["local-files".to_string()][..])
7301 );
7302 }
7303
7304 #[test]
7305 fn session_config_builder_composes() {
7306 use indexmap::IndexMap;
7307
7308 let cfg = SessionConfig::default()
7309 .with_session_id(SessionId::from("sess-1"))
7310 .with_model("claude-sonnet-4")
7311 .with_client_name("test-app")
7312 .with_reasoning_effort("medium")
7313 .with_reasoning_summary(ReasoningSummary::Concise)
7314 .with_context_tier("long_context")
7315 .with_streaming(true)
7316 .with_tools([Tool::new("greet")])
7317 .with_available_tools(["bash", "view"])
7318 .with_excluded_tools(["dangerous"])
7319 .with_mcp_servers(IndexMap::new())
7320 .with_mcp_oauth_token_storage("persistent")
7321 .with_enable_config_discovery(true)
7322 .with_enable_on_demand_instruction_discovery(true)
7323 .with_skill_directories([PathBuf::from("/tmp/skills")])
7324 .with_disabled_skills(["broken-skill"])
7325 .with_disabled_mcp_servers(["local-files"])
7326 .with_agent("researcher")
7327 .with_config_directory(PathBuf::from("/tmp/config"))
7328 .with_working_directory(PathBuf::from("/tmp/work"))
7329 .with_additional_directories([PathBuf::from("/tmp/shared")])
7330 .with_github_token("ghp_test")
7331 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7332 .with_enable_session_telemetry(false)
7333 .with_include_sub_agent_streaming_events(false)
7334 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
7335
7336 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
7337 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
7338 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
7339 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
7340 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
7341 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
7342 assert_eq!(cfg.streaming, Some(true));
7343 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
7344 assert_eq!(
7345 cfg.available_tools.as_deref(),
7346 Some(&["bash".to_string(), "view".to_string()][..])
7347 );
7348 assert_eq!(
7349 cfg.excluded_tools.as_deref(),
7350 Some(&["dangerous".to_string()][..])
7351 );
7352 assert!(cfg.mcp_servers.is_some());
7353 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
7354 assert_eq!(cfg.enable_config_discovery, Some(true));
7355 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
7356 assert_eq!(
7357 cfg.skill_directories.as_deref(),
7358 Some(&[PathBuf::from("/tmp/skills")][..])
7359 );
7360 assert_eq!(
7361 cfg.disabled_skills.as_deref(),
7362 Some(&["broken-skill".to_string()][..])
7363 );
7364 assert_eq!(
7365 cfg.disabled_mcp_servers.as_deref(),
7366 Some(&["local-files".to_string()][..])
7367 );
7368 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
7369 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
7370 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
7371 assert_eq!(
7372 cfg.additional_directories.as_deref(),
7373 Some(&[PathBuf::from("/tmp/shared")][..])
7374 );
7375 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
7376 assert_eq!(
7377 cfg.capi,
7378 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7379 );
7380 assert_eq!(cfg.enable_session_telemetry, Some(false));
7381 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
7382 assert_eq!(
7383 cfg.extension_info,
7384 Some(ExtensionInfo::new("github-app", "counter"))
7385 );
7386 }
7387
7388 #[test]
7389 fn resume_session_config_builder_composes() {
7390 use indexmap::IndexMap;
7391
7392 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
7393 .with_client_name("test-app")
7394 .with_reasoning_summary(ReasoningSummary::None)
7395 .with_context_tier("default")
7396 .with_streaming(true)
7397 .with_tools([Tool::new("greet")])
7398 .with_available_tools(["bash", "view"])
7399 .with_excluded_tools(["dangerous"])
7400 .with_mcp_servers(IndexMap::new())
7401 .with_mcp_oauth_token_storage("persistent")
7402 .with_enable_config_discovery(true)
7403 .with_enable_on_demand_instruction_discovery(false)
7404 .with_skill_directories([PathBuf::from("/tmp/skills")])
7405 .with_disabled_skills(["broken-skill"])
7406 .with_disabled_mcp_servers(["local-files"])
7407 .with_agent("researcher")
7408 .with_config_directory(PathBuf::from("/tmp/config"))
7409 .with_working_directory(PathBuf::from("/tmp/work"))
7410 .with_additional_directories([PathBuf::from("/tmp/shared")])
7411 .with_github_token("ghp_test")
7412 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7413 .with_enable_session_telemetry(false)
7414 .with_include_sub_agent_streaming_events(true)
7415 .with_suppress_resume_event(true)
7416 .with_continue_pending_work(true)
7417 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
7418
7419 assert_eq!(cfg.session_id.as_str(), "sess-2");
7420 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
7421 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
7422 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
7423 assert_eq!(cfg.streaming, Some(true));
7424 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
7425 assert_eq!(
7426 cfg.available_tools.as_deref(),
7427 Some(&["bash".to_string(), "view".to_string()][..])
7428 );
7429 assert_eq!(
7430 cfg.excluded_tools.as_deref(),
7431 Some(&["dangerous".to_string()][..])
7432 );
7433 assert!(cfg.mcp_servers.is_some());
7434 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
7435 assert_eq!(cfg.enable_config_discovery, Some(true));
7436 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
7437 assert_eq!(
7438 cfg.skill_directories.as_deref(),
7439 Some(&[PathBuf::from("/tmp/skills")][..])
7440 );
7441 assert_eq!(
7442 cfg.disabled_skills.as_deref(),
7443 Some(&["broken-skill".to_string()][..])
7444 );
7445 assert_eq!(
7446 cfg.disabled_mcp_servers.as_deref(),
7447 Some(&["local-files".to_string()][..])
7448 );
7449 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
7450 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
7451 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
7452 assert_eq!(
7453 cfg.additional_directories.as_deref(),
7454 Some(&[PathBuf::from("/tmp/shared")][..])
7455 );
7456 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
7457 assert_eq!(
7458 cfg.capi,
7459 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7460 );
7461 assert_eq!(cfg.enable_session_telemetry, Some(false));
7462 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
7463 assert_eq!(cfg.suppress_resume_event, Some(true));
7464 assert_eq!(cfg.continue_pending_work, Some(true));
7465 assert_eq!(
7466 cfg.extension_info,
7467 Some(ExtensionInfo::new("github-app", "counter"))
7468 );
7469 }
7470
7471 #[test]
7475 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
7476 let cfg =
7477 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
7478 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7479 let json = serde_json::to_value(&wire).unwrap();
7480 assert_eq!(json["continuePendingWork"], true);
7481
7482 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7484 .into_wire()
7485 .expect("no duplicate handlers");
7486 let json = serde_json::to_value(&wire).unwrap();
7487 assert!(json.get("continuePendingWork").is_none());
7488 }
7489
7490 #[test]
7491 fn session_configs_serialize_additional_directories() {
7492 let create = SessionConfig::default().with_additional_directories([
7493 PathBuf::from("/tmp/shared"),
7494 PathBuf::from("/tmp/generated"),
7495 ]);
7496 let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
7497 let create_json = serde_json::to_value(&create_wire).unwrap();
7498 assert_eq!(
7499 create_json["additionalDirectories"],
7500 serde_json::json!(["/tmp/shared", "/tmp/generated"])
7501 );
7502
7503 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
7504 .with_additional_directories([PathBuf::from("/tmp/resumed")]);
7505 let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
7506 let resume_json = serde_json::to_value(&resume_wire).unwrap();
7507 assert_eq!(
7508 resume_json["additionalDirectories"],
7509 serde_json::json!(["/tmp/resumed"])
7510 );
7511 }
7512
7513 #[test]
7517 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
7518 let cfg =
7519 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
7520 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7521 let json = serde_json::to_value(&wire).unwrap();
7522 assert_eq!(json["disableResume"], true);
7523 assert!(json.get("suppressResumeEvent").is_none());
7524 }
7525
7526 #[test]
7529 fn session_config_serializes_instruction_directories_to_camel_case() {
7530 let cfg =
7531 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
7532 let (wire, _) = cfg
7533 .into_wire(Some(SessionId::from("instr-on")))
7534 .expect("no duplicate handlers");
7535 let json = serde_json::to_value(&wire).unwrap();
7536 assert_eq!(
7537 json["instructionDirectories"],
7538 serde_json::json!(["/tmp/instr"])
7539 );
7540
7541 let (wire, _) = SessionConfig::default()
7543 .into_wire(Some(SessionId::from("instr-off")))
7544 .expect("no duplicate handlers");
7545 let json = serde_json::to_value(&wire).unwrap();
7546 assert!(json.get("instructionDirectories").is_none());
7547 }
7548
7549 #[test]
7552 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
7553 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
7554 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
7555 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7556 let json = serde_json::to_value(&wire).unwrap();
7557 assert_eq!(
7558 json["instructionDirectories"],
7559 serde_json::json!(["/tmp/instr"])
7560 );
7561
7562 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7563 .into_wire()
7564 .expect("no duplicate handlers");
7565 let json = serde_json::to_value(&wire).unwrap();
7566 assert!(json.get("instructionDirectories").is_none());
7567 }
7568
7569 #[test]
7570 fn custom_agent_config_builder_composes() {
7571 use indexmap::IndexMap;
7572
7573 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
7574 .with_display_name("Research Assistant")
7575 .with_description("Investigates technical questions.")
7576 .with_tools(["bash", "view"])
7577 .with_mcp_servers(IndexMap::new())
7578 .with_infer(true)
7579 .with_skills(["rust-coding-skill"]);
7580
7581 assert_eq!(cfg.name, "researcher");
7582 assert_eq!(cfg.prompt, "You are a research assistant.");
7583 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
7584 assert_eq!(
7585 cfg.description.as_deref(),
7586 Some("Investigates technical questions.")
7587 );
7588 assert_eq!(
7589 cfg.tools.as_deref(),
7590 Some(&["bash".to_string(), "view".to_string()][..])
7591 );
7592 assert!(cfg.mcp_servers.is_some());
7593 assert_eq!(cfg.infer, Some(true));
7594 assert_eq!(
7595 cfg.skills.as_deref(),
7596 Some(&["rust-coding-skill".to_string()][..])
7597 );
7598 }
7599
7600 #[test]
7601 fn mcp_servers_serialize_in_insertion_order() {
7602 use indexmap::IndexMap;
7603
7604 let order = [
7610 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
7611 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
7612 ];
7613 let mut servers = IndexMap::new();
7614 for name in order {
7615 servers.insert(
7616 name.to_string(),
7617 McpServerConfig::Stdio(McpStdioServerConfig {
7618 command: "run".to_string(),
7619 ..Default::default()
7620 }),
7621 );
7622 }
7623
7624 let (wire, _runtime) = SessionConfig::default()
7625 .with_mcp_servers(servers)
7626 .into_wire(None)
7627 .expect("into_wire should succeed");
7628 let json = serde_json::to_string(&wire).expect("serialize wire");
7629
7630 let positions: Vec<usize> = order
7631 .iter()
7632 .map(|name| {
7633 json.find(&format!("\"{name}\""))
7634 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
7635 })
7636 .collect();
7637 let mut ascending = positions.clone();
7638 ascending.sort_unstable();
7639 assert_eq!(
7640 positions, ascending,
7641 "mcp server keys must serialize in insertion order: {json}"
7642 );
7643 }
7644
7645 #[test]
7646 fn infinite_session_config_builder_composes() {
7647 let cfg = InfiniteSessionConfig::new()
7648 .with_enabled(true)
7649 .with_background_compaction_threshold(0.75)
7650 .with_buffer_exhaustion_threshold(0.92);
7651
7652 assert_eq!(cfg.enabled, Some(true));
7653 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
7654 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
7655 }
7656
7657 #[test]
7658 fn provider_config_builder_composes() {
7659 use std::collections::HashMap;
7660
7661 let mut headers = HashMap::new();
7662 headers.insert("X-Custom".to_string(), "value".to_string());
7663
7664 let cfg = ProviderConfig::new("https://api.example.com")
7665 .with_provider_type("openai")
7666 .with_wire_api("completions")
7667 .with_transport("websockets")
7668 .with_api_key("sk-test")
7669 .with_bearer_token("bearer-test")
7670 .with_headers(headers)
7671 .with_model_id("gpt-4")
7672 .with_wire_model("azure-gpt-4-deployment")
7673 .with_max_prompt_tokens(8192)
7674 .with_max_output_tokens(2048);
7675
7676 assert_eq!(cfg.base_url, "https://api.example.com");
7677 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
7678 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
7679 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
7680 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
7681 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
7682 assert_eq!(
7683 cfg.headers
7684 .as_ref()
7685 .and_then(|h| h.get("X-Custom"))
7686 .map(String::as_str),
7687 Some("value"),
7688 );
7689 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
7690 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
7691 assert_eq!(cfg.max_prompt_tokens, Some(8192));
7692 assert_eq!(cfg.max_output_tokens, Some(2048));
7693
7694 let wire = serde_json::to_value(&cfg).unwrap();
7696 assert_eq!(wire["modelId"], "gpt-4");
7697 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
7698 assert_eq!(wire["maxPromptTokens"], 8192);
7699 assert_eq!(wire["maxOutputTokens"], 2048);
7700
7701 let unset = ProviderConfig::new("https://api.example.com");
7702 let wire_unset = serde_json::to_value(&unset).unwrap();
7703 assert!(wire_unset.get("modelId").is_none());
7704 assert!(wire_unset.get("wireModel").is_none());
7705 assert!(wire_unset.get("maxPromptTokens").is_none());
7706 assert!(wire_unset.get("maxOutputTokens").is_none());
7707 }
7708
7709 #[test]
7710 fn capi_session_options_builder_composes_and_serializes() {
7711 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
7712
7713 assert_eq!(cfg.enable_web_socket_responses, Some(false));
7714
7715 let wire = serde_json::to_value(&cfg).unwrap();
7716 assert_eq!(
7717 wire,
7718 serde_json::json!({ "enableWebSocketResponses": false })
7719 );
7720
7721 let unset = CapiSessionOptions::new();
7722 let wire_unset = serde_json::to_value(&unset).unwrap();
7723 assert!(wire_unset.get("enableWebSocketResponses").is_none());
7724 assert!(wire_unset.get("autoTier").is_none());
7725 assert_eq!(wire_unset, json!({}));
7726 }
7727
7728 #[test]
7729 fn capi_auto_tier_canonical_values_round_trip_and_forward() {
7730 for (tier, value) in [
7731 (AutoTier::Efficiency, "efficiency"),
7732 (AutoTier::Balance, "balance"),
7733 (AutoTier::Intelligence, "intelligence"),
7734 (AutoTier::Fast, "fast"),
7735 ] {
7736 let exported: crate::AutoTier = tier.clone();
7737 let capi = CapiSessionOptions::new().with_auto_tier(exported);
7738 assert_eq!(capi.auto_tier, Some(tier));
7739 assert_eq!(
7740 serde_json::to_value(&capi).unwrap(),
7741 json!({"autoTier": value})
7742 );
7743 assert_eq!(
7744 serde_json::from_value::<CapiSessionOptions>(json!({"autoTier": value})).unwrap(),
7745 capi
7746 );
7747
7748 let capi = capi.with_enable_web_socket_responses(false);
7749 let expected = json!({"autoTier": value, "enableWebSocketResponses": false});
7750 let (create, _) = SessionConfig::default()
7751 .with_model("auto")
7752 .with_capi(capi.clone())
7753 .into_wire(Some(SessionId::from("capi-create")))
7754 .unwrap();
7755 assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected);
7756
7757 let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7758 .with_capi(capi)
7759 .into_wire()
7760 .unwrap();
7761 assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected);
7762 }
7763 }
7764
7765 #[test]
7766 fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() {
7767 for value in ["balanced", "Balance", "unknown"] {
7768 assert_eq!(
7769 serde_json::from_value::<AutoTier>(json!(value)).unwrap(),
7770 AutoTier::Unknown
7771 );
7772 }
7773 let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap();
7774 assert_eq!(capi.auto_tier, None);
7775 }
7776
7777 #[test]
7778 fn session_config_with_capi_serializes() {
7779 let (wire, _) = SessionConfig::default()
7780 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7781 .into_wire(Some(SessionId::from("capi-create")))
7782 .expect("no duplicate handlers");
7783 let json = serde_json::to_value(&wire).unwrap();
7784 assert_eq!(
7785 json["capi"],
7786 serde_json::json!({ "enableWebSocketResponses": false })
7787 );
7788
7789 let (empty_wire, _) = SessionConfig::default()
7790 .into_wire(Some(SessionId::from("capi-create-unset")))
7791 .expect("no duplicate handlers");
7792 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7793 assert!(empty_json.get("capi").is_none());
7794 }
7795
7796 #[test]
7797 fn resume_session_config_with_capi_serializes() {
7798 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7799 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7800 .into_wire()
7801 .expect("no duplicate handlers");
7802 let json = serde_json::to_value(&wire).unwrap();
7803 assert_eq!(
7804 json["capi"],
7805 serde_json::json!({ "enableWebSocketResponses": false })
7806 );
7807
7808 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
7809 .into_wire()
7810 .expect("no duplicate handlers");
7811 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7812 assert!(empty_json.get("capi").is_none());
7813 }
7814
7815 #[test]
7816 fn system_message_config_builder_composes() {
7817 use std::collections::HashMap;
7818
7819 let cfg = SystemMessageConfig::new()
7820 .with_mode("replace")
7821 .with_content("Custom system message.")
7822 .with_sections(HashMap::new());
7823
7824 assert_eq!(cfg.mode.as_deref(), Some("replace"));
7825 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
7826 assert!(cfg.sections.is_some());
7827 }
7828
7829 #[test]
7830 fn delivery_mode_serializes_to_kebab_case_strings() {
7831 assert_eq!(
7832 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
7833 "\"enqueue\""
7834 );
7835 assert_eq!(
7836 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
7837 "\"immediate\""
7838 );
7839 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
7840 assert_eq!(parsed, DeliveryMode::Immediate);
7841 }
7842
7843 #[test]
7844 fn agent_mode_serializes_to_kebab_case_strings() {
7845 assert_eq!(
7846 serde_json::to_string(&AgentMode::Interactive).unwrap(),
7847 "\"interactive\""
7848 );
7849 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
7850 assert_eq!(
7851 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
7852 "\"autopilot\""
7853 );
7854 assert_eq!(
7855 serde_json::to_string(&AgentMode::Shell).unwrap(),
7856 "\"shell\""
7857 );
7858 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
7859 assert_eq!(parsed, AgentMode::Plan);
7860 }
7861
7862 #[test]
7863 fn connection_state_distinguishes_variants() {
7864 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
7867 }
7868
7869 #[test]
7875 fn session_event_round_trips_agent_id_on_envelope() {
7876 let wire = json!({
7877 "id": "evt-1",
7878 "timestamp": "2026-04-30T12:00:00Z",
7879 "parentId": null,
7880 "agentId": "sub-agent-42",
7881 "type": "assistant.message",
7882 "data": { "message": "hi" }
7883 });
7884
7885 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
7886 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7887
7888 let roundtripped = serde_json::to_value(&event).unwrap();
7890 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7891
7892 let main_agent_event: SessionEvent = serde_json::from_value(json!({
7894 "id": "evt-2",
7895 "timestamp": "2026-04-30T12:00:01Z",
7896 "parentId": null,
7897 "type": "session.idle",
7898 "data": {}
7899 }))
7900 .unwrap();
7901 assert!(main_agent_event.agent_id.is_none());
7902 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
7903 assert!(roundtripped.get("agentId").is_none());
7904 }
7905
7906 #[test]
7908 fn typed_session_event_round_trips_agent_id_on_envelope() {
7909 let wire = json!({
7910 "id": "evt-1",
7911 "timestamp": "2026-04-30T12:00:00Z",
7912 "parentId": null,
7913 "agentId": "sub-agent-42",
7914 "type": "session.idle",
7915 "data": {}
7916 });
7917
7918 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
7919 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7920
7921 let roundtripped = serde_json::to_value(&event).unwrap();
7922 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7923 }
7924
7925 #[test]
7926 fn connection_state_variants_compile() {
7927 let _ = ConnectionState::Disconnected;
7931 let _ = ConnectionState::Connecting;
7932 let _ = ConnectionState::Connected;
7933 let _ = ConnectionState::Error;
7934 }
7935
7936 #[test]
7937 fn deserializes_runtime_attachment_variants() {
7938 let attachments: Vec<Attachment> = serde_json::from_value(json!([
7939 {
7940 "type": "file",
7941 "path": "/tmp/file.rs",
7942 "displayName": "file.rs",
7943 "lineRange": { "start": 7, "end": 12 }
7944 },
7945 {
7946 "type": "directory",
7947 "path": "/tmp/project",
7948 "displayName": "project"
7949 },
7950 {
7951 "type": "selection",
7952 "filePath": "/tmp/lib.rs",
7953 "displayName": "lib.rs",
7954 "text": "fn main() {}",
7955 "selection": {
7956 "start": { "line": 1, "character": 2 },
7957 "end": { "line": 3, "character": 4 }
7958 }
7959 },
7960 {
7961 "type": "blob",
7962 "data": "Zm9v",
7963 "mimeType": "image/png",
7964 "displayName": "image.png"
7965 },
7966 {
7967 "type": "github_reference",
7968 "number": 42,
7969 "title": "Fix rendering",
7970 "referenceType": "issue",
7971 "state": "open",
7972 "url": "https://github.com/example/repo/issues/42"
7973 },
7974 {
7975 "type": "extension_context",
7976 "capturedAt": "2026-09-18T11:00:00Z",
7977 "extensionId": "example:extension",
7978 "title": "Unbound context"
7979 }
7980 ]))
7981 .expect("attachments should deserialize");
7982
7983 assert_eq!(attachments.len(), 6);
7984 assert!(matches!(
7985 &attachments[0],
7986 Attachment::File {
7987 path,
7988 display_name,
7989 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7990 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7991 ));
7992 assert!(matches!(
7993 &attachments[1],
7994 Attachment::Directory { path, display_name }
7995 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7996 ));
7997 assert!(matches!(
7998 &attachments[2],
7999 Attachment::Selection {
8000 file_path,
8001 display_name,
8002 selection:
8003 AttachmentSelectionRange {
8004 start: AttachmentSelectionPosition { line: 1, character: 2 },
8005 end: AttachmentSelectionPosition { line: 3, character: 4 },
8006 },
8007 ..
8008 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
8009 ));
8010 assert!(matches!(
8011 &attachments[3],
8012 Attachment::Blob {
8013 data,
8014 mime_type,
8015 display_name,
8016 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
8017 ));
8018 assert!(matches!(
8019 &attachments[4],
8020 Attachment::GitHubReference {
8021 number: 42,
8022 title,
8023 reference_type: GitHubReferenceType::Issue,
8024 state,
8025 url,
8026 } if title == "Fix rendering"
8027 && state == "open"
8028 && url == "https://github.com/example/repo/issues/42"
8029 ));
8030 assert!(matches!(
8031 &attachments[5],
8032 Attachment::ExtensionContext {
8033 captured_at,
8034 extension_id,
8035 canvas_id: None,
8036 instance_id: None,
8037 title,
8038 payload: None,
8039 } if captured_at == "2026-09-18T11:00:00Z"
8040 && extension_id == "example:extension"
8041 && title == "Unbound context"
8042 ));
8043 assert_eq!(
8044 serde_json::to_value(&attachments[5]).expect("serialize extension context"),
8045 json!({
8046 "type": "extension_context",
8047 "capturedAt": "2026-09-18T11:00:00Z",
8048 "extensionId": "example:extension",
8049 "title": "Unbound context"
8050 })
8051 );
8052 }
8053
8054 #[test]
8055 fn ensures_display_names_for_variants_that_support_them() {
8056 let mut attachments = vec![
8057 Attachment::File {
8058 path: PathBuf::from("/tmp/file.rs"),
8059 display_name: None,
8060 line_range: None,
8061 },
8062 Attachment::Selection {
8063 file_path: PathBuf::from("/tmp/src/lib.rs"),
8064 display_name: None,
8065 text: "fn main() {}".to_string(),
8066 selection: AttachmentSelectionRange {
8067 start: AttachmentSelectionPosition {
8068 line: 0,
8069 character: 0,
8070 },
8071 end: AttachmentSelectionPosition {
8072 line: 0,
8073 character: 10,
8074 },
8075 },
8076 },
8077 Attachment::Blob {
8078 data: "Zm9v".to_string(),
8079 mime_type: "image/png".to_string(),
8080 display_name: None,
8081 },
8082 Attachment::GitHubReference {
8083 number: 7,
8084 title: "Track regressions".to_string(),
8085 reference_type: GitHubReferenceType::Issue,
8086 state: "open".to_string(),
8087 url: "https://example.com/issues/7".to_string(),
8088 },
8089 ];
8090
8091 ensure_attachment_display_names(&mut attachments);
8092
8093 assert_eq!(attachments[0].display_name(), Some("file.rs"));
8094 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
8095 assert_eq!(attachments[2].display_name(), Some("attachment"));
8096 assert_eq!(attachments[3].display_name(), None);
8097 assert_eq!(
8098 attachments[3].label(),
8099 Some("Track regressions".to_string())
8100 );
8101 }
8102
8103 #[test]
8104 fn github_anchored_attachment_variants_round_trip() {
8105 let cases = vec![
8106 (
8107 "github_commit",
8108 json!({
8109 "type": "github_commit",
8110 "message": "Fix the thing",
8111 "oid": "abc123",
8112 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
8113 "url": "https://github.com/octocat/repo/commit/abc123"
8114 }),
8115 ),
8116 (
8117 "github_release",
8118 json!({
8119 "type": "github_release",
8120 "name": "v1.2.3",
8121 "repo": { "name": "repo", "owner": "octocat" },
8122 "tagName": "v1.2.3",
8123 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
8124 }),
8125 ),
8126 (
8127 "github_actions_job",
8128 json!({
8129 "type": "github_actions_job",
8130 "conclusion": "failure",
8131 "jobId": 99,
8132 "jobName": "build",
8133 "repo": { "name": "repo", "owner": "octocat" },
8134 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
8135 "workflowName": "CI"
8136 }),
8137 ),
8138 (
8139 "github_repository",
8140 json!({
8141 "type": "github_repository",
8142 "description": "An example repository",
8143 "ref": "main",
8144 "repo": { "name": "repo", "owner": "octocat" },
8145 "url": "https://github.com/octocat/repo"
8146 }),
8147 ),
8148 (
8149 "github_file_diff",
8150 json!({
8151 "type": "github_file_diff",
8152 "base": {
8153 "path": "src/lib.rs",
8154 "ref": "main",
8155 "repo": { "name": "repo", "owner": "octocat" }
8156 },
8157 "head": {
8158 "path": "src/lib.rs",
8159 "ref": "feature",
8160 "repo": { "name": "repo", "owner": "octocat" }
8161 },
8162 "url": "https://github.com/octocat/repo/compare/main...feature"
8163 }),
8164 ),
8165 (
8166 "github_tree_comparison",
8167 json!({
8168 "type": "github_tree_comparison",
8169 "base": {
8170 "repo": { "name": "repo", "owner": "octocat" },
8171 "revision": "main"
8172 },
8173 "head": {
8174 "repo": { "name": "repo", "owner": "octocat" },
8175 "revision": "feature"
8176 },
8177 "url": "https://github.com/octocat/repo/compare/main...feature"
8178 }),
8179 ),
8180 (
8181 "github_url",
8182 json!({
8183 "type": "github_url",
8184 "url": "https://github.com/octocat/repo/wiki"
8185 }),
8186 ),
8187 (
8188 "github_file",
8189 json!({
8190 "type": "github_file",
8191 "path": "src/main.rs",
8192 "ref": "main",
8193 "repo": { "name": "repo", "owner": "octocat" },
8194 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
8195 }),
8196 ),
8197 (
8198 "github_snippet",
8199 json!({
8200 "type": "github_snippet",
8201 "lineRange": { "start": 10, "end": 20 },
8202 "path": "src/main.rs",
8203 "ref": "main",
8204 "repo": { "name": "repo", "owner": "octocat" },
8205 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
8206 }),
8207 ),
8208 ];
8209
8210 for (expected_type, input) in cases {
8211 let attachment: Attachment = serde_json::from_value(input.clone())
8212 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
8213
8214 let serialized_string = serde_json::to_string(&attachment)
8219 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
8220
8221 assert_eq!(
8223 serialized_string.matches("\"type\":").count(),
8224 1,
8225 "{expected_type} must serialize a single `type` key"
8226 );
8227
8228 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
8229 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
8230 assert_eq!(
8231 serialized.get("type").and_then(|value| value.as_str()),
8232 Some(expected_type),
8233 "{expected_type} must serialize the correct discriminator"
8234 );
8235
8236 assert_eq!(
8238 serialized, input,
8239 "{expected_type} should round-trip without data loss"
8240 );
8241 let reparsed: Attachment = serde_json::from_value(serialized)
8242 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
8243 assert_eq!(
8244 reparsed, attachment,
8245 "{expected_type} should re-deserialize to the same value"
8246 );
8247 }
8248 }
8249}
8250
8251#[cfg(test)]
8252mod permission_builder_tests {
8253 use std::sync::Arc;
8254
8255 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
8256 use crate::permission;
8257 use crate::types::{
8258 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
8259 SessionId,
8260 };
8261
8262 fn data() -> PermissionRequestData {
8263 PermissionRequestData {
8264 extra: serde_json::json!({"tool": "shell"}),
8265 ..Default::default()
8266 }
8267 }
8268
8269 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
8272 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
8273 }
8274
8275 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
8276 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
8277 }
8278
8279 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
8280 handler
8281 .handle(SessionId::from("s1"), RequestId::new("1"), data())
8282 .await
8283 }
8284
8285 #[tokio::test]
8286 async fn approve_all_with_handler_present_approves() {
8287 let cfg = SessionConfig::default()
8288 .with_permission_handler(Arc::new(ApproveAllHandler))
8289 .approve_all_permissions();
8290 let h = resolve_create(cfg).expect("policy + handler yields handler");
8291 assert!(matches!(
8292 dispatch(&h).await,
8293 PermissionResult::Decision {
8294 decision: PermissionDecision::ApproveOnce(_),
8295 ..
8296 }
8297 ));
8298 }
8299
8300 #[tokio::test]
8301 async fn approve_all_standalone_produces_handler() {
8302 let cfg = SessionConfig::default().approve_all_permissions();
8303 let h = resolve_create(cfg).expect("policy alone yields handler");
8304 assert!(matches!(
8305 dispatch(&h).await,
8306 PermissionResult::Decision {
8307 decision: PermissionDecision::ApproveOnce(_),
8308 ..
8309 }
8310 ));
8311 }
8312
8313 #[tokio::test]
8316 async fn approve_all_is_order_independent() {
8317 let a = SessionConfig::default()
8318 .with_permission_handler(Arc::new(ApproveAllHandler))
8319 .approve_all_permissions();
8320 let b = SessionConfig::default()
8321 .approve_all_permissions()
8322 .with_permission_handler(Arc::new(ApproveAllHandler));
8323 let ha = resolve_create(a).unwrap();
8324 let hb = resolve_create(b).unwrap();
8325 assert!(matches!(
8326 dispatch(&ha).await,
8327 PermissionResult::Decision {
8328 decision: PermissionDecision::ApproveOnce(_),
8329 ..
8330 }
8331 ));
8332 assert!(matches!(
8333 dispatch(&hb).await,
8334 PermissionResult::Decision {
8335 decision: PermissionDecision::ApproveOnce(_),
8336 ..
8337 }
8338 ));
8339 }
8340
8341 #[tokio::test]
8342 async fn deny_all_is_order_independent() {
8343 let a = SessionConfig::default()
8344 .with_permission_handler(Arc::new(ApproveAllHandler))
8345 .deny_all_permissions();
8346 let b = SessionConfig::default()
8347 .deny_all_permissions()
8348 .with_permission_handler(Arc::new(ApproveAllHandler));
8349 let ha = resolve_create(a).unwrap();
8350 let hb = resolve_create(b).unwrap();
8351 assert!(matches!(
8352 dispatch(&ha).await,
8353 PermissionResult::Decision {
8354 decision: PermissionDecision::Reject(_),
8355 ..
8356 }
8357 ));
8358 assert!(matches!(
8359 dispatch(&hb).await,
8360 PermissionResult::Decision {
8361 decision: PermissionDecision::Reject(_),
8362 ..
8363 }
8364 ));
8365 }
8366
8367 #[tokio::test]
8368 async fn approve_permissions_if_consults_predicate() {
8369 let cfg = SessionConfig::default().approve_permissions_if(|d| {
8370 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
8371 });
8372 let h = resolve_create(cfg).unwrap();
8373 assert!(matches!(
8374 dispatch(&h).await,
8375 PermissionResult::Decision {
8376 decision: PermissionDecision::Reject(_),
8377 ..
8378 }
8379 ));
8380 }
8381
8382 #[tokio::test]
8383 async fn approve_permissions_if_is_order_independent() {
8384 let predicate = |d: &PermissionRequestData| {
8385 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
8386 };
8387 let a = SessionConfig::default()
8388 .with_permission_handler(Arc::new(ApproveAllHandler))
8389 .approve_permissions_if(predicate);
8390 let b = SessionConfig::default()
8391 .approve_permissions_if(predicate)
8392 .with_permission_handler(Arc::new(ApproveAllHandler));
8393 let ha = resolve_create(a).unwrap();
8394 let hb = resolve_create(b).unwrap();
8395 assert!(matches!(
8396 dispatch(&ha).await,
8397 PermissionResult::Decision {
8398 decision: PermissionDecision::Reject(_),
8399 ..
8400 }
8401 ));
8402 assert!(matches!(
8403 dispatch(&hb).await,
8404 PermissionResult::Decision {
8405 decision: PermissionDecision::Reject(_),
8406 ..
8407 }
8408 ));
8409 }
8410
8411 #[tokio::test]
8412 async fn resume_session_config_approve_all_works() {
8413 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
8414 .with_permission_handler(Arc::new(ApproveAllHandler))
8415 .approve_all_permissions();
8416 let h = resolve_resume(cfg).unwrap();
8417 assert!(matches!(
8418 dispatch(&h).await,
8419 PermissionResult::Decision {
8420 decision: PermissionDecision::ApproveOnce(_),
8421 ..
8422 }
8423 ));
8424 }
8425
8426 #[tokio::test]
8427 async fn resume_session_config_approve_all_is_order_independent() {
8428 let a = ResumeSessionConfig::new(SessionId::from("s1"))
8429 .with_permission_handler(Arc::new(ApproveAllHandler))
8430 .approve_all_permissions();
8431 let b = ResumeSessionConfig::new(SessionId::from("s1"))
8432 .approve_all_permissions()
8433 .with_permission_handler(Arc::new(ApproveAllHandler));
8434 let ha = resolve_resume(a).unwrap();
8435 let hb = resolve_resume(b).unwrap();
8436 assert!(matches!(
8437 dispatch(&ha).await,
8438 PermissionResult::Decision {
8439 decision: PermissionDecision::ApproveOnce(_),
8440 ..
8441 }
8442 ));
8443 assert!(matches!(
8444 dispatch(&hb).await,
8445 PermissionResult::Decision {
8446 decision: PermissionDecision::ApproveOnce(_),
8447 ..
8448 }
8449 ));
8450 }
8451
8452 #[test]
8453 fn session_config_enable_experimental_mode_serializes_when_set() {
8454 let cfg = SessionConfig::default().with_enable_experimental_mode(false);
8455 assert_eq!(cfg.enable_experimental_mode, Some(false));
8456
8457 let (wire, _runtime) = cfg
8458 .into_wire(Some(SessionId::from("experimental-mode")))
8459 .expect("enable_experimental_mode config has no duplicate handlers");
8460 assert_eq!(wire.is_experimental_mode, Some(false));
8461
8462 let json = serde_json::to_value(&wire).unwrap();
8463 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
8464 }
8465
8466 #[test]
8467 fn session_config_enable_experimental_mode_omitted_when_none() {
8468 let cfg = SessionConfig::default();
8469 assert_eq!(cfg.enable_experimental_mode, None);
8470
8471 let (wire, _runtime) = cfg
8472 .into_wire(Some(SessionId::from("no-experimental-mode")))
8473 .expect("default config has no duplicate handlers");
8474 assert_eq!(wire.is_experimental_mode, None);
8475
8476 let json = serde_json::to_value(&wire).unwrap();
8477 assert!(json.get("isExperimentalMode").is_none());
8478 }
8479
8480 #[test]
8481 fn resume_session_config_enable_experimental_mode_serializes_when_set() {
8482 let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
8483 .with_enable_experimental_mode(false);
8484 assert_eq!(cfg.enable_experimental_mode, Some(false));
8485
8486 let (wire, _runtime) = cfg
8487 .into_wire()
8488 .expect("resume enable_experimental_mode config has no duplicate handlers");
8489 assert_eq!(wire.is_experimental_mode, Some(false));
8490
8491 let json = serde_json::to_value(&wire).unwrap();
8492 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
8493 }
8494
8495 #[test]
8496 fn resume_session_config_enable_experimental_mode_omitted_when_none() {
8497 let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
8498 assert_eq!(cfg.enable_experimental_mode, None);
8499
8500 let (wire, _runtime) = cfg
8501 .into_wire()
8502 .expect("default resume config has no duplicate handlers");
8503 assert_eq!(wire.is_experimental_mode, None);
8504
8505 let json = serde_json::to_value(&wire).unwrap();
8506 assert!(json.get("isExperimentalMode").is_none());
8507 }
8508}
8509
8510#[cfg(test)]
8511mod is_terminal_tests {
8512 use super::Tool;
8513
8514 #[test]
8515 fn is_terminal_serializes_as_camel_case_when_set() {
8516 let tool = Tool {
8517 name: "clear_context".to_owned(),
8518 is_terminal: true,
8519 ..Default::default()
8520 };
8521 let value = serde_json::to_value(&tool).expect("tool serializes");
8522 assert_eq!(
8523 value.get("isTerminal"),
8524 Some(&serde_json::Value::Bool(true))
8525 );
8526 }
8527
8528 #[test]
8529 fn is_terminal_is_omitted_when_false() {
8530 let tool = Tool {
8531 name: "plain".to_owned(),
8532 ..Default::default()
8533 };
8534 let value = serde_json::to_value(&tool).expect("tool serializes");
8535 assert!(value.get("isTerminal").is_none());
8536 }
8537
8538 #[test]
8541 fn is_terminal_appears_in_debug_output() {
8542 let terminal = Tool {
8543 name: "clear_context".to_owned(),
8544 is_terminal: true,
8545 ..Default::default()
8546 };
8547 assert!(format!("{terminal:?}").contains("is_terminal: true"));
8548
8549 let plain = Tool {
8550 name: "plain".to_owned(),
8551 ..Default::default()
8552 };
8553 assert!(format!("{plain:?}").contains("is_terminal: false"));
8554 }
8555}
8556
8557#[cfg(test)]
8558mod refresh_custom_instructions_tests;