1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11
12use indexmap::IndexMap;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16use crate::canvas::{CanvasDeclaration, CanvasHandler};
17pub use crate::copilot_request_handler::{
18 CopilotHttpRequest, CopilotHttpResponse, CopilotHttpResponseBody, CopilotRequestContext,
19 CopilotRequestError, CopilotRequestHandler, CopilotRequestTransport, CopilotWebSocketForwarder,
20 CopilotWebSocketForwarderBuilder, CopilotWebSocketHandler, CopilotWebSocketMessage,
21 CopilotWebSocketResponse, WebSocketTransform, forward_http,
22};
23use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance};
24pub use crate::generated::api_types::{ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus};
26pub use crate::generated::session_events::AutoTier;
31use crate::generated::session_events::ReasoningSummary;
32pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig};
34use crate::github_token::GitHubTokenProvider;
35use crate::handler::{
36 AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler,
37 PermissionHandler, UserInputHandler,
38};
39use crate::hooks::SessionHooks;
40use crate::provider_token::BearerTokenProvider;
41pub use crate::session_fs::{
42 DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig,
43 SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult,
44 SessionFsSqliteQueryType, SessionFsSqliteTransactionError,
45 SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionStatement,
46};
47pub use crate::trace_context::{TraceContext, TraceContextProvider};
48use crate::transforms::SystemMessageTransform;
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53#[allow(dead_code)]
54#[non_exhaustive]
55pub(crate) enum ConnectionState {
56 Disconnected,
58 Connecting,
60 Connected,
62 Error,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
71#[non_exhaustive]
72pub enum SessionLifecycleEventType {
73 #[serde(rename = "session.created")]
75 Created,
76 #[serde(rename = "session.deleted")]
78 Deleted,
79 #[serde(rename = "session.updated")]
81 Updated,
82 #[serde(rename = "session.foreground")]
84 Foreground,
85 #[serde(rename = "session.background")]
87 Background,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct SessionLifecycleEventMetadata {
93 #[serde(rename = "startTime")]
95 pub start_time: String,
96 #[serde(rename = "modifiedTime")]
98 pub modified_time: String,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub summary: Option<String>,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct SessionLifecycleEvent {
108 #[serde(rename = "type")]
110 pub event_type: SessionLifecycleEventType,
111 #[serde(rename = "sessionId")]
113 pub session_id: SessionId,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub metadata: Option<SessionLifecycleEventMetadata>,
117}
118
119#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
125#[serde(transparent)]
126pub struct SessionId(String);
127
128impl SessionId {
129 pub fn new(id: impl Into<String>) -> Self {
131 Self(id.into())
132 }
133
134 pub fn as_str(&self) -> &str {
136 &self.0
137 }
138
139 pub fn into_inner(self) -> String {
141 self.0
142 }
143}
144
145impl std::ops::Deref for SessionId {
146 type Target = str;
147
148 fn deref(&self) -> &str {
149 &self.0
150 }
151}
152
153impl std::fmt::Display for SessionId {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 f.write_str(&self.0)
156 }
157}
158
159impl From<String> for SessionId {
160 fn from(s: String) -> Self {
161 Self(s)
162 }
163}
164
165impl From<&str> for SessionId {
166 fn from(s: &str) -> Self {
167 Self(s.to_owned())
168 }
169}
170
171impl AsRef<str> for SessionId {
172 fn as_ref(&self) -> &str {
173 &self.0
174 }
175}
176
177impl std::borrow::Borrow<str> for SessionId {
178 fn borrow(&self) -> &str {
179 &self.0
180 }
181}
182
183impl From<SessionId> for String {
184 fn from(id: SessionId) -> String {
185 id.0
186 }
187}
188
189impl PartialEq<str> for SessionId {
190 fn eq(&self, other: &str) -> bool {
191 self.0 == other
192 }
193}
194
195impl PartialEq<String> for SessionId {
196 fn eq(&self, other: &String) -> bool {
197 &self.0 == other
198 }
199}
200
201impl PartialEq<SessionId> for String {
202 fn eq(&self, other: &SessionId) -> bool {
203 self == &other.0
204 }
205}
206
207impl PartialEq<&str> for SessionId {
208 fn eq(&self, other: &&str) -> bool {
209 self.0 == *other
210 }
211}
212
213impl PartialEq<&SessionId> for SessionId {
214 fn eq(&self, other: &&SessionId) -> bool {
215 self.0 == other.0
216 }
217}
218
219impl PartialEq<SessionId> for &SessionId {
220 fn eq(&self, other: &SessionId) -> bool {
221 self.0 == other.0
222 }
223}
224
225#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
231#[serde(transparent)]
232pub struct RequestId(String);
233
234impl RequestId {
235 pub fn new(id: impl Into<String>) -> Self {
237 Self(id.into())
238 }
239
240 pub fn into_inner(self) -> String {
242 self.0
243 }
244}
245
246impl std::ops::Deref for RequestId {
247 type Target = str;
248
249 fn deref(&self) -> &str {
250 &self.0
251 }
252}
253
254impl std::fmt::Display for RequestId {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 f.write_str(&self.0)
257 }
258}
259
260impl From<String> for RequestId {
261 fn from(s: String) -> Self {
262 Self(s)
263 }
264}
265
266impl From<&str> for RequestId {
267 fn from(s: &str) -> Self {
268 Self(s.to_owned())
269 }
270}
271
272impl AsRef<str> for RequestId {
273 fn as_ref(&self) -> &str {
274 &self.0
275 }
276}
277
278impl std::borrow::Borrow<str> for RequestId {
279 fn borrow(&self) -> &str {
280 &self.0
281 }
282}
283
284impl From<RequestId> for String {
285 fn from(id: RequestId) -> String {
286 id.0
287 }
288}
289
290impl PartialEq<str> for RequestId {
291 fn eq(&self, other: &str) -> bool {
292 self.0 == other
293 }
294}
295
296impl PartialEq<String> for RequestId {
297 fn eq(&self, other: &String) -> bool {
298 &self.0 == other
299 }
300}
301
302impl PartialEq<RequestId> for String {
303 fn eq(&self, other: &RequestId) -> bool {
304 self == &other.0
305 }
306}
307
308impl PartialEq<&str> for RequestId {
309 fn eq(&self, other: &&str) -> bool {
310 self.0 == *other
311 }
312}
313
314#[derive(Clone, Default, Serialize, Deserialize)]
329#[serde(rename_all = "camelCase")]
330#[non_exhaustive]
331pub struct Tool {
332 pub name: String,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub namespaced_name: Option<String>,
338 #[serde(default)]
340 pub description: String,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub instructions: Option<String>,
344 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
346 pub parameters: IndexMap<String, Value>,
347 #[serde(default, skip_serializing_if = "is_false")]
351 pub overrides_built_in_tool: bool,
352 #[serde(default, skip_serializing_if = "is_false")]
356 pub skip_permission: bool,
357 #[serde(default, skip_serializing_if = "is_false")]
362 pub is_terminal: bool,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub defer: Option<DeferMode>,
370 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
375 pub metadata: IndexMap<String, Value>,
376 #[serde(skip)]
388 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
389}
390
391#[inline]
392fn is_false(b: &bool) -> bool {
393 !*b
394}
395
396#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
399#[serde(rename_all = "lowercase")]
400pub enum DeferMode {
401 Auto,
403 Never,
405}
406
407impl Tool {
408 pub fn new(name: impl Into<String>) -> Self {
428 Self {
429 name: name.into(),
430 ..Default::default()
431 }
432 }
433
434 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
437 self.namespaced_name = Some(namespaced_name.into());
438 self
439 }
440
441 pub fn with_description(mut self, description: impl Into<String>) -> Self {
443 self.description = description.into();
444 self
445 }
446
447 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
449 self.instructions = Some(instructions.into());
450 self
451 }
452
453 pub fn with_parameters(mut self, parameters: Value) -> Self {
467 self.parameters = crate::tool::tool_parameters(parameters);
468 self
469 }
470
471 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
475 self.overrides_built_in_tool = overrides;
476 self
477 }
478
479 pub fn with_skip_permission(mut self, skip: bool) -> Self {
483 self.skip_permission = skip;
484 self
485 }
486
487 #[must_use]
494 pub fn with_is_terminal(mut self, is_terminal: bool) -> Self {
495 self.is_terminal = is_terminal;
496 self
497 }
498
499 pub fn with_defer(mut self, defer: DeferMode) -> Self {
503 self.defer = Some(defer);
504 self
505 }
506
507 pub fn with_metadata(mut self, metadata: IndexMap<String, Value>) -> Self {
510 self.metadata = metadata;
511 self
512 }
513
514 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
518 self.handler = Some(handler);
519 self
520 }
521
522 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
527 self.handler.as_ref()
528 }
529}
530
531impl std::fmt::Debug for Tool {
532 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
533 f.debug_struct("Tool")
534 .field("name", &self.name)
535 .field("namespaced_name", &self.namespaced_name)
536 .field("description", &self.description)
537 .field("instructions", &self.instructions)
538 .field("parameters", &self.parameters)
539 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
540 .field("skip_permission", &self.skip_permission)
541 .field("is_terminal", &self.is_terminal)
542 .field("defer", &self.defer)
543 .field("metadata", &self.metadata)
544 .field(
545 "handler",
546 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
547 )
548 .finish()
549 }
550}
551
552#[non_exhaustive]
555#[derive(Debug, Clone)]
556pub struct CommandContext {
557 pub session_id: SessionId,
559 pub command: String,
561 pub command_name: String,
563 pub args: String,
565}
566
567#[async_trait::async_trait]
573pub trait CommandHandler: Send + Sync {
574 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
576}
577
578#[non_exhaustive]
584#[derive(Clone)]
585pub struct CommandDefinition {
586 pub name: String,
588 pub description: Option<String>,
590 pub handler: Arc<dyn CommandHandler>,
592}
593
594impl CommandDefinition {
595 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
598 Self {
599 name: name.into(),
600 description: None,
601 handler,
602 }
603 }
604
605 pub fn with_description(mut self, description: impl Into<String>) -> Self {
607 self.description = Some(description.into());
608 self
609 }
610}
611
612impl std::fmt::Debug for CommandDefinition {
613 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
614 f.debug_struct("CommandDefinition")
615 .field("name", &self.name)
616 .field("description", &self.description)
617 .field("handler", &"<set>")
618 .finish()
619 }
620}
621
622impl Serialize for CommandDefinition {
623 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
624 use serde::ser::SerializeStruct;
625 let mut state = serializer.serialize_struct("CommandDefinition", 2)?;
626 state.serialize_field("name", &self.name)?;
627 state.serialize_field("description", self.description.as_deref().unwrap_or(""))?;
628 state.end()
629 }
630}
631
632#[derive(Debug, Clone, Default, Serialize, Deserialize)]
639#[serde(rename_all = "camelCase")]
640#[non_exhaustive]
641pub struct CustomAgentConfig {
642 pub name: String,
644 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub display_name: Option<String>,
647 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub description: Option<String>,
650 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub tools: Option<Vec<String>>,
653 pub prompt: String,
655 #[serde(default, skip_serializing_if = "Option::is_none")]
657 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
658 #[serde(default, skip_serializing_if = "Option::is_none")]
660 pub infer: Option<bool>,
661 #[serde(default, skip_serializing_if = "Option::is_none")]
663 pub skills: Option<Vec<String>>,
664 #[serde(default, skip_serializing_if = "Option::is_none")]
669 pub model: Option<String>,
670 #[serde(default, skip_serializing_if = "Option::is_none")]
675 pub reasoning_effort: Option<String>,
676}
677
678impl CustomAgentConfig {
679 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
686 Self {
687 name: name.into(),
688 prompt: prompt.into(),
689 ..Self::default()
690 }
691 }
692
693 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
695 self.display_name = Some(display_name.into());
696 self
697 }
698
699 pub fn with_description(mut self, description: impl Into<String>) -> Self {
701 self.description = Some(description.into());
702 self
703 }
704
705 pub fn with_tools<I, S>(mut self, tools: I) -> Self
708 where
709 I: IntoIterator<Item = S>,
710 S: Into<String>,
711 {
712 self.tools = Some(tools.into_iter().map(Into::into).collect());
713 self
714 }
715
716 pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
718 self.mcp_servers = Some(mcp_servers);
719 self
720 }
721
722 pub fn with_infer(mut self, infer: bool) -> Self {
724 self.infer = Some(infer);
725 self
726 }
727
728 pub fn with_skills<I, S>(mut self, skills: I) -> Self
730 where
731 I: IntoIterator<Item = S>,
732 S: Into<String>,
733 {
734 self.skills = Some(skills.into_iter().map(Into::into).collect());
735 self
736 }
737
738 pub fn with_model(mut self, model: impl Into<String>) -> Self {
740 self.model = Some(model.into());
741 self
742 }
743
744 pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into<String>) -> Self {
746 self.reasoning_effort = Some(reasoning_effort.into());
747 self
748 }
749}
750
751#[derive(Debug, Clone, Default, Serialize, Deserialize)]
758#[serde(rename_all = "camelCase")]
759pub struct DefaultAgentConfig {
760 #[serde(default, skip_serializing_if = "Option::is_none")]
762 pub excluded_tools: Option<Vec<String>>,
763}
764
765#[derive(Debug, Clone, Default, Serialize, Deserialize)]
771#[serde(rename_all = "camelCase")]
772#[non_exhaustive]
773pub struct LargeToolOutputConfig {
774 #[serde(default, skip_serializing_if = "Option::is_none")]
776 pub enabled: Option<bool>,
777 #[serde(default, skip_serializing_if = "Option::is_none")]
780 pub max_size_bytes: Option<u64>,
781 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
784 pub output_directory: Option<PathBuf>,
785}
786
787impl LargeToolOutputConfig {
788 pub fn new() -> Self {
791 Self::default()
792 }
793
794 pub fn with_enabled(mut self, enabled: bool) -> Self {
796 self.enabled = Some(enabled);
797 self
798 }
799
800 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
802 self.max_size_bytes = Some(max_size_bytes);
803 self
804 }
805
806 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
808 self.output_directory = Some(output_directory.into());
809 self
810 }
811}
812
813#[derive(Debug, Clone, Default, Serialize, Deserialize)]
819#[serde(rename_all = "camelCase")]
820#[non_exhaustive]
821pub struct ToolSearchConfig {
822 #[serde(default, skip_serializing_if = "Option::is_none")]
824 pub enabled: Option<bool>,
825 #[serde(default, skip_serializing_if = "Option::is_none")]
828 pub defer_threshold: Option<u32>,
829}
830
831impl ToolSearchConfig {
832 pub fn new() -> Self {
835 Self::default()
836 }
837
838 pub fn with_enabled(mut self, enabled: bool) -> Self {
840 self.enabled = Some(enabled);
841 self
842 }
843
844 pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
847 self.defer_threshold = Some(defer_threshold);
848 self
849 }
850}
851
852#[derive(Debug, Clone, Default, Serialize, Deserialize)]
857#[serde(rename_all = "camelCase")]
858#[non_exhaustive]
859pub struct GitHubMcpToolConfig {
860 #[serde(default, skip_serializing_if = "Option::is_none")]
862 pub enable_all_tools: Option<bool>,
863 #[serde(default, skip_serializing_if = "Option::is_none")]
865 pub additional_toolsets: Option<Vec<String>>,
866 #[serde(default, skip_serializing_if = "Option::is_none")]
868 pub additional_tools: Option<Vec<String>>,
869 #[serde(default, skip_serializing_if = "Option::is_none")]
871 pub enable_insiders_mode: Option<bool>,
872 #[serde(default, skip_serializing_if = "Option::is_none")]
876 pub disable_form_deferral: Option<bool>,
877}
878
879impl GitHubMcpToolConfig {
880 pub fn new() -> Self {
882 Self::default()
883 }
884
885 pub fn with_enable_all_tools(mut self, value: bool) -> Self {
887 self.enable_all_tools = Some(value);
888 self
889 }
890
891 pub fn with_additional_toolsets<I, S>(mut self, values: I) -> Self
893 where
894 I: IntoIterator<Item = S>,
895 S: Into<String>,
896 {
897 self.additional_toolsets = Some(values.into_iter().map(Into::into).collect());
898 self
899 }
900
901 pub fn with_additional_tools<I, S>(mut self, values: I) -> Self
903 where
904 I: IntoIterator<Item = S>,
905 S: Into<String>,
906 {
907 self.additional_tools = Some(values.into_iter().map(Into::into).collect());
908 self
909 }
910
911 pub fn with_enable_insiders_mode(mut self, value: bool) -> Self {
913 self.enable_insiders_mode = Some(value);
914 self
915 }
916
917 pub fn with_disable_form_deferral(mut self, value: bool) -> Self {
921 self.disable_form_deferral = Some(value);
922 self
923 }
924}
925
926#[derive(Debug, Clone, Default, Serialize, Deserialize)]
933#[serde(rename_all = "camelCase")]
934#[non_exhaustive]
935pub struct InfiniteSessionConfig {
936 #[serde(default, skip_serializing_if = "Option::is_none")]
938 pub enabled: Option<bool>,
939 #[serde(default, skip_serializing_if = "Option::is_none")]
942 pub background_compaction_threshold: Option<f64>,
943 #[serde(default, skip_serializing_if = "Option::is_none")]
946 pub buffer_exhaustion_threshold: Option<f64>,
947}
948
949impl InfiniteSessionConfig {
950 pub fn new() -> Self {
953 Self::default()
954 }
955
956 pub fn with_enabled(mut self, enabled: bool) -> Self {
959 self.enabled = Some(enabled);
960 self
961 }
962
963 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
966 self.background_compaction_threshold = Some(threshold);
967 self
968 }
969
970 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
973 self.buffer_exhaustion_threshold = Some(threshold);
974 self
975 }
976}
977
978#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
989#[serde(rename_all = "camelCase")]
990#[non_exhaustive]
991pub struct MemoryConfiguration {
992 pub enabled: bool,
994}
995
996impl MemoryConfiguration {
997 pub fn enabled() -> Self {
999 Self { enabled: true }
1000 }
1001
1002 pub fn disabled() -> Self {
1004 Self { enabled: false }
1005 }
1006
1007 pub fn with_enabled(mut self, enabled: bool) -> Self {
1009 self.enabled = enabled;
1010 self
1011 }
1012}
1013
1014#[derive(Debug, Clone, Serialize, Deserialize)]
1016#[serde(rename_all = "camelCase")]
1017#[non_exhaustive]
1018pub struct CloudSessionRepository {
1019 pub owner: String,
1021 pub name: String,
1023 #[serde(skip_serializing_if = "Option::is_none")]
1025 pub branch: Option<String>,
1026}
1027
1028impl CloudSessionRepository {
1029 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
1031 Self {
1032 owner: owner.into(),
1033 name: name.into(),
1034 branch: None,
1035 }
1036 }
1037
1038 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
1040 self.branch = Some(branch.into());
1041 self
1042 }
1043}
1044
1045#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1047#[serde(rename_all = "camelCase")]
1048#[non_exhaustive]
1049pub struct CloudSessionOptions {
1050 #[serde(skip_serializing_if = "Option::is_none")]
1052 pub repository: Option<CloudSessionRepository>,
1053}
1054
1055impl CloudSessionOptions {
1056 pub fn with_repository(repository: CloudSessionRepository) -> Self {
1058 Self {
1059 repository: Some(repository),
1060 }
1061 }
1062}
1063
1064#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1066#[serde(rename_all = "camelCase")]
1067pub struct ExtensionInfo {
1068 pub source: String,
1070 pub name: String,
1072}
1073
1074impl ExtensionInfo {
1075 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
1077 Self {
1078 source: source.into(),
1079 name: name.into(),
1080 }
1081 }
1082}
1083
1084#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1095#[serde(rename_all = "camelCase")]
1096pub struct CanvasProviderIdentity {
1097 pub id: String,
1099 #[serde(skip_serializing_if = "Option::is_none")]
1101 pub name: Option<String>,
1102}
1103
1104impl CanvasProviderIdentity {
1105 pub fn new(id: impl Into<String>) -> Self {
1107 Self {
1108 id: id.into(),
1109 name: None,
1110 }
1111 }
1112
1113 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1115 self.name = Some(name.into());
1116 self
1117 }
1118}
1119
1120#[derive(Debug, Clone, Serialize, Deserialize)]
1154#[serde(tag = "type", rename_all = "lowercase")]
1155#[non_exhaustive]
1156pub enum McpServerConfig {
1157 #[serde(alias = "local")]
1161 Stdio(McpStdioServerConfig),
1162 Http(McpHttpServerConfig),
1164 Sse(McpHttpServerConfig),
1166}
1167
1168#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1172#[serde(rename_all = "camelCase")]
1173pub struct McpStdioServerConfig {
1174 #[serde(default, skip_serializing_if = "Option::is_none")]
1180 pub tools: Option<Vec<String>>,
1181 #[serde(default, skip_serializing_if = "Option::is_none")]
1183 pub timeout: Option<i64>,
1184 pub command: String,
1186 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1188 pub args: Vec<String>,
1189 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1192 pub env: HashMap<String, String>,
1193 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1195 pub working_directory: Option<String>,
1196}
1197
1198#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1202#[serde(rename_all = "camelCase")]
1203pub struct McpHttpServerConfig {
1204 #[serde(default, skip_serializing_if = "Option::is_none")]
1210 pub tools: Option<Vec<String>>,
1211 #[serde(default, skip_serializing_if = "Option::is_none")]
1213 pub timeout: Option<i64>,
1214 pub url: String,
1216 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1218 pub headers: HashMap<String, String>,
1219}
1220
1221#[derive(Clone, Default, Serialize, Deserialize)]
1227#[serde(rename_all = "camelCase")]
1228#[non_exhaustive]
1229pub struct ProviderConfig {
1230 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1233 pub provider_type: Option<String>,
1234 #[serde(default, skip_serializing_if = "Option::is_none")]
1237 pub wire_api: Option<String>,
1238 #[serde(default, skip_serializing_if = "Option::is_none")]
1243 pub transport: Option<String>,
1244 pub base_url: String,
1246 #[serde(default, skip_serializing_if = "Option::is_none")]
1248 pub api_key: Option<String>,
1249 #[serde(default, skip_serializing_if = "Option::is_none")]
1253 pub bearer_token: Option<String>,
1254 #[serde(skip)]
1257 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1258 #[serde(default, skip_serializing_if = "Option::is_none")]
1259 pub(crate) has_bearer_token_provider: Option<bool>,
1260 #[serde(default, skip_serializing_if = "Option::is_none")]
1262 pub azure: Option<AzureProviderOptions>,
1263 #[serde(default, skip_serializing_if = "Option::is_none")]
1265 pub headers: Option<HashMap<String, String>>,
1266 #[serde(default, skip_serializing_if = "Option::is_none")]
1270 pub model_id: Option<String>,
1271 #[serde(default, skip_serializing_if = "Option::is_none")]
1278 pub wire_model: Option<String>,
1279 #[serde(default, skip_serializing_if = "Option::is_none")]
1284 pub max_prompt_tokens: Option<i64>,
1285 #[serde(default, skip_serializing_if = "Option::is_none")]
1288 pub max_output_tokens: Option<i64>,
1289}
1290
1291impl std::fmt::Debug for ProviderConfig {
1292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1293 f.debug_struct("ProviderConfig")
1294 .field("provider_type", &self.provider_type)
1295 .field("wire_api", &self.wire_api)
1296 .field("transport", &self.transport)
1297 .field("base_url", &self.base_url)
1298 .field("api_key", &self.api_key)
1299 .field("bearer_token", &self.bearer_token)
1300 .field(
1301 "bearer_token_provider",
1302 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1303 )
1304 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1305 .field("azure", &self.azure)
1306 .field("headers", &self.headers)
1307 .field("model_id", &self.model_id)
1308 .field("wire_model", &self.wire_model)
1309 .field("max_prompt_tokens", &self.max_prompt_tokens)
1310 .field("max_output_tokens", &self.max_output_tokens)
1311 .finish()
1312 }
1313}
1314
1315impl ProviderConfig {
1316 pub fn new(base_url: impl Into<String>) -> Self {
1319 Self {
1320 base_url: base_url.into(),
1321 ..Self::default()
1322 }
1323 }
1324
1325 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1327 self.provider_type = Some(provider_type.into());
1328 self
1329 }
1330
1331 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1333 self.wire_api = Some(wire_api.into());
1334 self
1335 }
1336
1337 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1340 self.transport = Some(transport.into());
1341 self
1342 }
1343
1344 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1346 self.api_key = Some(api_key.into());
1347 self
1348 }
1349
1350 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1353 self.bearer_token = Some(bearer_token.into());
1354 self
1355 }
1356
1357 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1363 self.bearer_token_provider = Some(provider);
1364 self
1365 }
1366
1367 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1369 self.azure = Some(azure);
1370 self
1371 }
1372
1373 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1375 self.headers = Some(headers);
1376 self
1377 }
1378
1379 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1382 self.model_id = Some(model_id.into());
1383 self
1384 }
1385
1386 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1391 self.wire_model = Some(wire_model.into());
1392 self
1393 }
1394
1395 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1399 self.max_prompt_tokens = Some(max);
1400 self
1401 }
1402
1403 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1406 self.max_output_tokens = Some(max);
1407 self
1408 }
1409}
1410
1411#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1424#[serde(rename_all = "camelCase")]
1425#[non_exhaustive]
1426pub struct CapiSessionOptions {
1427 #[serde(default, skip_serializing_if = "Option::is_none")]
1438 pub auto_tier: Option<AutoTier>,
1439
1440 #[serde(default, skip_serializing_if = "Option::is_none")]
1446 pub enable_web_socket_responses: Option<bool>,
1447}
1448
1449impl CapiSessionOptions {
1450 pub fn new() -> Self {
1452 Self::default()
1453 }
1454
1455 pub fn with_auto_tier(mut self, auto_tier: AutoTier) -> Self {
1457 self.auto_tier = Some(auto_tier);
1458 self
1459 }
1460
1461 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1463 self.enable_web_socket_responses = Some(enable);
1464 self
1465 }
1466}
1467
1468#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1470#[serde(rename_all = "camelCase")]
1471pub struct AzureProviderOptions {
1472 #[serde(default, skip_serializing_if = "Option::is_none")]
1474 pub api_version: Option<String>,
1475}
1476
1477#[derive(Clone, Default, Serialize, Deserialize)]
1488#[serde(rename_all = "camelCase")]
1489#[non_exhaustive]
1490pub struct NamedProviderConfig {
1491 pub name: String,
1494 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1497 pub provider_type: Option<String>,
1498 #[serde(default, skip_serializing_if = "Option::is_none")]
1501 pub wire_api: Option<String>,
1502 pub base_url: String,
1504 #[serde(default, skip_serializing_if = "Option::is_none")]
1506 pub api_key: Option<String>,
1507 #[serde(default, skip_serializing_if = "Option::is_none")]
1510 pub bearer_token: Option<String>,
1511 #[serde(skip)]
1514 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1515 #[serde(default, skip_serializing_if = "Option::is_none")]
1516 pub(crate) has_bearer_token_provider: Option<bool>,
1517 #[serde(default, skip_serializing_if = "Option::is_none")]
1519 pub azure: Option<AzureProviderOptions>,
1520 #[serde(default, skip_serializing_if = "Option::is_none")]
1522 pub headers: Option<HashMap<String, String>>,
1523}
1524
1525impl std::fmt::Debug for NamedProviderConfig {
1526 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1527 f.debug_struct("NamedProviderConfig")
1528 .field("name", &self.name)
1529 .field("provider_type", &self.provider_type)
1530 .field("wire_api", &self.wire_api)
1531 .field("base_url", &self.base_url)
1532 .field("api_key", &self.api_key)
1533 .field("bearer_token", &self.bearer_token)
1534 .field(
1535 "bearer_token_provider",
1536 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1537 )
1538 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1539 .field("azure", &self.azure)
1540 .field("headers", &self.headers)
1541 .finish()
1542 }
1543}
1544
1545impl NamedProviderConfig {
1546 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1549 Self {
1550 name: name.into(),
1551 base_url: base_url.into(),
1552 ..Self::default()
1553 }
1554 }
1555
1556 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1558 self.provider_type = Some(provider_type.into());
1559 self
1560 }
1561
1562 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1564 self.wire_api = Some(wire_api.into());
1565 self
1566 }
1567
1568 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1570 self.api_key = Some(api_key.into());
1571 self
1572 }
1573
1574 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1577 self.bearer_token = Some(bearer_token.into());
1578 self
1579 }
1580
1581 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1587 self.bearer_token_provider = Some(provider);
1588 self
1589 }
1590
1591 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1593 self.azure = Some(azure);
1594 self
1595 }
1596
1597 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1599 self.headers = Some(headers);
1600 self
1601 }
1602}
1603
1604fn prepare_bearer_token_providers(
1605 provider: &mut Option<ProviderConfig>,
1606 providers: &mut Option<Vec<NamedProviderConfig>>,
1607) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1608 let mut bearer_token_providers = HashMap::new();
1609
1610 if let Some(provider) = provider.as_mut()
1611 && let Some(token_provider) = provider.bearer_token_provider.take()
1612 {
1613 provider.has_bearer_token_provider = Some(true);
1614 bearer_token_providers.insert("default".to_string(), token_provider);
1615 }
1616
1617 if let Some(providers) = providers.as_mut() {
1618 for provider in providers {
1619 if let Some(token_provider) = provider.bearer_token_provider.take() {
1620 provider.has_bearer_token_provider = Some(true);
1621 bearer_token_providers.insert(provider.name.clone(), token_provider);
1622 }
1623 }
1624 }
1625
1626 bearer_token_providers
1627}
1628
1629#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1637#[serde(rename_all = "camelCase")]
1638#[non_exhaustive]
1639pub struct ProviderModelConfig {
1640 pub id: String,
1643 pub provider: String,
1645 #[serde(default, skip_serializing_if = "Option::is_none")]
1648 pub wire_model: Option<String>,
1649 #[serde(default, skip_serializing_if = "Option::is_none")]
1652 pub model_id: Option<String>,
1653 #[serde(default, skip_serializing_if = "Option::is_none")]
1655 pub name: Option<String>,
1656 #[serde(default, skip_serializing_if = "Option::is_none")]
1658 pub max_prompt_tokens: Option<i64>,
1659 #[serde(default, skip_serializing_if = "Option::is_none")]
1661 pub max_context_window_tokens: Option<i64>,
1662 #[serde(default, skip_serializing_if = "Option::is_none")]
1664 pub max_output_tokens: Option<i64>,
1665 #[serde(default, skip_serializing_if = "Option::is_none")]
1668 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1669}
1670
1671impl ProviderModelConfig {
1672 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1675 Self {
1676 id: id.into(),
1677 provider: provider.into(),
1678 ..Self::default()
1679 }
1680 }
1681
1682 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1684 self.wire_model = Some(wire_model.into());
1685 self
1686 }
1687
1688 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1691 self.model_id = Some(model_id.into());
1692 self
1693 }
1694
1695 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1697 self.name = Some(name.into());
1698 self
1699 }
1700
1701 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1703 self.max_prompt_tokens = Some(max);
1704 self
1705 }
1706
1707 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1709 self.max_context_window_tokens = Some(max);
1710 self
1711 }
1712
1713 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1715 self.max_output_tokens = Some(max);
1716 self
1717 }
1718
1719 pub fn with_capabilities(
1721 mut self,
1722 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1723 ) -> Self {
1724 self.capabilities = Some(capabilities);
1725 self
1726 }
1727}
1728
1729#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1733#[serde(untagged)]
1734pub enum ExpFlagValue {
1735 Bool(bool),
1737 Integer(i64),
1739 Float(f64),
1741 String(String),
1743 Null,
1745}
1746
1747#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1751#[serde(rename_all = "PascalCase")]
1752pub struct ExpConfigEntry {
1753 pub id: String,
1755 pub parameters: HashMap<String, ExpFlagValue>,
1757}
1758
1759#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1765#[serde(rename_all = "PascalCase")]
1766pub struct CopilotExpAssignmentResponse {
1767 #[serde(default)]
1769 pub features: Vec<String>,
1770 #[serde(default)]
1772 pub flights: HashMap<String, String>,
1773 #[serde(default)]
1775 pub configs: Vec<ExpConfigEntry>,
1776 #[serde(default, skip_serializing_if = "Option::is_none")]
1778 pub parameter_groups: Option<Value>,
1779 #[serde(default, skip_serializing_if = "Option::is_none")]
1781 pub flighting_version: Option<i64>,
1782 #[serde(default, skip_serializing_if = "Option::is_none")]
1784 pub impression_id: Option<String>,
1785 #[serde(default)]
1787 pub assignment_context: String,
1788}
1789
1790pub struct DisableBypassPermissionsModes;
1792
1793impl DisableBypassPermissionsModes {
1794 pub const ALLOW_AUTO_ONLY: &'static str = "allow-auto-only";
1796 pub const DISABLE: &'static str = "disable";
1798}
1799
1800#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1809#[serde(rename_all = "camelCase")]
1810#[non_exhaustive]
1811pub struct ManagedSettingsPermissions {
1812 #[serde(default, skip_serializing_if = "Option::is_none")]
1816 pub disable_bypass_permissions_mode: Option<String>,
1817 #[serde(default, skip_serializing_if = "Option::is_none")]
1819 pub deny: Option<Vec<String>>,
1820 #[serde(default, skip_serializing_if = "Option::is_none")]
1822 pub ask: Option<Vec<String>>,
1823 #[serde(default, skip_serializing_if = "Option::is_none")]
1825 pub allow: Option<Vec<String>>,
1826}
1827
1828impl ManagedSettingsPermissions {
1829 pub fn with_disable_bypass_permissions_mode(mut self, value: impl Into<String>) -> Self {
1831 self.disable_bypass_permissions_mode = Some(value.into());
1832 self
1833 }
1834
1835 pub fn with_deny(mut self, rules: Vec<String>) -> Self {
1837 self.deny = Some(rules);
1838 self
1839 }
1840
1841 pub fn with_ask(mut self, rules: Vec<String>) -> Self {
1843 self.ask = Some(rules);
1844 self
1845 }
1846
1847 pub fn with_allow(mut self, rules: Vec<String>) -> Self {
1849 self.allow = Some(rules);
1850 self
1851 }
1852}
1853
1854#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1864#[serde(rename_all = "camelCase")]
1865#[non_exhaustive]
1866pub struct ManagedSettings {
1867 #[serde(default, skip_serializing_if = "Option::is_none")]
1869 pub permissions: Option<ManagedSettingsPermissions>,
1870}
1871
1872impl ManagedSettings {
1873 pub fn with_permissions(mut self, permissions: ManagedSettingsPermissions) -> Self {
1875 self.permissions = Some(permissions);
1876 self
1877 }
1878}
1879
1880#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1882#[serde(rename_all = "lowercase")]
1883#[non_exhaustive]
1884pub enum AskUserVariant {
1885 #[default]
1887 Legacy,
1888 Elicitation,
1890}
1891
1892#[derive(Clone)]
1944#[non_exhaustive]
1945pub struct SessionConfig {
1946 pub session_id: Option<SessionId>,
1948 pub model: Option<String>,
1950 pub allowed_models: Option<Vec<String>>,
1955 pub client_name: Option<String>,
1957 pub reasoning_effort: Option<String>,
1959 pub reasoning_summary: Option<ReasoningSummary>,
1963 pub context_tier: Option<String>,
1966 pub streaming: Option<bool>,
1968 pub system_message: Option<SystemMessageConfig>,
1970 pub ask_user_variant: Option<AskUserVariant>,
1975 pub tools: Option<Vec<Tool>>,
1977 pub canvases: Option<Vec<CanvasDeclaration>>,
1979 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1984 pub request_canvas_renderer: Option<bool>,
1986 pub request_extensions: Option<bool>,
1988 pub extension_sdk_path: Option<String>,
1992 pub extension_info: Option<ExtensionInfo>,
1994 pub canvas_provider: Option<CanvasProviderIdentity>,
1997 pub available_tools: Option<Vec<String>>,
1999 pub excluded_tools: Option<Vec<String>>,
2001 pub excluded_builtin_agents: Option<Vec<String>>,
2007 pub included_builtin_skills: Option<Vec<String>>,
2011 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
2013 pub mcp_oauth_token_storage: Option<String>,
2022 pub auth_client_id_metadata_url: Option<String>,
2029 pub enable_config_discovery: Option<bool>,
2032 pub skip_embedding_retrieval: Option<bool>,
2034 pub embedding_cache_storage: Option<String>,
2037 pub organization_custom_instructions: Option<String>,
2039 pub enable_on_demand_instruction_discovery: Option<bool>,
2041 pub enable_file_hooks: Option<bool>,
2043 pub enable_host_git_operations: Option<bool>,
2045 pub enable_session_store: Option<bool>,
2047 pub enable_skills: Option<bool>,
2049 pub enable_mcp_apps: Option<bool>,
2076 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
2081 pub skill_directories: Option<Vec<PathBuf>>,
2083 pub instruction_directories: Option<Vec<PathBuf>>,
2086 pub plugin_directories: Option<Vec<PathBuf>>,
2088 pub large_output: Option<LargeToolOutputConfig>,
2090 pub tool_search: Option<ToolSearchConfig>,
2094 pub disabled_skills: Option<Vec<String>>,
2097 pub disabled_mcp_servers: Option<Vec<String>>,
2101 pub hooks: Option<bool>,
2105 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2107 pub default_agent: Option<DefaultAgentConfig>,
2111 pub agent: Option<String>,
2114 pub infinite_sessions: Option<InfiniteSessionConfig>,
2117 pub provider: Option<ProviderConfig>,
2121 pub capi: Option<CapiSessionOptions>,
2127 pub providers: Option<Vec<NamedProviderConfig>>,
2134 pub models: Option<Vec<ProviderModelConfig>>,
2140 pub enable_session_telemetry: Option<bool>,
2148 pub enable_citations: Option<bool>,
2150 pub enable_file_change_tracking: Option<bool>,
2153 pub session_limits: Option<SessionLimitsConfig>,
2155 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2158 pub memory: Option<MemoryConfiguration>,
2160 pub config_directory: Option<PathBuf>,
2163 pub working_directory: Option<PathBuf>,
2166 pub additional_directories: Option<Vec<PathBuf>>,
2170 pub github_token: Option<String>,
2176 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2182 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2188 pub cloud: Option<CloudSessionOptions>,
2191 pub include_sub_agent_streaming_events: Option<bool>,
2195 pub commands: Option<Vec<CommandDefinition>>,
2199 pub feature_flags: Option<HashMap<String, bool>>,
2205 #[doc(hidden)]
2212 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
2213 pub enable_managed_settings: Option<bool>,
2221 pub managed_settings: Option<ManagedSettings>,
2230 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2235 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2239 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2242 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2245 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2249 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2252 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2255 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2259 pub(crate) permission_policy: Option<crate::permission::Policy>,
2263 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2268 pub skip_custom_instructions: Option<bool>,
2272 pub custom_agents_local_only: Option<bool>,
2276 pub enable_experimental_mode: Option<bool>,
2281 pub coauthor_enabled: Option<bool>,
2285 pub manage_schedule_enabled: Option<bool>,
2289 pub event_buffer_capacity: Option<usize>,
2306}
2307
2308impl std::fmt::Debug for SessionConfig {
2309 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2310 f.debug_struct("SessionConfig")
2311 .field("session_id", &self.session_id)
2312 .field("model", &self.model)
2313 .field("allowed_models", &self.allowed_models)
2314 .field("client_name", &self.client_name)
2315 .field("reasoning_effort", &self.reasoning_effort)
2316 .field("reasoning_summary", &self.reasoning_summary)
2317 .field("context_tier", &self.context_tier)
2318 .field("streaming", &self.streaming)
2319 .field("system_message", &self.system_message)
2320 .field("ask_user_variant", &self.ask_user_variant)
2321 .field("tools", &self.tools)
2322 .field("canvases", &self.canvases)
2323 .field(
2324 "canvas_handler",
2325 &self.canvas_handler.as_ref().map(|_| "<set>"),
2326 )
2327 .field("request_canvas_renderer", &self.request_canvas_renderer)
2328 .field("request_extensions", &self.request_extensions)
2329 .field("extension_sdk_path", &self.extension_sdk_path)
2330 .field("extension_info", &self.extension_info)
2331 .field("canvas_provider", &self.canvas_provider)
2332 .field("available_tools", &self.available_tools)
2333 .field("excluded_tools", &self.excluded_tools)
2334 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2335 .field("included_builtin_skills", &self.included_builtin_skills)
2336 .field("mcp_servers", &self.mcp_servers)
2337 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2338 .field(
2339 "auth_client_id_metadata_url",
2340 &self.auth_client_id_metadata_url,
2341 )
2342 .field("embedding_cache_storage", &self.embedding_cache_storage)
2343 .field("enable_config_discovery", &self.enable_config_discovery)
2344 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2345 .field(
2346 "organization_custom_instructions",
2347 &self
2348 .organization_custom_instructions
2349 .as_ref()
2350 .map(|_| "<redacted>"),
2351 )
2352 .field(
2353 "enable_on_demand_instruction_discovery",
2354 &self.enable_on_demand_instruction_discovery,
2355 )
2356 .field("enable_file_hooks", &self.enable_file_hooks)
2357 .field(
2358 "enable_host_git_operations",
2359 &self.enable_host_git_operations,
2360 )
2361 .field("enable_session_store", &self.enable_session_store)
2362 .field("enable_skills", &self.enable_skills)
2363 .field("enable_mcp_apps", &self.enable_mcp_apps)
2364 .field("skill_directories", &self.skill_directories)
2365 .field("instruction_directories", &self.instruction_directories)
2366 .field("plugin_directories", &self.plugin_directories)
2367 .field("large_output", &self.large_output)
2368 .field("tool_search", &self.tool_search)
2369 .field("disabled_skills", &self.disabled_skills)
2370 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
2371 .field("hooks", &self.hooks)
2372 .field("custom_agents", &self.custom_agents)
2373 .field("default_agent", &self.default_agent)
2374 .field("agent", &self.agent)
2375 .field("infinite_sessions", &self.infinite_sessions)
2376 .field("provider", &self.provider)
2377 .field("capi", &self.capi)
2378 .field("enable_session_telemetry", &self.enable_session_telemetry)
2379 .field("enable_citations", &self.enable_citations)
2380 .field(
2381 "enable_file_change_tracking",
2382 &self.enable_file_change_tracking,
2383 )
2384 .field("session_limits", &self.session_limits)
2385 .field("model_capabilities", &self.model_capabilities)
2386 .field("memory", &self.memory)
2387 .field("config_directory", &self.config_directory)
2388 .field("working_directory", &self.working_directory)
2389 .field("additional_directories", &self.additional_directories)
2390 .field(
2391 "github_token",
2392 &self.github_token.as_ref().map(|_| "<redacted>"),
2393 )
2394 .field(
2395 "github_token_provider",
2396 &self.github_token_provider.as_ref().map(|_| "<set>"),
2397 )
2398 .field("remote_session", &self.remote_session)
2399 .field("cloud", &self.cloud)
2400 .field(
2401 "include_sub_agent_streaming_events",
2402 &self.include_sub_agent_streaming_events,
2403 )
2404 .field("commands", &self.commands)
2405 .field("feature_flags", &self.feature_flags)
2406 .field("exp_assignments", &self.exp_assignments)
2407 .field("enable_managed_settings", &self.enable_managed_settings)
2408 .field("enable_experimental_mode", &self.enable_experimental_mode)
2409 .field("managed_settings", &self.managed_settings)
2410 .field(
2411 "session_fs_provider",
2412 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2413 )
2414 .field(
2415 "permission_handler",
2416 &self.permission_handler.as_ref().map(|_| "<set>"),
2417 )
2418 .field(
2419 "elicitation_handler",
2420 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2421 )
2422 .field(
2423 "mcp_auth_handler",
2424 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2425 )
2426 .field(
2427 "user_input_handler",
2428 &self.user_input_handler.as_ref().map(|_| "<set>"),
2429 )
2430 .field(
2431 "exit_plan_mode_handler",
2432 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2433 )
2434 .field(
2435 "auto_mode_switch_handler",
2436 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2437 )
2438 .field(
2439 "hooks_handler",
2440 &self.hooks_handler.as_ref().map(|_| "<set>"),
2441 )
2442 .field(
2443 "system_message_transform",
2444 &self.system_message_transform.as_ref().map(|_| "<set>"),
2445 )
2446 .field("event_buffer_capacity", &self.event_buffer_capacity)
2447 .finish()
2448 }
2449}
2450
2451impl Default for SessionConfig {
2452 fn default() -> Self {
2458 Self {
2459 session_id: None,
2460 model: None,
2461 allowed_models: None,
2462 client_name: None,
2463 reasoning_effort: None,
2464 reasoning_summary: None,
2465 context_tier: None,
2466 streaming: None,
2467 system_message: None,
2468 ask_user_variant: None,
2469 tools: None,
2470 canvases: None,
2471 canvas_handler: None,
2472 request_canvas_renderer: None,
2473 request_extensions: None,
2474 extension_sdk_path: None,
2475 extension_info: None,
2476 canvas_provider: None,
2477 available_tools: None,
2478 excluded_tools: None,
2479 excluded_builtin_agents: None,
2480 included_builtin_skills: None,
2481 mcp_servers: None,
2482 mcp_oauth_token_storage: None,
2483 auth_client_id_metadata_url: None,
2484 enable_config_discovery: None,
2485 skip_embedding_retrieval: None,
2486 organization_custom_instructions: None,
2487 enable_on_demand_instruction_discovery: None,
2488 enable_file_hooks: None,
2489 enable_host_git_operations: None,
2490 enable_session_store: None,
2491 enable_skills: None,
2492 embedding_cache_storage: None,
2493 enable_mcp_apps: None,
2494 github_mcp_tool_config: None,
2495 skill_directories: None,
2496 instruction_directories: None,
2497 plugin_directories: None,
2498 large_output: None,
2499 tool_search: None,
2500 disabled_skills: None,
2501 disabled_mcp_servers: None,
2502 hooks: None,
2503 custom_agents: None,
2504 default_agent: None,
2505 agent: None,
2506 infinite_sessions: None,
2507 provider: None,
2508 capi: None,
2509 providers: None,
2510 models: None,
2511 enable_session_telemetry: None,
2512 enable_citations: None,
2513 enable_file_change_tracking: None,
2514 session_limits: None,
2515 model_capabilities: None,
2516 memory: None,
2517 config_directory: None,
2518 working_directory: None,
2519 additional_directories: None,
2520 github_token: None,
2521 github_token_provider: None,
2522 remote_session: None,
2523 cloud: None,
2524 include_sub_agent_streaming_events: None,
2525 commands: None,
2526 feature_flags: None,
2527 exp_assignments: None,
2528 enable_managed_settings: None,
2529 managed_settings: None,
2530 session_fs_provider: None,
2531 permission_handler: None,
2532 elicitation_handler: None,
2533 mcp_auth_handler: None,
2534 user_input_handler: None,
2535 exit_plan_mode_handler: None,
2536 auto_mode_switch_handler: None,
2537 hooks_handler: None,
2538 permission_policy: None,
2539 system_message_transform: None,
2540 skip_custom_instructions: None,
2541 custom_agents_local_only: None,
2542 enable_experimental_mode: None,
2543 coauthor_enabled: None,
2544 manage_schedule_enabled: None,
2545 event_buffer_capacity: None,
2546 }
2547 }
2548}
2549
2550pub(crate) struct SessionConfigRuntime {
2556 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2557 pub permission_policy: Option<crate::permission::Policy>,
2558 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2559 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2560 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2561 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2562 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2563 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2564 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2565 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2566 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2567 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2568 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2569 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2570 pub commands: Option<Vec<CommandDefinition>>,
2571}
2572
2573impl SessionConfig {
2574 pub(crate) fn into_wire(
2586 mut self,
2587 session_id: Option<SessionId>,
2588 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2589 if self.github_token.is_some() && self.github_token_provider.is_some() {
2590 return Err(crate::Error::with_message(
2591 crate::ErrorKind::InvalidConfig,
2592 "github_token and github_token_provider are mutually exclusive",
2593 ));
2594 }
2595 let permission_active =
2596 self.permission_handler.is_some() || self.permission_policy.is_some();
2597 let request_user_input = self.user_input_handler.is_some();
2598 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2599 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2600 let request_elicitation = self.elicitation_handler.is_some();
2601 let hooks_flag = self.hooks_handler.is_some();
2602
2603 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2604 if let Some(tools) = self.tools.as_mut() {
2605 for tool in tools.iter_mut() {
2606 if let Some(handler) = tool.handler.take()
2607 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2608 {
2609 return Err(crate::Error::with_message(
2610 crate::ErrorKind::InvalidConfig,
2611 format!("duplicate tool handler registered for name {:?}", tool.name),
2612 ));
2613 }
2614 }
2615 }
2616
2617 let wire_commands = self.commands.as_ref().map(|cmds| {
2618 cmds.iter()
2619 .map(|c| crate::wire::CommandWireDefinition {
2620 name: c.name.clone(),
2621 description: c.description.clone().unwrap_or_default(),
2622 })
2623 .collect()
2624 });
2625 let wire_canvases = self.canvases.clone();
2626 let canvas_handler = self.canvas_handler.clone();
2627 let bearer_token_providers =
2628 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2629
2630 let wire = crate::wire::SessionCreateWire {
2631 session_id,
2632 model: self.model,
2633 allowed_models: self.allowed_models,
2634 client_name: self.client_name,
2635 reasoning_effort: self.reasoning_effort,
2636 reasoning_summary: self.reasoning_summary,
2637 context_tier: self.context_tier,
2638 streaming: self.streaming,
2639 system_message: self.system_message,
2640 ask_user_variant: self.ask_user_variant,
2641 tools: self.tools,
2642 canvases: wire_canvases,
2643 request_canvas_renderer: self.request_canvas_renderer,
2644 request_extensions: self.request_extensions,
2645 extension_sdk_path: self.extension_sdk_path,
2646 extension_info: self.extension_info,
2647 canvas_provider: self.canvas_provider,
2648 available_tools: self.available_tools,
2649 excluded_tools: self.excluded_tools,
2650 excluded_builtin_agents: self.excluded_builtin_agents,
2651 tool_filter_precedence: "excluded",
2652 mcp_servers: self.mcp_servers,
2653 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2654 auth_client_id_metadata_url: self.auth_client_id_metadata_url,
2655 embedding_cache_storage: self.embedding_cache_storage,
2656 env_value_mode: "direct",
2657 enable_config_discovery: self.enable_config_discovery,
2658 skip_embedding_retrieval: self.skip_embedding_retrieval,
2659 organization_custom_instructions: self.organization_custom_instructions,
2660 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2661 enable_file_hooks: self.enable_file_hooks,
2662 enable_host_git_operations: self.enable_host_git_operations,
2663 enable_session_store: self.enable_session_store,
2664 enable_skills: self.enable_skills,
2665 request_user_input,
2666 request_permission: permission_active,
2667 request_exit_plan_mode,
2668 request_auto_mode_switch,
2669 request_elicitation,
2670 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2671 github_mcp_tool_config: self.github_mcp_tool_config,
2672 hooks: hooks_flag,
2673 skill_directories: self.skill_directories,
2674 instruction_directories: self.instruction_directories,
2675 plugin_directories: self.plugin_directories,
2676 large_output: self.large_output,
2677 tool_search: self.tool_search,
2678 disabled_skills: self.disabled_skills,
2679 disabled_mcp_servers: self.disabled_mcp_servers,
2680 custom_agents: self.custom_agents,
2681 custom_agents_local_only: self.custom_agents_local_only,
2682 default_agent: self.default_agent,
2683 agent: self.agent,
2684 infinite_sessions: self.infinite_sessions,
2685 provider: self.provider,
2686 capi: self.capi,
2687 providers: self.providers,
2688 models: self.models,
2689 enable_session_telemetry: self.enable_session_telemetry,
2690 enable_citations: self.enable_citations,
2691 enable_file_change_tracking: self.enable_file_change_tracking,
2692 session_limits: self.session_limits,
2693 model_capabilities: self.model_capabilities,
2694 memory: self.memory,
2695 config_dir: self.config_directory,
2696 working_directory: self.working_directory,
2697 additional_directories: self.additional_directories,
2698 github_token: self.github_token,
2699 github_token_provider_registration_id: None,
2700 remote_session: self.remote_session,
2701 cloud: self.cloud,
2702 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2703 enable_github_telemetry_forwarding: None,
2704 commands: wire_commands,
2705 feature_flags: self.feature_flags,
2706 exp_assignments: self.exp_assignments,
2707 enable_managed_settings: self.enable_managed_settings,
2708 is_experimental_mode: self.enable_experimental_mode,
2709 managed_settings: self.managed_settings,
2710 };
2711
2712 let runtime = SessionConfigRuntime {
2713 permission_handler: self.permission_handler,
2714 permission_policy: self.permission_policy,
2715 elicitation_handler: self.elicitation_handler,
2716 mcp_auth_handler: self.mcp_auth_handler,
2717 user_input_handler: self.user_input_handler,
2718 exit_plan_mode_handler: self.exit_plan_mode_handler,
2719 auto_mode_switch_handler: self.auto_mode_switch_handler,
2720 hooks_handler: self.hooks_handler,
2721 system_message_transform: self.system_message_transform,
2722 tool_handlers,
2723 canvas_handler,
2724 session_fs_provider: self.session_fs_provider,
2725 bearer_token_providers,
2726 github_token_provider: self.github_token_provider,
2727 commands: self.commands,
2728 };
2729
2730 Ok((wire, runtime))
2731 }
2732
2733 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2737 self.permission_handler = Some(handler);
2738 self
2739 }
2740
2741 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2744 self.elicitation_handler = Some(handler);
2745 self
2746 }
2747
2748 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2750 self.mcp_auth_handler = Some(handler);
2751 self
2752 }
2753
2754 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2757 self.user_input_handler = Some(handler);
2758 self
2759 }
2760
2761 pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self {
2763 self.ask_user_variant = Some(variant);
2764 self
2765 }
2766
2767 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2769 self.exit_plan_mode_handler = Some(handler);
2770 self
2771 }
2772
2773 pub fn with_auto_mode_switch_handler(
2775 mut self,
2776 handler: Arc<dyn AutoModeSwitchHandler>,
2777 ) -> Self {
2778 self.auto_mode_switch_handler = Some(handler);
2779 self
2780 }
2781
2782 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2787 self.commands = Some(commands);
2788 self
2789 }
2790
2791 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2795 self.session_fs_provider = Some(provider);
2796 self
2797 }
2798
2799 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2802 self.hooks_handler = Some(hooks);
2803 self
2804 }
2805
2806 pub fn with_system_message_transform(
2810 mut self,
2811 transform: Arc<dyn SystemMessageTransform>,
2812 ) -> Self {
2813 self.system_message_transform = Some(transform);
2814 self
2815 }
2816
2817 pub fn approve_all_permissions(mut self) -> Self {
2823 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2824 self
2825 }
2826
2827 pub fn deny_all_permissions(mut self) -> Self {
2830 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2831 self
2832 }
2833
2834 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2839 where
2840 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2841 {
2842 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2843 self
2844 }
2845
2846 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2848 self.session_id = Some(id.into());
2849 self
2850 }
2851
2852 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2854 self.model = Some(model.into());
2855 self
2856 }
2857
2858 pub fn with_allowed_models<I, S>(mut self, models: I) -> Self
2863 where
2864 I: IntoIterator<Item = S>,
2865 S: Into<String>,
2866 {
2867 self.allowed_models = Some(models.into_iter().map(Into::into).collect());
2868 self
2869 }
2870
2871 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2873 self.client_name = Some(name.into());
2874 self
2875 }
2876
2877 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2879 self.reasoning_effort = Some(effort.into());
2880 self
2881 }
2882
2883 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2885 self.reasoning_summary = Some(summary);
2886 self
2887 }
2888
2889 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2891 self.context_tier = Some(tier.into());
2892 self
2893 }
2894
2895 pub fn with_streaming(mut self, streaming: bool) -> Self {
2897 self.streaming = Some(streaming);
2898 self
2899 }
2900
2901 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2903 self.system_message = Some(system_message);
2904 self
2905 }
2906
2907 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2909 self.tools = Some(tools.into_iter().collect());
2910 self
2911 }
2912
2913 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2918 self.canvases = Some(canvases.into_iter().collect());
2919 self
2920 }
2921
2922 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2924 self.canvas_handler = Some(handler);
2925 self
2926 }
2927
2928 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2930 self.request_canvas_renderer = Some(request);
2931 self
2932 }
2933
2934 pub fn with_request_extensions(mut self, request: bool) -> Self {
2936 self.request_extensions = Some(request);
2937 self
2938 }
2939
2940 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2944 self.extension_sdk_path = Some(path.into());
2945 self
2946 }
2947
2948 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2950 self.extension_info = Some(extension_info);
2951 self
2952 }
2953
2954 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2957 self.canvas_provider = Some(canvas_provider);
2958 self
2959 }
2960
2961 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2963 where
2964 I: IntoIterator<Item = S>,
2965 S: Into<String>,
2966 {
2967 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2968 self
2969 }
2970
2971 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2973 where
2974 I: IntoIterator<Item = S>,
2975 S: Into<String>,
2976 {
2977 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2978 self
2979 }
2980
2981 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2983 where
2984 I: IntoIterator<Item = S>,
2985 S: Into<String>,
2986 {
2987 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2988 self
2989 }
2990
2991 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2993 self.mcp_servers = Some(servers);
2994 self
2995 }
2996
2997 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3005 self.mcp_oauth_token_storage = Some(mode.into());
3006 self
3007 }
3008
3009 pub fn with_auth_client_id_metadata_url(mut self, url: impl Into<String>) -> Self {
3011 self.auth_client_id_metadata_url = Some(url.into());
3012 self
3013 }
3014
3015 pub fn with_embedding_cache_storage(
3017 mut self,
3018 embedding_cache_storage: impl Into<String>,
3019 ) -> Self {
3020 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3021 self
3022 }
3023
3024 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3027 self.enable_config_discovery = Some(enable);
3028 self
3029 }
3030
3031 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3033 self.skip_embedding_retrieval = Some(value);
3034 self
3035 }
3036
3037 pub fn with_organization_custom_instructions(
3039 mut self,
3040 instructions: impl Into<String>,
3041 ) -> Self {
3042 self.organization_custom_instructions = Some(instructions.into());
3043 self
3044 }
3045
3046 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3048 self.enable_on_demand_instruction_discovery = Some(value);
3049 self
3050 }
3051
3052 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3054 self.enable_file_hooks = Some(value);
3055 self
3056 }
3057
3058 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3060 self.enable_host_git_operations = Some(value);
3061 self
3062 }
3063
3064 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3066 self.enable_session_store = Some(value);
3067 self
3068 }
3069
3070 pub fn with_enable_skills(mut self, value: bool) -> Self {
3072 self.enable_skills = Some(value);
3073 self
3074 }
3075
3076 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3082 self.enable_mcp_apps = Some(enable);
3083 self
3084 }
3085
3086 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
3088 self.github_mcp_tool_config = Some(config);
3089 self
3090 }
3091
3092 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3094 where
3095 I: IntoIterator<Item = P>,
3096 P: Into<PathBuf>,
3097 {
3098 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3099 self
3100 }
3101
3102 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
3104 where
3105 I: IntoIterator<Item = S>,
3106 S: Into<String>,
3107 {
3108 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
3109 self
3110 }
3111
3112 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3116 where
3117 I: IntoIterator<Item = P>,
3118 P: Into<PathBuf>,
3119 {
3120 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3121 self
3122 }
3123
3124 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3126 where
3127 I: IntoIterator<Item = P>,
3128 P: Into<PathBuf>,
3129 {
3130 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3131 self
3132 }
3133
3134 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3136 self.large_output = Some(config);
3137 self
3138 }
3139
3140 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
3143 self.tool_search = Some(config);
3144 self
3145 }
3146
3147 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3149 where
3150 I: IntoIterator<Item = S>,
3151 S: Into<String>,
3152 {
3153 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3154 self
3155 }
3156
3157 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
3159 where
3160 I: IntoIterator<Item = S>,
3161 S: Into<String>,
3162 {
3163 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
3164 self
3165 }
3166
3167 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3169 mut self,
3170 agents: I,
3171 ) -> Self {
3172 self.custom_agents = Some(agents.into_iter().collect());
3173 self
3174 }
3175
3176 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3178 self.default_agent = Some(agent);
3179 self
3180 }
3181
3182 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3185 self.agent = Some(name.into());
3186 self
3187 }
3188
3189 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3192 self.infinite_sessions = Some(config);
3193 self
3194 }
3195
3196 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3198 self.provider = Some(provider);
3199 self
3200 }
3201
3202 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3204 self.capi = Some(capi);
3205 self
3206 }
3207
3208 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3214 self.providers = Some(providers);
3215 self
3216 }
3217
3218 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3224 self.models = Some(models);
3225 self
3226 }
3227
3228 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3232 self.enable_session_telemetry = Some(enable);
3233 self
3234 }
3235
3236 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3238 self.enable_citations = Some(enable);
3239 self
3240 }
3241
3242 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
3245 self.enable_file_change_tracking = Some(enable);
3246 self
3247 }
3248
3249 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3251 self.session_limits = Some(limits);
3252 self
3253 }
3254
3255 pub fn with_model_capabilities(
3257 mut self,
3258 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3259 ) -> Self {
3260 self.model_capabilities = Some(capabilities);
3261 self
3262 }
3263
3264 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3266 self.memory = Some(memory);
3267 self
3268 }
3269
3270 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3272 self.config_directory = Some(dir.into());
3273 self
3274 }
3275
3276 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3279 self.working_directory = Some(dir.into());
3280 self
3281 }
3282
3283 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
3285 where
3286 I: IntoIterator<Item = P>,
3287 P: Into<PathBuf>,
3288 {
3289 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
3290 self
3291 }
3292
3293 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3298 self.github_token = Some(token.into());
3299 self
3300 }
3301
3302 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
3308 self.github_token_provider = Some(provider);
3309 self
3310 }
3311
3312 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3315 self.include_sub_agent_streaming_events = Some(include);
3316 self
3317 }
3318
3319 pub fn with_remote_session(
3321 mut self,
3322 mode: crate::generated::api_types::RemoteSessionMode,
3323 ) -> Self {
3324 self.remote_session = Some(mode);
3325 self
3326 }
3327
3328 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
3330 self.cloud = Some(cloud);
3331 self
3332 }
3333
3334 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3336 self.skip_custom_instructions = Some(value);
3337 self
3338 }
3339
3340 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3342 self.custom_agents_local_only = Some(value);
3343 self
3344 }
3345
3346 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
3348 self.enable_experimental_mode = Some(enable_experimental_mode);
3349 self
3350 }
3351
3352 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3354 self.coauthor_enabled = Some(value);
3355 self
3356 }
3357
3358 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3360 self.manage_schedule_enabled = Some(value);
3361 self
3362 }
3363
3364 pub fn with_feature_flags(mut self, feature_flags: HashMap<String, bool>) -> Self {
3366 self.feature_flags = Some(feature_flags);
3367 self
3368 }
3369
3370 pub fn with_event_buffer_capacity(mut self, capacity: usize) -> Self {
3378 self.event_buffer_capacity = Some(capacity);
3379 self
3380 }
3381
3382 #[doc(hidden)]
3390 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
3391 self.exp_assignments = Some(assignments);
3392 self
3393 }
3394
3395 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3402 self.enable_managed_settings = Some(enabled);
3403 self
3404 }
3405
3406 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
3411 self.managed_settings = Some(managed_settings);
3412 self
3413 }
3414}
3415#[derive(Clone)]
3422#[non_exhaustive]
3423pub struct ResumeSessionConfig {
3424 pub session_id: SessionId,
3426 pub model: Option<String>,
3429 pub allowed_models: Option<Vec<String>>,
3434 pub client_name: Option<String>,
3436 pub reasoning_effort: Option<String>,
3438 pub reasoning_summary: Option<ReasoningSummary>,
3442 pub context_tier: Option<String>,
3445 pub streaming: Option<bool>,
3447 pub system_message: Option<SystemMessageConfig>,
3450 pub ask_user_variant: Option<AskUserVariant>,
3455 pub tools: Option<Vec<Tool>>,
3457 pub canvases: Option<Vec<CanvasDeclaration>>,
3459 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
3462 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3464 pub request_canvas_renderer: Option<bool>,
3466 pub request_extensions: Option<bool>,
3468 pub extension_sdk_path: Option<String>,
3472 pub extension_info: Option<ExtensionInfo>,
3474 pub canvas_provider: Option<CanvasProviderIdentity>,
3477 pub available_tools: Option<Vec<String>>,
3479 pub excluded_tools: Option<Vec<String>>,
3481 pub excluded_builtin_agents: Option<Vec<String>>,
3487 pub included_builtin_skills: Option<Vec<String>>,
3491 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3493 pub mcp_oauth_token_storage: Option<String>,
3496 pub auth_client_id_metadata_url: Option<String>,
3502 pub enable_config_discovery: Option<bool>,
3505 pub skip_embedding_retrieval: Option<bool>,
3507 pub embedding_cache_storage: Option<String>,
3509 pub organization_custom_instructions: Option<String>,
3511 pub enable_on_demand_instruction_discovery: Option<bool>,
3513 pub enable_file_hooks: Option<bool>,
3515 pub enable_host_git_operations: Option<bool>,
3517 pub enable_session_store: Option<bool>,
3519 pub enable_skills: Option<bool>,
3521 pub enable_mcp_apps: Option<bool>,
3527 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
3532 pub skill_directories: Option<Vec<PathBuf>>,
3534 pub instruction_directories: Option<Vec<PathBuf>>,
3537 pub plugin_directories: Option<Vec<PathBuf>>,
3539 pub large_output: Option<LargeToolOutputConfig>,
3541 pub tool_search: Option<ToolSearchConfig>,
3544 pub disabled_skills: Option<Vec<String>>,
3546 pub disabled_mcp_servers: Option<Vec<String>>,
3549 pub hooks: Option<bool>,
3551 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3553 pub default_agent: Option<DefaultAgentConfig>,
3555 pub agent: Option<String>,
3557 pub infinite_sessions: Option<InfiniteSessionConfig>,
3559 pub provider: Option<ProviderConfig>,
3561 pub capi: Option<CapiSessionOptions>,
3567 pub providers: Option<Vec<NamedProviderConfig>>,
3573 pub models: Option<Vec<ProviderModelConfig>>,
3579 pub enable_session_telemetry: Option<bool>,
3587 pub enable_citations: Option<bool>,
3589 pub enable_file_change_tracking: Option<bool>,
3593 pub session_limits: Option<SessionLimitsConfig>,
3595 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3597 pub memory: Option<MemoryConfiguration>,
3599 pub config_directory: Option<PathBuf>,
3601 pub working_directory: Option<PathBuf>,
3603 pub additional_directories: Option<Vec<PathBuf>>,
3606 pub github_token: Option<String>,
3609 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
3612 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3615 pub include_sub_agent_streaming_events: Option<bool>,
3617 pub commands: Option<Vec<CommandDefinition>>,
3621 pub feature_flags: Option<HashMap<String, bool>>,
3625 #[doc(hidden)]
3630 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3631 pub enable_managed_settings: Option<bool>,
3637 pub managed_settings: Option<ManagedSettings>,
3643 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3648 pub suppress_resume_event: Option<bool>,
3651 pub continue_pending_work: Option<bool>,
3659 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3662 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3665 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3667 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3670 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3673 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3676 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3678 pub(crate) permission_policy: Option<crate::permission::Policy>,
3680 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3682 pub skip_custom_instructions: Option<bool>,
3684 pub custom_agents_local_only: Option<bool>,
3686 pub enable_experimental_mode: Option<bool>,
3691 pub coauthor_enabled: Option<bool>,
3693 pub manage_schedule_enabled: Option<bool>,
3695 pub event_buffer_capacity: Option<usize>,
3697}
3698
3699impl std::fmt::Debug for ResumeSessionConfig {
3700 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3701 f.debug_struct("ResumeSessionConfig")
3702 .field("session_id", &self.session_id)
3703 .field("model", &self.model)
3704 .field("allowed_models", &self.allowed_models)
3705 .field("client_name", &self.client_name)
3706 .field("reasoning_effort", &self.reasoning_effort)
3707 .field("reasoning_summary", &self.reasoning_summary)
3708 .field("context_tier", &self.context_tier)
3709 .field("streaming", &self.streaming)
3710 .field("system_message", &self.system_message)
3711 .field("ask_user_variant", &self.ask_user_variant)
3712 .field("tools", &self.tools)
3713 .field("canvases", &self.canvases)
3714 .field(
3715 "canvas_handler",
3716 &self.canvas_handler.as_ref().map(|_| "<set>"),
3717 )
3718 .field("open_canvases", &self.open_canvases)
3719 .field("request_canvas_renderer", &self.request_canvas_renderer)
3720 .field("request_extensions", &self.request_extensions)
3721 .field("extension_sdk_path", &self.extension_sdk_path)
3722 .field("extension_info", &self.extension_info)
3723 .field("canvas_provider", &self.canvas_provider)
3724 .field("available_tools", &self.available_tools)
3725 .field("excluded_tools", &self.excluded_tools)
3726 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3727 .field("included_builtin_skills", &self.included_builtin_skills)
3728 .field("mcp_servers", &self.mcp_servers)
3729 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3730 .field(
3731 "auth_client_id_metadata_url",
3732 &self.auth_client_id_metadata_url,
3733 )
3734 .field("embedding_cache_storage", &self.embedding_cache_storage)
3735 .field("enable_config_discovery", &self.enable_config_discovery)
3736 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3737 .field(
3738 "organization_custom_instructions",
3739 &self
3740 .organization_custom_instructions
3741 .as_ref()
3742 .map(|_| "<redacted>"),
3743 )
3744 .field(
3745 "enable_on_demand_instruction_discovery",
3746 &self.enable_on_demand_instruction_discovery,
3747 )
3748 .field("enable_file_hooks", &self.enable_file_hooks)
3749 .field(
3750 "enable_host_git_operations",
3751 &self.enable_host_git_operations,
3752 )
3753 .field("enable_session_store", &self.enable_session_store)
3754 .field("enable_skills", &self.enable_skills)
3755 .field("enable_mcp_apps", &self.enable_mcp_apps)
3756 .field("skill_directories", &self.skill_directories)
3757 .field("instruction_directories", &self.instruction_directories)
3758 .field("plugin_directories", &self.plugin_directories)
3759 .field("large_output", &self.large_output)
3760 .field("tool_search", &self.tool_search)
3761 .field("disabled_skills", &self.disabled_skills)
3762 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
3763 .field("hooks", &self.hooks)
3764 .field("custom_agents", &self.custom_agents)
3765 .field("default_agent", &self.default_agent)
3766 .field("agent", &self.agent)
3767 .field("infinite_sessions", &self.infinite_sessions)
3768 .field("provider", &self.provider)
3769 .field("capi", &self.capi)
3770 .field("enable_session_telemetry", &self.enable_session_telemetry)
3771 .field("enable_citations", &self.enable_citations)
3772 .field(
3773 "enable_file_change_tracking",
3774 &self.enable_file_change_tracking,
3775 )
3776 .field("session_limits", &self.session_limits)
3777 .field("model_capabilities", &self.model_capabilities)
3778 .field("memory", &self.memory)
3779 .field("config_directory", &self.config_directory)
3780 .field("working_directory", &self.working_directory)
3781 .field("additional_directories", &self.additional_directories)
3782 .field(
3783 "github_token",
3784 &self.github_token.as_ref().map(|_| "<redacted>"),
3785 )
3786 .field(
3787 "github_token_provider",
3788 &self.github_token_provider.as_ref().map(|_| "<set>"),
3789 )
3790 .field("remote_session", &self.remote_session)
3791 .field(
3792 "include_sub_agent_streaming_events",
3793 &self.include_sub_agent_streaming_events,
3794 )
3795 .field("commands", &self.commands)
3796 .field("feature_flags", &self.feature_flags)
3797 .field("exp_assignments", &self.exp_assignments)
3798 .field("enable_managed_settings", &self.enable_managed_settings)
3799 .field("enable_experimental_mode", &self.enable_experimental_mode)
3800 .field("managed_settings", &self.managed_settings)
3801 .field(
3802 "session_fs_provider",
3803 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3804 )
3805 .field(
3806 "permission_handler",
3807 &self.permission_handler.as_ref().map(|_| "<set>"),
3808 )
3809 .field(
3810 "elicitation_handler",
3811 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3812 )
3813 .field(
3814 "user_input_handler",
3815 &self.user_input_handler.as_ref().map(|_| "<set>"),
3816 )
3817 .field(
3818 "exit_plan_mode_handler",
3819 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3820 )
3821 .field(
3822 "auto_mode_switch_handler",
3823 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3824 )
3825 .field(
3826 "hooks_handler",
3827 &self.hooks_handler.as_ref().map(|_| "<set>"),
3828 )
3829 .field(
3830 "system_message_transform",
3831 &self.system_message_transform.as_ref().map(|_| "<set>"),
3832 )
3833 .field("suppress_resume_event", &self.suppress_resume_event)
3834 .field("continue_pending_work", &self.continue_pending_work)
3835 .field("event_buffer_capacity", &self.event_buffer_capacity)
3836 .finish()
3837 }
3838}
3839
3840impl ResumeSessionConfig {
3841 pub(crate) fn into_wire(
3849 mut self,
3850 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3851 if self.github_token.is_some() && self.github_token_provider.is_some() {
3852 return Err(crate::Error::with_message(
3853 crate::ErrorKind::InvalidConfig,
3854 "github_token and github_token_provider are mutually exclusive",
3855 ));
3856 }
3857 let permission_active =
3858 self.permission_handler.is_some() || self.permission_policy.is_some();
3859 let request_user_input = self.user_input_handler.is_some();
3860 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3861 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3862 let request_elicitation = self.elicitation_handler.is_some();
3863 let hooks_flag = self.hooks_handler.is_some();
3864
3865 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3866 if let Some(tools) = self.tools.as_mut() {
3867 for tool in tools.iter_mut() {
3868 if let Some(handler) = tool.handler.take()
3869 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3870 {
3871 return Err(crate::Error::with_message(
3872 crate::ErrorKind::InvalidConfig,
3873 format!("duplicate tool handler registered for name {:?}", tool.name),
3874 ));
3875 }
3876 }
3877 }
3878
3879 let wire_commands = self.commands.as_ref().map(|cmds| {
3880 cmds.iter()
3881 .map(|c| crate::wire::CommandWireDefinition {
3882 name: c.name.clone(),
3883 description: c.description.clone().unwrap_or_default(),
3884 })
3885 .collect()
3886 });
3887 let wire_canvases = self.canvases.clone();
3888 let canvas_handler = self.canvas_handler.clone();
3889 let bearer_token_providers =
3890 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3891
3892 let wire = crate::wire::SessionResumeWire {
3893 session_id: self.session_id,
3894 model: self.model,
3895 allowed_models: self.allowed_models,
3896 client_name: self.client_name,
3897 reasoning_effort: self.reasoning_effort,
3898 reasoning_summary: self.reasoning_summary,
3899 context_tier: self.context_tier,
3900 streaming: self.streaming,
3901 system_message: self.system_message,
3902 ask_user_variant: self.ask_user_variant,
3903 tools: self.tools,
3904 canvases: wire_canvases,
3905 open_canvases: self.open_canvases,
3906 request_canvas_renderer: self.request_canvas_renderer,
3907 request_extensions: self.request_extensions,
3908 extension_sdk_path: self.extension_sdk_path,
3909 extension_info: self.extension_info,
3910 canvas_provider: self.canvas_provider,
3911 available_tools: self.available_tools,
3912 excluded_tools: self.excluded_tools,
3913 excluded_builtin_agents: self.excluded_builtin_agents,
3914 tool_filter_precedence: "excluded",
3915 mcp_servers: self.mcp_servers,
3916 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3917 auth_client_id_metadata_url: self.auth_client_id_metadata_url,
3918 embedding_cache_storage: self.embedding_cache_storage,
3919 env_value_mode: "direct",
3920 enable_config_discovery: self.enable_config_discovery,
3921 skip_embedding_retrieval: self.skip_embedding_retrieval,
3922 organization_custom_instructions: self.organization_custom_instructions,
3923 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3924 enable_file_hooks: self.enable_file_hooks,
3925 enable_host_git_operations: self.enable_host_git_operations,
3926 enable_session_store: self.enable_session_store,
3927 enable_skills: self.enable_skills,
3928 request_user_input,
3929 request_permission: permission_active,
3930 request_exit_plan_mode,
3931 request_auto_mode_switch,
3932 request_elicitation,
3933 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3934 github_mcp_tool_config: self.github_mcp_tool_config,
3935 hooks: hooks_flag,
3936 skill_directories: self.skill_directories,
3937 instruction_directories: self.instruction_directories,
3938 plugin_directories: self.plugin_directories,
3939 large_output: self.large_output,
3940 tool_search: self.tool_search,
3941 disabled_skills: self.disabled_skills,
3942 disabled_mcp_servers: self.disabled_mcp_servers,
3943 custom_agents: self.custom_agents,
3944 custom_agents_local_only: self.custom_agents_local_only,
3945 default_agent: self.default_agent,
3946 agent: self.agent,
3947 infinite_sessions: self.infinite_sessions,
3948 provider: self.provider,
3949 capi: self.capi,
3950 providers: self.providers,
3951 models: self.models,
3952 enable_session_telemetry: self.enable_session_telemetry,
3953 enable_citations: self.enable_citations,
3954 enable_file_change_tracking: self.enable_file_change_tracking,
3955 session_limits: self.session_limits,
3956 model_capabilities: self.model_capabilities,
3957 memory: self.memory,
3958 config_dir: self.config_directory,
3959 working_directory: self.working_directory,
3960 additional_directories: self.additional_directories,
3961 github_token: self.github_token,
3962 github_token_provider_registration_id: None,
3963 remote_session: self.remote_session,
3964 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3965 enable_github_telemetry_forwarding: None,
3966 commands: wire_commands,
3967 feature_flags: self.feature_flags,
3968 exp_assignments: self.exp_assignments,
3969 enable_managed_settings: self.enable_managed_settings,
3970 is_experimental_mode: self.enable_experimental_mode,
3971 managed_settings: self.managed_settings,
3972 suppress_resume_event: self.suppress_resume_event,
3973 continue_pending_work: self.continue_pending_work,
3974 };
3975
3976 let runtime = SessionConfigRuntime {
3977 permission_handler: self.permission_handler,
3978 permission_policy: self.permission_policy,
3979 elicitation_handler: self.elicitation_handler,
3980 mcp_auth_handler: self.mcp_auth_handler,
3981 user_input_handler: self.user_input_handler,
3982 exit_plan_mode_handler: self.exit_plan_mode_handler,
3983 auto_mode_switch_handler: self.auto_mode_switch_handler,
3984 hooks_handler: self.hooks_handler,
3985 system_message_transform: self.system_message_transform,
3986 tool_handlers,
3987 canvas_handler,
3988 session_fs_provider: self.session_fs_provider,
3989 bearer_token_providers,
3990 github_token_provider: self.github_token_provider,
3991 commands: self.commands,
3992 };
3993
3994 Ok((wire, runtime))
3995 }
3996
3997 pub fn new(session_id: SessionId) -> Self {
4002 Self {
4003 session_id,
4004 model: None,
4005 allowed_models: None,
4006 client_name: None,
4007 reasoning_effort: None,
4008 reasoning_summary: None,
4009 context_tier: None,
4010 streaming: None,
4011 system_message: None,
4012 ask_user_variant: None,
4013 tools: None,
4014 canvases: None,
4015 canvas_handler: None,
4016 open_canvases: None,
4017 request_canvas_renderer: None,
4018 request_extensions: None,
4019 extension_sdk_path: None,
4020 extension_info: None,
4021 canvas_provider: None,
4022 available_tools: None,
4023 excluded_tools: None,
4024 excluded_builtin_agents: None,
4025 included_builtin_skills: None,
4026 mcp_servers: None,
4027 mcp_oauth_token_storage: None,
4028 auth_client_id_metadata_url: None,
4029 enable_config_discovery: None,
4030 skip_embedding_retrieval: None,
4031 organization_custom_instructions: None,
4032 enable_on_demand_instruction_discovery: None,
4033 enable_file_hooks: None,
4034 enable_host_git_operations: None,
4035 enable_session_store: None,
4036 enable_skills: None,
4037 embedding_cache_storage: None,
4038 enable_mcp_apps: None,
4039 github_mcp_tool_config: None,
4040 skill_directories: None,
4041 instruction_directories: None,
4042 plugin_directories: None,
4043 large_output: None,
4044 tool_search: None,
4045 disabled_skills: None,
4046 disabled_mcp_servers: None,
4047 hooks: None,
4048 custom_agents: None,
4049 default_agent: None,
4050 agent: None,
4051 infinite_sessions: None,
4052 provider: None,
4053 capi: None,
4054 providers: None,
4055 models: None,
4056 enable_session_telemetry: None,
4057 enable_citations: None,
4058 enable_file_change_tracking: None,
4059 session_limits: None,
4060 model_capabilities: None,
4061 memory: None,
4062 config_directory: None,
4063 working_directory: None,
4064 additional_directories: None,
4065 github_token: None,
4066 github_token_provider: None,
4067 remote_session: None,
4068 include_sub_agent_streaming_events: None,
4069 commands: None,
4070 feature_flags: None,
4071 exp_assignments: None,
4072 enable_managed_settings: None,
4073 managed_settings: None,
4074 session_fs_provider: None,
4075 suppress_resume_event: None,
4076 continue_pending_work: None,
4077 permission_handler: None,
4078 elicitation_handler: None,
4079 mcp_auth_handler: None,
4080 user_input_handler: None,
4081 exit_plan_mode_handler: None,
4082 auto_mode_switch_handler: None,
4083 hooks_handler: None,
4084 permission_policy: None,
4085 system_message_transform: None,
4086 skip_custom_instructions: None,
4087 custom_agents_local_only: None,
4088 enable_experimental_mode: None,
4089 coauthor_enabled: None,
4090 manage_schedule_enabled: None,
4091 event_buffer_capacity: None,
4092 }
4093 }
4094
4095 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
4097 self.permission_handler = Some(handler);
4098 self
4099 }
4100
4101 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
4103 self.elicitation_handler = Some(handler);
4104 self
4105 }
4106
4107 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
4109 self.mcp_auth_handler = Some(handler);
4110 self
4111 }
4112
4113 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
4115 self.user_input_handler = Some(handler);
4116 self
4117 }
4118
4119 pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self {
4121 self.ask_user_variant = Some(variant);
4122 self
4123 }
4124
4125 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
4127 self.exit_plan_mode_handler = Some(handler);
4128 self
4129 }
4130
4131 pub fn with_auto_mode_switch_handler(
4133 mut self,
4134 handler: Arc<dyn AutoModeSwitchHandler>,
4135 ) -> Self {
4136 self.auto_mode_switch_handler = Some(handler);
4137 self
4138 }
4139
4140 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
4143 self.hooks_handler = Some(hooks);
4144 self
4145 }
4146
4147 pub fn with_system_message_transform(
4149 mut self,
4150 transform: Arc<dyn SystemMessageTransform>,
4151 ) -> Self {
4152 self.system_message_transform = Some(transform);
4153 self
4154 }
4155
4156 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
4160 self.commands = Some(commands);
4161 self
4162 }
4163
4164 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
4167 self.session_fs_provider = Some(provider);
4168 self
4169 }
4170
4171 pub fn approve_all_permissions(mut self) -> Self {
4174 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
4175 self
4176 }
4177
4178 pub fn deny_all_permissions(mut self) -> Self {
4181 self.permission_policy = Some(crate::permission::Policy::DenyAll);
4182 self
4183 }
4184
4185 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
4188 where
4189 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
4190 {
4191 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
4192 self
4193 }
4194
4195 pub fn with_model(mut self, model: impl Into<String>) -> Self {
4197 self.model = Some(model.into());
4198 self
4199 }
4200
4201 pub fn with_allowed_models<I, S>(mut self, models: I) -> Self
4206 where
4207 I: IntoIterator<Item = S>,
4208 S: Into<String>,
4209 {
4210 self.allowed_models = Some(models.into_iter().map(Into::into).collect());
4211 self
4212 }
4213
4214 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
4216 self.client_name = Some(name.into());
4217 self
4218 }
4219
4220 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4222 self.reasoning_effort = Some(effort.into());
4223 self
4224 }
4225
4226 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4228 self.reasoning_summary = Some(summary);
4229 self
4230 }
4231
4232 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
4235 self.context_tier = Some(tier.into());
4236 self
4237 }
4238
4239 pub fn with_streaming(mut self, streaming: bool) -> Self {
4241 self.streaming = Some(streaming);
4242 self
4243 }
4244
4245 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
4248 self.system_message = Some(system_message);
4249 self
4250 }
4251
4252 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
4254 self.tools = Some(tools.into_iter().collect());
4255 self
4256 }
4257
4258 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
4260 self.canvases = Some(canvases.into_iter().collect());
4261 self
4262 }
4263
4264 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
4266 self.canvas_handler = Some(handler);
4267 self
4268 }
4269
4270 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
4272 mut self,
4273 open_canvases: I,
4274 ) -> Self {
4275 self.open_canvases = Some(open_canvases.into_iter().collect());
4276 self
4277 }
4278
4279 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
4281 self.request_canvas_renderer = Some(request);
4282 self
4283 }
4284
4285 pub fn with_request_extensions(mut self, request: bool) -> Self {
4287 self.request_extensions = Some(request);
4288 self
4289 }
4290
4291 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
4295 self.extension_sdk_path = Some(path.into());
4296 self
4297 }
4298
4299 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
4301 self.extension_info = Some(extension_info);
4302 self
4303 }
4304
4305 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
4308 self.canvas_provider = Some(canvas_provider);
4309 self
4310 }
4311
4312 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
4314 where
4315 I: IntoIterator<Item = S>,
4316 S: Into<String>,
4317 {
4318 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
4319 self
4320 }
4321
4322 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
4324 where
4325 I: IntoIterator<Item = S>,
4326 S: Into<String>,
4327 {
4328 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
4329 self
4330 }
4331
4332 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
4334 where
4335 I: IntoIterator<Item = S>,
4336 S: Into<String>,
4337 {
4338 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
4339 self
4340 }
4341
4342 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
4344 self.mcp_servers = Some(servers);
4345 self
4346 }
4347
4348 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
4351 self.mcp_oauth_token_storage = Some(mode.into());
4352 self
4353 }
4354
4355 pub fn with_auth_client_id_metadata_url(mut self, url: impl Into<String>) -> Self {
4357 self.auth_client_id_metadata_url = Some(url.into());
4358 self
4359 }
4360
4361 pub fn with_embedding_cache_storage(
4363 mut self,
4364 embedding_cache_storage: impl Into<String>,
4365 ) -> Self {
4366 self.embedding_cache_storage = Some(embedding_cache_storage.into());
4367 self
4368 }
4369
4370 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
4373 self.enable_config_discovery = Some(enable);
4374 self
4375 }
4376
4377 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
4379 self.skip_embedding_retrieval = Some(value);
4380 self
4381 }
4382
4383 pub fn with_organization_custom_instructions(
4385 mut self,
4386 instructions: impl Into<String>,
4387 ) -> Self {
4388 self.organization_custom_instructions = Some(instructions.into());
4389 self
4390 }
4391
4392 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
4394 self.enable_on_demand_instruction_discovery = Some(value);
4395 self
4396 }
4397
4398 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
4400 self.enable_file_hooks = Some(value);
4401 self
4402 }
4403
4404 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
4406 self.enable_host_git_operations = Some(value);
4407 self
4408 }
4409
4410 pub fn with_enable_session_store(mut self, value: bool) -> Self {
4412 self.enable_session_store = Some(value);
4413 self
4414 }
4415
4416 pub fn with_enable_skills(mut self, value: bool) -> Self {
4418 self.enable_skills = Some(value);
4419 self
4420 }
4421
4422 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
4428 self.enable_mcp_apps = Some(enable);
4429 self
4430 }
4431
4432 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
4434 self.github_mcp_tool_config = Some(config);
4435 self
4436 }
4437
4438 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
4440 where
4441 I: IntoIterator<Item = P>,
4442 P: Into<PathBuf>,
4443 {
4444 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
4445 self
4446 }
4447
4448 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
4450 where
4451 I: IntoIterator<Item = S>,
4452 S: Into<String>,
4453 {
4454 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
4455 self
4456 }
4457
4458 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
4462 where
4463 I: IntoIterator<Item = P>,
4464 P: Into<PathBuf>,
4465 {
4466 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
4467 self
4468 }
4469
4470 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
4472 where
4473 I: IntoIterator<Item = P>,
4474 P: Into<PathBuf>,
4475 {
4476 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
4477 self
4478 }
4479
4480 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
4482 self.large_output = Some(config);
4483 self
4484 }
4485
4486 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
4489 self.tool_search = Some(config);
4490 self
4491 }
4492
4493 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
4495 where
4496 I: IntoIterator<Item = S>,
4497 S: Into<String>,
4498 {
4499 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
4500 self
4501 }
4502
4503 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
4505 where
4506 I: IntoIterator<Item = S>,
4507 S: Into<String>,
4508 {
4509 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
4510 self
4511 }
4512
4513 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
4515 mut self,
4516 agents: I,
4517 ) -> Self {
4518 self.custom_agents = Some(agents.into_iter().collect());
4519 self
4520 }
4521
4522 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
4524 self.default_agent = Some(agent);
4525 self
4526 }
4527
4528 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
4530 self.agent = Some(name.into());
4531 self
4532 }
4533
4534 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
4536 self.infinite_sessions = Some(config);
4537 self
4538 }
4539
4540 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
4542 self.provider = Some(provider);
4543 self
4544 }
4545
4546 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
4548 self.capi = Some(capi);
4549 self
4550 }
4551
4552 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
4558 self.providers = Some(providers);
4559 self
4560 }
4561
4562 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
4568 self.models = Some(models);
4569 self
4570 }
4571
4572 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
4576 self.enable_session_telemetry = Some(enable);
4577 self
4578 }
4579
4580 pub fn with_enable_citations(mut self, enable: bool) -> Self {
4582 self.enable_citations = Some(enable);
4583 self
4584 }
4585
4586 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
4589 self.enable_file_change_tracking = Some(enable);
4590 self
4591 }
4592
4593 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
4595 self.session_limits = Some(limits);
4596 self
4597 }
4598
4599 pub fn with_model_capabilities(
4601 mut self,
4602 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
4603 ) -> Self {
4604 self.model_capabilities = Some(capabilities);
4605 self
4606 }
4607
4608 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
4610 self.memory = Some(memory);
4611 self
4612 }
4613
4614 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4616 self.config_directory = Some(dir.into());
4617 self
4618 }
4619
4620 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4622 self.working_directory = Some(dir.into());
4623 self
4624 }
4625
4626 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
4628 where
4629 I: IntoIterator<Item = P>,
4630 P: Into<PathBuf>,
4631 {
4632 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
4633 self
4634 }
4635
4636 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
4640 self.github_token = Some(token.into());
4641 self
4642 }
4643
4644 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
4650 self.github_token_provider = Some(provider);
4651 self
4652 }
4653
4654 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4656 self.include_sub_agent_streaming_events = Some(include);
4657 self
4658 }
4659
4660 pub fn with_remote_session(
4662 mut self,
4663 mode: crate::generated::api_types::RemoteSessionMode,
4664 ) -> Self {
4665 self.remote_session = Some(mode);
4666 self
4667 }
4668
4669 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4672 self.suppress_resume_event = Some(suppress);
4673 self
4674 }
4675
4676 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4682 self.continue_pending_work = Some(continue_pending);
4683 self
4684 }
4685
4686 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4688 self.skip_custom_instructions = Some(value);
4689 self
4690 }
4691
4692 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4694 self.custom_agents_local_only = Some(value);
4695 self
4696 }
4697
4698 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
4700 self.enable_experimental_mode = Some(enable_experimental_mode);
4701 self
4702 }
4703
4704 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4706 self.coauthor_enabled = Some(value);
4707 self
4708 }
4709
4710 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4712 self.manage_schedule_enabled = Some(value);
4713 self
4714 }
4715
4716 pub fn with_feature_flags(mut self, feature_flags: HashMap<String, bool>) -> Self {
4718 self.feature_flags = Some(feature_flags);
4719 self
4720 }
4721
4722 pub fn with_event_buffer_capacity(mut self, capacity: usize) -> Self {
4730 self.event_buffer_capacity = Some(capacity);
4731 self
4732 }
4733
4734 #[doc(hidden)]
4738 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4739 self.exp_assignments = Some(assignments);
4740 self
4741 }
4742
4743 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4746 self.enable_managed_settings = Some(enabled);
4747 self
4748 }
4749
4750 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
4754 self.managed_settings = Some(managed_settings);
4755 self
4756 }
4757}
4758
4759#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4765#[serde(rename_all = "camelCase")]
4766#[non_exhaustive]
4767pub struct SystemMessageConfig {
4768 #[serde(skip_serializing_if = "Option::is_none")]
4770 pub mode: Option<String>,
4771 #[serde(skip_serializing_if = "Option::is_none")]
4773 pub content: Option<String>,
4774 #[serde(skip_serializing_if = "Option::is_none")]
4776 pub sections: Option<HashMap<String, SectionOverride>>,
4777}
4778
4779impl SystemMessageConfig {
4780 pub fn new() -> Self {
4783 Self::default()
4784 }
4785
4786 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4789 self.mode = Some(mode.into());
4790 self
4791 }
4792
4793 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4796 self.content = Some(content.into());
4797 self
4798 }
4799
4800 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4802 self.sections = Some(sections);
4803 self
4804 }
4805}
4806
4807#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4813#[serde(rename_all = "camelCase")]
4814pub struct SectionOverride {
4815 #[serde(skip_serializing_if = "Option::is_none")]
4818 pub action: Option<String>,
4819 #[serde(skip_serializing_if = "Option::is_none")]
4821 pub content: Option<String>,
4822}
4823
4824#[derive(Debug, Clone, Serialize, Deserialize)]
4826#[serde(rename_all = "camelCase")]
4827pub struct CreateSessionResult {
4828 pub session_id: SessionId,
4830 #[serde(skip_serializing_if = "Option::is_none")]
4832 pub workspace_path: Option<PathBuf>,
4833 #[serde(default, alias = "remote_url")]
4835 pub remote_url: Option<String>,
4836 #[serde(skip_serializing_if = "Option::is_none")]
4838 pub capabilities: Option<SessionCapabilities>,
4839}
4840
4841#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4843#[serde(rename_all = "camelCase")]
4844pub(crate) struct ResumeSessionResult {
4845 #[serde(default)]
4847 pub session_id: Option<SessionId>,
4848 #[serde(default, skip_serializing_if = "Option::is_none")]
4850 pub workspace_path: Option<PathBuf>,
4851 #[serde(default, alias = "remote_url")]
4853 pub remote_url: Option<String>,
4854 #[serde(default, skip_serializing_if = "Option::is_none")]
4856 pub capabilities: Option<SessionCapabilities>,
4857 #[serde(
4859 default,
4860 alias = "openCanvasInstances",
4861 skip_serializing_if = "Option::is_none"
4862 )]
4863 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4864}
4865
4866#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4868#[serde(rename_all = "lowercase")]
4869pub enum LogLevel {
4870 #[default]
4872 Info,
4873 Warning,
4875 Error,
4877}
4878
4879#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4884#[serde(rename_all = "camelCase")]
4885pub struct LogOptions {
4886 #[serde(skip_serializing_if = "Option::is_none")]
4888 pub level: Option<LogLevel>,
4889 #[serde(skip_serializing_if = "Option::is_none")]
4892 pub ephemeral: Option<bool>,
4893}
4894
4895impl LogOptions {
4896 pub fn with_level(mut self, level: LogLevel) -> Self {
4898 self.level = Some(level);
4899 self
4900 }
4901
4902 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4904 self.ephemeral = Some(ephemeral);
4905 self
4906 }
4907}
4908
4909#[derive(Debug, Clone, Default)]
4913pub struct SetModelOptions {
4914 pub reasoning_effort: Option<String>,
4917 pub reasoning_summary: Option<ReasoningSummary>,
4921 pub context_tier: Option<ContextTier>,
4924 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4928 pub auto_tier: Option<AutoTierPreference>,
4936}
4937
4938#[derive(Debug, Clone, PartialEq, Eq)]
4947pub enum AutoTierPreference {
4948 Tier(AutoTier),
4950 Reset,
4952}
4953
4954impl SetModelOptions {
4955 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4957 self.reasoning_effort = Some(effort.into());
4958 self
4959 }
4960
4961 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4963 self.reasoning_summary = Some(summary);
4964 self
4965 }
4966
4967 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4969 self.context_tier = Some(tier);
4970 self
4971 }
4972
4973 pub fn with_model_capabilities(
4975 mut self,
4976 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4977 ) -> Self {
4978 self.model_capabilities = Some(caps);
4979 self
4980 }
4981
4982 pub fn with_auto_tier(mut self, tier: AutoTier) -> Self {
4984 self.auto_tier = Some(AutoTierPreference::Tier(tier));
4985 self
4986 }
4987
4988 pub fn with_reset_auto_tier(mut self) -> Self {
4991 self.auto_tier = Some(AutoTierPreference::Reset);
4992 self
4993 }
4994}
4995
4996#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
5003#[serde(rename_all = "camelCase")]
5004pub struct PingResponse {
5005 #[serde(default)]
5007 pub message: String,
5008 #[serde(default)]
5010 pub timestamp: String,
5011 #[serde(skip_serializing_if = "Option::is_none")]
5013 pub protocol_version: Option<u32>,
5014}
5015
5016#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5018#[serde(rename_all = "camelCase")]
5019pub struct AttachmentLineRange {
5020 pub start: u32,
5022 pub end: u32,
5024}
5025
5026#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5028#[serde(rename_all = "camelCase")]
5029pub struct AttachmentSelectionPosition {
5030 pub line: u32,
5032 pub character: u32,
5034}
5035
5036#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5038#[serde(rename_all = "camelCase")]
5039pub struct AttachmentSelectionRange {
5040 pub start: AttachmentSelectionPosition,
5042 pub end: AttachmentSelectionPosition,
5044}
5045
5046#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5048#[serde(rename_all = "snake_case")]
5049#[non_exhaustive]
5050pub enum GitHubReferenceType {
5051 Issue,
5053 Pr,
5055 Discussion,
5057}
5058
5059#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5065#[serde(rename_all = "camelCase")]
5066pub struct GitHubRepoPointer {
5067 #[serde(skip_serializing_if = "Option::is_none")]
5069 pub id: Option<i64>,
5070 pub name: String,
5072 pub owner: String,
5074}
5075
5076#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5078#[serde(rename_all = "camelCase")]
5079pub struct GitHubFileDiffSide {
5080 pub path: String,
5082 pub r#ref: String,
5084 pub repo: GitHubRepoPointer,
5086}
5087
5088#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5090#[serde(rename_all = "camelCase")]
5091pub struct GitHubTreeComparisonSide {
5092 pub repo: GitHubRepoPointer,
5094 pub revision: String,
5096}
5097
5098#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5100#[serde(rename_all = "camelCase")]
5101pub struct GitHubSnippetLineRange {
5102 pub start: i64,
5104 pub end: i64,
5106}
5107
5108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5110#[serde(
5111 tag = "type",
5112 rename_all = "camelCase",
5113 rename_all_fields = "camelCase"
5114)]
5115#[non_exhaustive]
5116pub enum Attachment {
5117 File {
5119 path: PathBuf,
5121 #[serde(skip_serializing_if = "Option::is_none")]
5123 display_name: Option<String>,
5124 #[serde(skip_serializing_if = "Option::is_none")]
5126 line_range: Option<AttachmentLineRange>,
5127 },
5128 Directory {
5130 path: PathBuf,
5132 #[serde(skip_serializing_if = "Option::is_none")]
5134 display_name: Option<String>,
5135 },
5136 Selection {
5138 file_path: PathBuf,
5140 text: String,
5142 #[serde(skip_serializing_if = "Option::is_none")]
5144 display_name: Option<String>,
5145 selection: AttachmentSelectionRange,
5147 },
5148 Blob {
5150 data: String,
5152 mime_type: String,
5154 #[serde(skip_serializing_if = "Option::is_none")]
5156 display_name: Option<String>,
5157 },
5158 #[serde(rename = "extension_context")]
5160 ExtensionContext {
5161 captured_at: String,
5163 extension_id: String,
5165 #[serde(skip_serializing_if = "Option::is_none")]
5167 canvas_id: Option<String>,
5168 #[serde(skip_serializing_if = "Option::is_none")]
5170 instance_id: Option<String>,
5171 title: String,
5173 #[serde(skip_serializing_if = "Option::is_none")]
5175 payload: Option<Value>,
5176 },
5177 #[serde(rename = "github_reference")]
5179 GitHubReference {
5180 number: u64,
5182 title: String,
5184 reference_type: GitHubReferenceType,
5186 state: String,
5188 url: String,
5190 },
5191 #[serde(rename = "github_commit")]
5193 GitHubCommit {
5194 message: String,
5196 oid: String,
5198 repo: GitHubRepoPointer,
5200 url: String,
5202 },
5203 #[serde(rename = "github_release")]
5205 GitHubRelease {
5206 name: String,
5208 repo: GitHubRepoPointer,
5210 tag_name: String,
5212 url: String,
5214 },
5215 #[serde(rename = "github_actions_job")]
5217 GitHubActionsJob {
5218 #[serde(skip_serializing_if = "Option::is_none")]
5221 conclusion: Option<String>,
5222 job_id: i64,
5224 job_name: String,
5226 repo: GitHubRepoPointer,
5228 url: String,
5230 workflow_name: String,
5232 },
5233 #[serde(rename = "github_repository")]
5235 GitHubRepository {
5236 #[serde(skip_serializing_if = "Option::is_none")]
5238 description: Option<String>,
5239 #[serde(skip_serializing_if = "Option::is_none")]
5242 r#ref: Option<String>,
5243 repo: GitHubRepoPointer,
5245 url: String,
5247 },
5248 #[serde(rename = "github_file_diff")]
5250 GitHubFileDiff {
5251 #[serde(skip_serializing_if = "Option::is_none")]
5253 base: Option<GitHubFileDiffSide>,
5254 #[serde(skip_serializing_if = "Option::is_none")]
5256 head: Option<GitHubFileDiffSide>,
5257 url: String,
5259 },
5260 #[serde(rename = "github_tree_comparison")]
5262 GitHubTreeComparison {
5263 base: GitHubTreeComparisonSide,
5265 head: GitHubTreeComparisonSide,
5267 url: String,
5269 },
5270 #[serde(rename = "github_url")]
5272 GitHubUrl {
5273 url: String,
5275 },
5276 #[serde(rename = "github_file")]
5278 GitHubFile {
5279 path: String,
5281 r#ref: String,
5283 repo: GitHubRepoPointer,
5285 url: String,
5287 },
5288 #[serde(rename = "github_snippet")]
5290 GitHubSnippet {
5291 line_range: GitHubSnippetLineRange,
5293 path: String,
5295 r#ref: String,
5297 repo: GitHubRepoPointer,
5299 url: String,
5301 },
5302}
5303
5304impl Attachment {
5305 pub fn display_name(&self) -> Option<&str> {
5307 match self {
5308 Self::File { display_name, .. }
5309 | Self::Directory { display_name, .. }
5310 | Self::Selection { display_name, .. }
5311 | Self::Blob { display_name, .. } => display_name.as_deref(),
5312 Self::GitHubReference { .. }
5313 | Self::GitHubCommit { .. }
5314 | Self::GitHubRelease { .. }
5315 | Self::GitHubActionsJob { .. }
5316 | Self::GitHubRepository { .. }
5317 | Self::GitHubFileDiff { .. }
5318 | Self::GitHubTreeComparison { .. }
5319 | Self::GitHubUrl { .. }
5320 | Self::GitHubFile { .. }
5321 | Self::GitHubSnippet { .. }
5322 | Self::ExtensionContext { .. } => None,
5323 }
5324 }
5325
5326 pub fn label(&self) -> Option<String> {
5328 if let Some(display_name) = self
5329 .display_name()
5330 .map(str::trim)
5331 .filter(|name| !name.is_empty())
5332 {
5333 return Some(display_name.to_string());
5334 }
5335
5336 match self {
5337 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
5338 format!("#{}", number)
5339 } else {
5340 title.trim().to_string()
5341 }),
5342 Self::ExtensionContext { title, .. } if !title.trim().is_empty() => {
5343 Some(title.trim().to_string())
5344 }
5345 _ => self.derived_display_name(),
5346 }
5347 }
5348
5349 pub fn ensure_display_name(&mut self) {
5351 if self
5352 .display_name()
5353 .map(str::trim)
5354 .is_some_and(|name| !name.is_empty())
5355 {
5356 return;
5357 }
5358
5359 let Some(derived_display_name) = self.derived_display_name() else {
5360 return;
5361 };
5362
5363 match self {
5364 Self::File { display_name, .. }
5365 | Self::Directory { display_name, .. }
5366 | Self::Selection { display_name, .. }
5367 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
5368 Self::GitHubReference { .. }
5369 | Self::GitHubCommit { .. }
5370 | Self::GitHubRelease { .. }
5371 | Self::GitHubActionsJob { .. }
5372 | Self::GitHubRepository { .. }
5373 | Self::GitHubFileDiff { .. }
5374 | Self::GitHubTreeComparison { .. }
5375 | Self::GitHubUrl { .. }
5376 | Self::GitHubFile { .. }
5377 | Self::GitHubSnippet { .. }
5378 | Self::ExtensionContext { .. } => {}
5379 }
5380 }
5381
5382 fn derived_display_name(&self) -> Option<String> {
5383 match self {
5384 Self::File { path, .. } | Self::Directory { path, .. } => {
5385 Some(attachment_name_from_path(path))
5386 }
5387 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
5388 Self::Blob { .. } => Some("attachment".to_string()),
5389 Self::GitHubReference { .. }
5390 | Self::GitHubCommit { .. }
5391 | Self::GitHubRelease { .. }
5392 | Self::GitHubActionsJob { .. }
5393 | Self::GitHubRepository { .. }
5394 | Self::GitHubFileDiff { .. }
5395 | Self::GitHubTreeComparison { .. }
5396 | Self::GitHubUrl { .. }
5397 | Self::GitHubFile { .. }
5398 | Self::GitHubSnippet { .. }
5399 | Self::ExtensionContext { .. } => None,
5400 }
5401 }
5402}
5403
5404fn attachment_name_from_path(path: &Path) -> String {
5405 path.file_name()
5406 .map(|name| name.to_string_lossy().into_owned())
5407 .filter(|name| !name.is_empty())
5408 .unwrap_or_else(|| {
5409 let full = path.to_string_lossy();
5410 if full.is_empty() {
5411 "attachment".to_string()
5412 } else {
5413 full.into_owned()
5414 }
5415 })
5416}
5417
5418pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
5420 for attachment in attachments {
5421 attachment.ensure_display_name();
5422 }
5423}
5424
5425#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5430#[non_exhaustive]
5431pub enum MessageSource {
5432 User,
5434 System,
5436 Agent(String),
5438}
5439
5440impl std::fmt::Display for MessageSource {
5441 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5442 match self {
5443 Self::User => f.write_str("user"),
5444 Self::System => f.write_str("system"),
5445 Self::Agent(id) => write!(f, "agent-{id}"),
5446 }
5447 }
5448}
5449
5450impl Serialize for MessageSource {
5451 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
5452 serializer.collect_str(self)
5453 }
5454}
5455
5456impl<'de> Deserialize<'de> for MessageSource {
5457 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
5458 let value = String::deserialize(deserializer)?;
5459 match value.as_str() {
5460 "user" => Ok(Self::User),
5461 "system" => Ok(Self::System),
5462 value => value
5463 .strip_prefix("agent-")
5464 .map(|id| Self::Agent(id.to_owned()))
5465 .ok_or_else(|| serde::de::Error::custom("expected user, system, or agent-<id>")),
5466 }
5467 }
5468}
5469
5470#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5475#[serde(rename_all = "lowercase")]
5476#[non_exhaustive]
5477pub enum DeliveryMode {
5478 Enqueue,
5480 Immediate,
5482}
5483
5484#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5489#[serde(rename_all = "lowercase")]
5490#[non_exhaustive]
5491pub enum AgentMode {
5492 Interactive,
5494 Plan,
5496 Autopilot,
5498 Shell,
5500}
5501
5502#[derive(Debug, Clone)]
5531#[non_exhaustive]
5532pub struct MessageOptions {
5533 pub response_schema: Option<Value>,
5536 pub prompt: String,
5538 pub source: Option<MessageSource>,
5541 pub mode: Option<DeliveryMode>,
5547 pub agent_mode: Option<AgentMode>,
5551 pub attachments: Option<Vec<Attachment>>,
5553 pub wait_timeout: Option<Duration>,
5556 pub request_headers: Option<HashMap<String, String>>,
5560 pub traceparent: Option<String>,
5567 pub tracestate: Option<String>,
5571 pub display_prompt: Option<String>,
5573}
5574
5575impl MessageOptions {
5576 pub fn new(prompt: impl Into<String>) -> Self {
5578 Self {
5579 prompt: prompt.into(),
5580 response_schema: None,
5581 source: None,
5582 mode: None,
5583 agent_mode: None,
5584 attachments: None,
5585 wait_timeout: None,
5586 request_headers: None,
5587 traceparent: None,
5588 tracestate: None,
5589 display_prompt: None,
5590 }
5591 }
5592
5593 pub fn with_source(mut self, source: MessageSource) -> Self {
5595 self.source = Some(source);
5596 self
5597 }
5598
5599 pub fn with_response_schema(mut self, schema: Value) -> Self {
5601 self.response_schema = Some(schema);
5602 self
5603 }
5604
5605 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
5611 self.mode = Some(mode);
5612 self
5613 }
5614
5615 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
5619 self.agent_mode = Some(agent_mode);
5620 self
5621 }
5622
5623 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
5625 self.attachments = Some(attachments);
5626 self
5627 }
5628
5629 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
5631 self.wait_timeout = Some(timeout);
5632 self
5633 }
5634
5635 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
5637 self.request_headers = Some(headers);
5638 self
5639 }
5640
5641 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
5646 self.traceparent = ctx.traceparent;
5647 self.tracestate = ctx.tracestate;
5648 self
5649 }
5650
5651 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5653 self.traceparent = Some(traceparent.into());
5654 self
5655 }
5656
5657 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5659 self.tracestate = Some(tracestate.into());
5660 self
5661 }
5662
5663 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5665 self.display_prompt = Some(display_prompt.into());
5666 self
5667 }
5668}
5669
5670impl From<&str> for MessageOptions {
5671 fn from(prompt: &str) -> Self {
5672 Self::new(prompt)
5673 }
5674}
5675
5676impl From<String> for MessageOptions {
5677 fn from(prompt: String) -> Self {
5678 Self::new(prompt)
5679 }
5680}
5681
5682impl From<&String> for MessageOptions {
5683 fn from(prompt: &String) -> Self {
5684 Self::new(prompt.clone())
5685 }
5686}
5687
5688#[derive(Debug, Clone, Serialize, Deserialize)]
5690#[serde(rename_all = "camelCase")]
5691#[non_exhaustive]
5692pub struct GetStatusResponse {
5693 pub version: String,
5695 pub protocol_version: u32,
5697}
5698
5699#[derive(Debug, Clone, Serialize, Deserialize)]
5701#[serde(rename_all = "camelCase")]
5702#[non_exhaustive]
5703pub struct GetAuthStatusResponse {
5704 pub is_authenticated: bool,
5706 #[serde(skip_serializing_if = "Option::is_none")]
5709 pub auth_type: Option<String>,
5710 #[serde(skip_serializing_if = "Option::is_none")]
5712 pub host: Option<String>,
5713 #[serde(skip_serializing_if = "Option::is_none")]
5715 pub login: Option<String>,
5716 #[serde(skip_serializing_if = "Option::is_none")]
5718 pub status_message: Option<String>,
5719}
5720
5721#[derive(Debug, Clone, Serialize, Deserialize)]
5725#[serde(rename_all = "camelCase")]
5726pub struct SessionEventNotification {
5727 pub session_id: SessionId,
5729 pub event: SessionEvent,
5731}
5732
5733#[derive(Debug, Clone, Serialize, Deserialize)]
5740#[serde(rename_all = "camelCase")]
5741pub struct SessionEvent {
5742 pub id: String,
5744 pub timestamp: String,
5746 pub parent_id: Option<String>,
5748 #[serde(skip_serializing_if = "Option::is_none")]
5750 pub ephemeral: Option<bool>,
5751 #[serde(skip_serializing_if = "Option::is_none")]
5754 pub agent_id: Option<String>,
5755 #[serde(skip_serializing_if = "Option::is_none")]
5757 pub debug_cli_received_at_ms: Option<i64>,
5758 #[serde(skip_serializing_if = "Option::is_none")]
5760 pub debug_ws_forwarded_at_ms: Option<i64>,
5761 #[serde(rename = "type")]
5763 pub event_type: String,
5764 pub data: Value,
5766}
5767
5768impl SessionEvent {
5769 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5774 use serde::de::IntoDeserializer;
5775 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5776 self.event_type.as_str().into_deserializer();
5777 crate::generated::SessionEventType::deserialize(deserializer)
5778 .unwrap_or(crate::generated::SessionEventType::Unknown)
5779 }
5780
5781 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5787 serde_json::from_value(self.data.clone()).ok()
5788 }
5789
5790 pub fn is_transient_error(&self) -> bool {
5794 self.event_type == "session.error"
5795 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5796 }
5797}
5798
5799#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5804#[serde(rename_all = "camelCase")]
5805#[non_exhaustive]
5806pub struct ToolInvocation {
5807 pub session_id: SessionId,
5809 pub tool_call_id: String,
5811 pub tool_name: String,
5813 pub arguments: Value,
5815 #[serde(skip)]
5823 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5824 #[serde(default, skip_serializing_if = "Option::is_none")]
5829 pub traceparent: Option<String>,
5830 #[serde(default, skip_serializing_if = "Option::is_none")]
5833 pub tracestate: Option<String>,
5834}
5835
5836impl ToolInvocation {
5837 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5858 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5859 }
5860
5861 pub fn trace_context(&self) -> TraceContext {
5864 TraceContext {
5865 traceparent: self.traceparent.clone(),
5866 tracestate: self.tracestate.clone(),
5867 }
5868 }
5869}
5870
5871#[derive(Debug, Clone, Serialize, Deserialize)]
5873#[serde(rename_all = "camelCase")]
5874pub struct ToolBinaryResult {
5875 pub data: String,
5877 pub mime_type: String,
5879 pub r#type: String,
5881 #[serde(default, skip_serializing_if = "Option::is_none")]
5883 pub description: Option<String>,
5884}
5885
5886#[derive(Debug, Clone, Serialize, Deserialize)]
5893#[serde(rename_all = "camelCase")]
5894#[non_exhaustive]
5895pub struct ToolResultExpanded {
5896 pub text_result_for_llm: String,
5898 pub result_type: String,
5900 #[serde(default, skip_serializing_if = "Option::is_none")]
5902 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5903 #[serde(skip_serializing_if = "Option::is_none")]
5905 pub session_log: Option<String>,
5906 #[serde(skip_serializing_if = "Option::is_none")]
5908 pub error: Option<String>,
5909 #[serde(default, skip_serializing_if = "Option::is_none")]
5911 pub tool_telemetry: Option<HashMap<String, Value>>,
5912 #[serde(default, skip_serializing_if = "Option::is_none")]
5914 pub tool_references: Option<Vec<String>>,
5915}
5916
5917impl ToolResultExpanded {
5918 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5922 Self {
5923 text_result_for_llm: text_result_for_llm.into(),
5924 result_type: result_type.into(),
5925 binary_results_for_llm: None,
5926 session_log: None,
5927 error: None,
5928 tool_telemetry: None,
5929 tool_references: None,
5930 }
5931 }
5932
5933 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5935 self.binary_results_for_llm = Some(results);
5936 self
5937 }
5938
5939 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5941 self.session_log = Some(session_log.into());
5942 self
5943 }
5944
5945 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5947 self.error = Some(error.into());
5948 self
5949 }
5950
5951 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5953 self.tool_telemetry = Some(telemetry);
5954 self
5955 }
5956
5957 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5959 where
5960 I: IntoIterator<Item = S>,
5961 S: Into<String>,
5962 {
5963 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5964 self
5965 }
5966}
5967
5968#[derive(Debug, Clone, Serialize, Deserialize)]
5970#[serde(untagged)]
5971#[non_exhaustive]
5972pub enum ToolResult {
5973 Text(String),
5975 Expanded(ToolResultExpanded),
5977}
5978
5979#[derive(Debug, Clone, Serialize, Deserialize)]
5981#[serde(rename_all = "camelCase")]
5982pub struct ToolResultResponse {
5983 pub result: ToolResult,
5985}
5986
5987#[derive(Debug, Clone, Serialize, Deserialize)]
5989#[serde(rename_all = "camelCase")]
5990pub struct SessionMetadata {
5991 pub session_id: SessionId,
5993 pub start_time: String,
5995 pub modified_time: String,
5997 #[serde(skip_serializing_if = "Option::is_none")]
5999 pub summary: Option<String>,
6000 pub is_remote: bool,
6002}
6003
6004#[derive(Debug, Clone, Serialize, Deserialize)]
6006#[serde(rename_all = "camelCase")]
6007pub struct ListSessionsResponse {
6008 pub sessions: Vec<SessionMetadata>,
6010}
6011
6012#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6016#[serde(rename_all = "camelCase")]
6017pub struct SessionListFilter {
6018 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
6020 pub working_directory: Option<String>,
6021 #[serde(default, skip_serializing_if = "Option::is_none")]
6023 pub git_root: Option<String>,
6024 #[serde(default, skip_serializing_if = "Option::is_none")]
6026 pub repository: Option<String>,
6027 #[serde(default, skip_serializing_if = "Option::is_none")]
6029 pub branch: Option<String>,
6030}
6031
6032#[derive(Debug, Clone, Serialize, Deserialize)]
6034#[serde(rename_all = "camelCase")]
6035pub struct GetSessionMetadataResponse {
6036 #[serde(skip_serializing_if = "Option::is_none")]
6038 pub session: Option<SessionMetadata>,
6039}
6040
6041#[derive(Debug, Clone, Serialize, Deserialize)]
6043#[serde(rename_all = "camelCase")]
6044pub struct GetLastSessionIdResponse {
6045 #[serde(skip_serializing_if = "Option::is_none")]
6047 pub session_id: Option<SessionId>,
6048}
6049
6050#[derive(Debug, Clone, Serialize, Deserialize)]
6052#[serde(rename_all = "camelCase")]
6053pub struct GetForegroundSessionResponse {
6054 #[serde(skip_serializing_if = "Option::is_none")]
6056 pub session_id: Option<SessionId>,
6057}
6058
6059#[derive(Debug, Clone, Serialize, Deserialize)]
6061#[serde(rename_all = "camelCase")]
6062pub struct GetMessagesResponse {
6063 pub events: Vec<SessionEvent>,
6065}
6066
6067#[derive(Debug, Clone, Serialize, Deserialize)]
6069#[serde(rename_all = "camelCase")]
6070pub struct ElicitationResult {
6071 pub action: String,
6073 #[serde(skip_serializing_if = "Option::is_none")]
6075 pub content: Option<Value>,
6076}
6077
6078#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6084#[serde(rename_all = "camelCase")]
6085#[non_exhaustive]
6086pub enum ElicitationMode {
6087 Form,
6089 Url,
6091 #[serde(other)]
6093 Unknown,
6094}
6095
6096#[derive(Debug, Clone, Serialize, Deserialize)]
6103#[serde(rename_all = "camelCase")]
6104pub struct ElicitationRequest {
6105 pub message: String,
6107 #[serde(skip_serializing_if = "Option::is_none")]
6109 pub requested_schema: Option<Value>,
6110 #[serde(skip_serializing_if = "Option::is_none")]
6112 pub mode: Option<ElicitationMode>,
6113 #[serde(skip_serializing_if = "Option::is_none")]
6115 pub elicitation_source: Option<String>,
6116 #[serde(skip_serializing_if = "Option::is_none")]
6118 pub url: Option<String>,
6119}
6120
6121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6126#[serde(rename_all = "camelCase")]
6127pub struct SessionCapabilities {
6128 #[serde(skip_serializing_if = "Option::is_none")]
6130 pub ui: Option<UiCapabilities>,
6131}
6132
6133#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6135#[serde(rename_all = "camelCase")]
6136pub struct UiCapabilities {
6137 #[serde(skip_serializing_if = "Option::is_none")]
6139 pub elicitation: Option<bool>,
6140 #[serde(skip_serializing_if = "Option::is_none")]
6151 pub mcp_apps: Option<bool>,
6152 #[serde(skip_serializing_if = "Option::is_none")]
6154 pub canvases: Option<bool>,
6155}
6156
6157#[derive(Debug, Clone, Default)]
6159pub struct UiInputOptions<'a> {
6160 pub title: Option<&'a str>,
6162 pub description: Option<&'a str>,
6164 pub min_length: Option<u64>,
6166 pub max_length: Option<u64>,
6168 pub format: Option<InputFormat>,
6170 pub default: Option<&'a str>,
6172}
6173
6174#[derive(Debug, Clone, Copy)]
6176#[non_exhaustive]
6177pub enum InputFormat {
6178 Email,
6180 Uri,
6182 Date,
6184 DateTime,
6186}
6187
6188impl InputFormat {
6189 pub fn as_str(&self) -> &'static str {
6191 match self {
6192 Self::Email => "email",
6193 Self::Uri => "uri",
6194 Self::Date => "date",
6195 Self::DateTime => "date-time",
6196 }
6197 }
6198}
6199
6200pub use crate::generated::api_types::{
6205 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
6206 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
6207 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
6208 PermissionDecisionApproveOnce, PermissionDecisionContext, PermissionDecisionOutcome,
6209 PermissionDecisionReject, PermissionDecisionSource, PermissionDecisionSurface,
6210 PermissionDecisionUserNotAvailable, PermissionResponseCapability,
6211};
6212
6213#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6219#[serde(rename_all = "kebab-case")]
6220#[non_exhaustive]
6221pub enum PermissionRequestKind {
6222 Shell,
6224 Write,
6226 Read,
6228 Url,
6230 Mcp,
6232 CustomTool,
6234 Memory,
6236 Hook,
6238 #[serde(other)]
6241 Unknown,
6242}
6243
6244#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6250#[serde(rename_all = "camelCase")]
6251pub struct PermissionRequestData {
6252 #[serde(default, skip_serializing_if = "Option::is_none")]
6256 pub kind: Option<PermissionRequestKind>,
6257 #[serde(default, skip_serializing_if = "Option::is_none")]
6260 pub tool_call_id: Option<String>,
6261 #[serde(default, skip_serializing_if = "Option::is_none")]
6263 pub managed_approval_required: Option<bool>,
6264 #[serde(default, skip_serializing_if = "is_false")]
6266 pub managed_settings_enabled: bool,
6267 #[serde(flatten)]
6271 pub extra: Value,
6272}
6273
6274#[derive(Debug, Clone, Serialize, Deserialize)]
6276#[serde(rename_all = "camelCase")]
6277pub struct ExitPlanModeData {
6278 #[serde(default)]
6280 pub summary: String,
6281 #[serde(default, skip_serializing_if = "Option::is_none")]
6283 pub plan_content: Option<String>,
6284 #[serde(default)]
6286 pub actions: Vec<String>,
6287 #[serde(default = "default_recommended_action")]
6289 pub recommended_action: String,
6290}
6291
6292fn default_recommended_action() -> String {
6293 "autopilot".to_string()
6294}
6295
6296impl Default for ExitPlanModeData {
6297 fn default() -> Self {
6298 Self {
6299 summary: String::new(),
6300 plan_content: None,
6301 actions: Vec::new(),
6302 recommended_action: default_recommended_action(),
6303 }
6304 }
6305}
6306
6307#[cfg(test)]
6308mod tests {
6309 use std::collections::HashMap;
6310 use std::path::PathBuf;
6311
6312 use serde_json::json;
6313
6314 use super::{
6315 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
6316 AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions,
6317 ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode,
6318 ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
6319 InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
6320 MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig,
6321 ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
6322 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
6323 ToolResultResponse, ensure_attachment_display_names,
6324 };
6325 use crate::generated::session_events::TypedSessionEvent;
6326
6327 #[test]
6328 fn permission_response_capability_is_publicly_exported() {
6329 assert_eq!(
6330 serde_json::to_value(PermissionResponseCapability::Interactive).unwrap(),
6331 json!("interactive")
6332 );
6333 }
6334
6335 #[test]
6336 fn tool_builder_composes() {
6337 let tool = Tool::new("greet")
6338 .with_description("Say hello")
6339 .with_namespaced_name("hello/greet")
6340 .with_instructions("Pass the user's name")
6341 .with_parameters(json!({
6342 "type": "object",
6343 "properties": { "name": { "type": "string" } },
6344 "required": ["name"]
6345 }))
6346 .with_overrides_built_in_tool(true)
6347 .with_skip_permission(true);
6348 assert_eq!(tool.name, "greet");
6349 assert_eq!(tool.description, "Say hello");
6350 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
6351 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
6352 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
6353 assert!(tool.overrides_built_in_tool);
6354 assert!(tool.skip_permission);
6355 }
6356
6357 #[test]
6358 fn tool_defer_serialization() {
6359 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
6360 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
6361 let value = serde_json::to_value(&tool).unwrap();
6362 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
6363
6364 let plain = Tool::new("plain");
6365 let value = serde_json::to_value(&plain).unwrap();
6366 assert!(value.get("defer").is_none());
6367 }
6368
6369 #[test]
6370 fn tool_metadata_serialization() {
6371 use indexmap::IndexMap;
6372
6373 let mut metadata = IndexMap::new();
6374 metadata.insert(
6375 "github.com/copilot:safeForTelemetry".to_string(),
6376 json!({ "name": true, "inputsNames": false }),
6377 );
6378 let tool = Tool::new("lookup").with_metadata(metadata);
6379 let value = serde_json::to_value(&tool).unwrap();
6380 assert_eq!(
6381 value
6382 .get("metadata")
6383 .unwrap()
6384 .get("github.com/copilot:safeForTelemetry")
6385 .unwrap(),
6386 &json!({ "name": true, "inputsNames": false })
6387 );
6388
6389 let plain = Tool::new("plain");
6391 let value = serde_json::to_value(&plain).unwrap();
6392 assert!(value.get("metadata").is_none());
6393 }
6394
6395 #[test]
6396 fn custom_agent_config_builder_with_model() {
6397 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
6398 .with_model("claude-haiku-4.5")
6399 .with_display_name("My Agent");
6400 assert_eq!(agent.name, "my-agent");
6401 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
6402 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
6403 }
6404
6405 #[test]
6406 fn custom_agent_config_serializes_model() {
6407 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
6408 let wire = serde_json::to_value(&agent).unwrap();
6409 assert_eq!(wire["model"], "claude-haiku-4.5");
6410 assert_eq!(wire["name"], "model-agent");
6411 }
6412
6413 #[test]
6414 fn custom_agent_config_omits_model_when_none() {
6415 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
6416 let wire = serde_json::to_value(&agent).unwrap();
6417 assert!(wire.get("model").is_none());
6418 }
6419
6420 #[test]
6421 fn custom_agent_config_builder_with_reasoning_effort() {
6422 let agent =
6423 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6424 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
6425 }
6426
6427 #[test]
6428 fn custom_agent_config_serializes_reasoning_effort() {
6429 let agent =
6430 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6431 let wire = serde_json::to_value(&agent).unwrap();
6432 assert_eq!(wire["reasoningEffort"], "high");
6433 }
6434
6435 #[test]
6436 fn custom_agent_config_omits_reasoning_effort_when_none() {
6437 let agent = CustomAgentConfig::new("default-agent", "prompt");
6438 let wire = serde_json::to_value(&agent).unwrap();
6439 assert!(wire.get("reasoningEffort").is_none());
6440 }
6441
6442 #[test]
6443 #[should_panic(expected = "tool parameter schema must be a JSON object")]
6444 fn tool_with_parameters_panics_on_non_object_value() {
6445 let _ = Tool::new("noop").with_parameters(json!(null));
6446 }
6447
6448 #[test]
6449 fn tool_result_expanded_serializes_binary_results_for_llm() {
6450 let response = ToolResultResponse {
6451 result: ToolResult::Expanded(ToolResultExpanded {
6452 text_result_for_llm: "rendered chart".to_string(),
6453 result_type: "success".to_string(),
6454 binary_results_for_llm: Some(vec![ToolBinaryResult {
6455 data: "aW1n".to_string(),
6456 mime_type: "image/png".to_string(),
6457 r#type: "image".to_string(),
6458 description: Some("chart preview".to_string()),
6459 }]),
6460 session_log: None,
6461 error: None,
6462 tool_telemetry: None,
6463 tool_references: None,
6464 }),
6465 };
6466
6467 let wire = serde_json::to_value(&response).unwrap();
6468
6469 assert_eq!(
6470 wire,
6471 json!({
6472 "result": {
6473 "textResultForLlm": "rendered chart",
6474 "resultType": "success",
6475 "binaryResultsForLlm": [
6476 {
6477 "data": "aW1n",
6478 "mimeType": "image/png",
6479 "type": "image",
6480 "description": "chart preview"
6481 }
6482 ]
6483 }
6484 })
6485 );
6486 }
6487
6488 #[test]
6489 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
6490 let response = ToolResultResponse {
6491 result: ToolResult::Expanded(ToolResultExpanded {
6492 text_result_for_llm: "ok".to_string(),
6493 result_type: "success".to_string(),
6494 binary_results_for_llm: None,
6495 session_log: None,
6496 error: None,
6497 tool_telemetry: None,
6498 tool_references: None,
6499 }),
6500 };
6501
6502 let wire = serde_json::to_value(&response).unwrap();
6503
6504 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6505 assert!(wire["result"].get("binaryResultsForLlm").is_none());
6506 }
6507
6508 #[test]
6509 fn tool_result_expanded_serializes_tool_references() {
6510 let response = ToolResultResponse {
6511 result: ToolResult::Expanded(
6512 ToolResultExpanded::new("found 2 tools", "success")
6513 .with_tool_references(["get_weather", "check_status"]),
6514 ),
6515 };
6516
6517 let wire = serde_json::to_value(&response).unwrap();
6518
6519 assert_eq!(
6520 wire,
6521 json!({
6522 "result": {
6523 "textResultForLlm": "found 2 tools",
6524 "resultType": "success",
6525 "toolReferences": ["get_weather", "check_status"]
6526 }
6527 })
6528 );
6529 }
6530
6531 #[test]
6532 fn tool_result_expanded_omits_tool_references_when_none() {
6533 let response = ToolResultResponse {
6534 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
6535 };
6536
6537 let wire = serde_json::to_value(&response).unwrap();
6538
6539 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6540 assert!(wire["result"].get("toolReferences").is_none());
6541 }
6542
6543 #[test]
6544 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
6545 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
6548 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
6549
6550 assert_eq!(
6551 expanded.tool_references.as_deref(),
6552 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6553 );
6554 }
6555
6556 #[test]
6557 fn tool_result_expanded_deserializes_tool_references() {
6558 let wire = json!({
6559 "textResultForLlm": "found tools",
6560 "resultType": "success",
6561 "toolReferences": ["alpha", "beta"]
6562 });
6563
6564 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
6565
6566 assert_eq!(
6567 expanded.tool_references.as_deref(),
6568 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6569 );
6570 }
6571
6572 #[test]
6573 fn session_config_default_wire_flags_off_without_handlers() {
6574 let cfg = SessionConfig::default();
6575 assert_eq!(cfg.mcp_oauth_token_storage, None);
6576 assert_eq!(cfg.allowed_models, None);
6577 let (wire, _runtime) = cfg
6581 .into_wire(Some(SessionId::from("default-flags")))
6582 .expect("default config has no duplicate handlers");
6583 assert!(!wire.request_user_input);
6584 assert!(!wire.request_permission);
6585 assert!(!wire.request_elicitation);
6586 assert!(!wire.request_exit_plan_mode);
6587 assert!(!wire.request_auto_mode_switch);
6588 assert!(!wire.hooks);
6589 assert!(!wire.request_mcp_apps);
6590 let json = serde_json::to_value(&wire).unwrap();
6591 assert!(json.get("askUserVariant").is_none());
6592 assert!(json.get("allowedModels").is_none());
6593 }
6594
6595 #[test]
6596 fn resume_session_config_new_wire_flags_off_without_handlers() {
6597 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
6598 assert_eq!(cfg.mcp_oauth_token_storage, None);
6599 assert_eq!(cfg.allowed_models, None);
6600 let (wire, _runtime) = cfg
6601 .into_wire()
6602 .expect("default resume config has no duplicate handlers");
6603 assert!(!wire.request_user_input);
6604 assert!(!wire.request_permission);
6605 assert!(!wire.request_elicitation);
6606 assert!(!wire.request_exit_plan_mode);
6607 assert!(!wire.request_auto_mode_switch);
6608 assert!(!wire.hooks);
6609 assert!(!wire.request_mcp_apps);
6610 let json = serde_json::to_value(&wire).unwrap();
6611 assert!(json.get("askUserVariant").is_none());
6612 assert!(json.get("allowedModels").is_none());
6613 }
6614
6615 #[test]
6616 fn session_configs_build_debug_and_serialize_allowed_models() {
6617 let create = SessionConfig::default().with_allowed_models(["gpt-5.4", "claude-sonnet-4"]);
6618 assert_eq!(
6619 create.allowed_models.as_deref(),
6620 Some(&["gpt-5.4".to_string(), "claude-sonnet-4".to_string()][..])
6621 );
6622 assert!(format!("{create:?}").contains("allowed_models"));
6623
6624 let (create_wire, _) = create
6625 .into_wire(Some(SessionId::from("create-allowed-models")))
6626 .expect("allowed model config has no duplicate handlers");
6627 let create_json = serde_json::to_value(&create_wire).unwrap();
6628 assert_eq!(
6629 create_json["allowedModels"],
6630 json!(["gpt-5.4", "claude-sonnet-4"])
6631 );
6632
6633 let resume = ResumeSessionConfig::new(SessionId::from("resume-allowed-models"))
6634 .with_allowed_models(vec!["gpt-5.4".to_string(), "gpt-5-mini".to_string()]);
6635 assert_eq!(
6636 resume.allowed_models.as_deref(),
6637 Some(&["gpt-5.4".to_string(), "gpt-5-mini".to_string()][..])
6638 );
6639 assert!(format!("{resume:?}").contains("allowed_models"));
6640
6641 let (resume_wire, _) = resume
6642 .into_wire()
6643 .expect("resume allowed model config has no duplicate handlers");
6644 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6645 assert_eq!(
6646 resume_json["allowedModels"],
6647 json!(["gpt-5.4", "gpt-5-mini"])
6648 );
6649 }
6650
6651 #[test]
6652 fn custom_agents_local_only_serializes_on_create_and_resume() {
6653 let (create_wire, _) = SessionConfig::default()
6654 .with_custom_agents_local_only(false)
6655 .into_wire(Some(SessionId::from("create-locality")))
6656 .expect("create config has no duplicate handlers");
6657 let create_json = serde_json::to_value(&create_wire).unwrap();
6658 assert_eq!(create_json["customAgentsLocalOnly"], false);
6659
6660 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
6661 .with_custom_agents_local_only(false)
6662 .into_wire()
6663 .expect("resume config has no duplicate handlers");
6664 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6665 assert_eq!(resume_json["customAgentsLocalOnly"], false);
6666
6667 let (unset_create_wire, _) = SessionConfig::default()
6668 .into_wire(Some(SessionId::from("create-unset")))
6669 .expect("create config has no duplicate handlers");
6670 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6671 assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
6672
6673 let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
6674 .into_wire()
6675 .expect("resume config has no duplicate handlers");
6676 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6677 assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
6678 }
6679
6680 #[test]
6681 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6682 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
6683 assert_eq!(cfg.enable_mcp_apps, Some(true));
6684
6685 let (wire, _runtime) = cfg
6686 .into_wire(Some(SessionId::from("enable-mcp-apps")))
6687 .expect("enable_mcp_apps config has no duplicate handlers");
6688 assert!(wire.request_mcp_apps);
6689
6690 let json = serde_json::to_value(&wire).unwrap();
6691 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6692 }
6693
6694 #[test]
6695 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6696 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
6697 .with_enable_mcp_apps(true);
6698 assert_eq!(cfg.enable_mcp_apps, Some(true));
6699
6700 let (wire, _runtime) = cfg
6701 .into_wire()
6702 .expect("resume enable_mcp_apps config has no duplicate handlers");
6703 assert!(wire.request_mcp_apps);
6704
6705 let json = serde_json::to_value(&wire).unwrap();
6706 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6707 }
6708
6709 #[test]
6710 fn github_mcp_tool_config_serializes_for_create_and_resume() {
6711 let github_config = GitHubMcpToolConfig::new()
6712 .with_enable_all_tools(true)
6713 .with_additional_toolsets(["repos"])
6714 .with_additional_tools(["get_issue"])
6715 .with_enable_insiders_mode(true)
6716 .with_disable_form_deferral(true);
6717
6718 let (create_wire, _) = SessionConfig::default()
6719 .with_github_mcp_tool_config(github_config.clone())
6720 .into_wire(Some(SessionId::from("github-mcp")))
6721 .expect("create config has no duplicate handlers");
6722 assert_eq!(
6723 serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6724 serde_json::json!({
6725 "enableAllTools": true,
6726 "additionalToolsets": ["repos"],
6727 "additionalTools": ["get_issue"],
6728 "enableInsidersMode": true,
6729 "disableFormDeferral": true,
6730 })
6731 );
6732
6733 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6734 .with_github_mcp_tool_config(github_config)
6735 .into_wire()
6736 .expect("resume config has no duplicate handlers");
6737 assert!(resume_wire.github_mcp_tool_config.is_some());
6738
6739 let (unset_wire, _) = SessionConfig::default()
6740 .into_wire(Some(SessionId::from("github-mcp-unset")))
6741 .expect("default config has no duplicate handlers");
6742 assert!(
6743 serde_json::to_value(&unset_wire)
6744 .unwrap()
6745 .get("githubMcpToolConfig")
6746 .is_none()
6747 );
6748 }
6749
6750 #[test]
6751 fn memory_configuration_constructors_and_serde() {
6752 assert!(MemoryConfiguration::enabled().enabled);
6753 assert!(!MemoryConfiguration::disabled().enabled);
6754 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6755
6756 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6757 assert_eq!(json, serde_json::json!({ "enabled": true }));
6758 }
6759
6760 #[test]
6761 fn session_config_with_memory_serializes() {
6762 let (wire, _runtime) = SessionConfig::default()
6763 .with_memory(MemoryConfiguration::enabled())
6764 .into_wire(Some(SessionId::from("memory-on")))
6765 .expect("no duplicate handlers");
6766 let json = serde_json::to_value(&wire).unwrap();
6767 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6768
6769 let (wire_off, _) = SessionConfig::default()
6770 .with_memory(MemoryConfiguration::disabled())
6771 .into_wire(Some(SessionId::from("memory-off")))
6772 .expect("no duplicate handlers");
6773 let json_off = serde_json::to_value(&wire_off).unwrap();
6774 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6775
6776 let (empty_wire, _) = SessionConfig::default()
6778 .into_wire(Some(SessionId::from("memory-unset")))
6779 .expect("no duplicate handlers");
6780 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6781 assert!(empty_json.get("memory").is_none());
6782 }
6783
6784 #[test]
6785 fn resume_session_config_with_memory_serializes() {
6786 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6787 .with_memory(MemoryConfiguration::enabled())
6788 .into_wire()
6789 .expect("no duplicate handlers");
6790 let json = serde_json::to_value(&wire).unwrap();
6791 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6792
6793 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6795 .into_wire()
6796 .expect("no duplicate handlers");
6797 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6798 assert!(empty_json.get("memory").is_none());
6799 }
6800
6801 #[test]
6802 fn feature_flags_serialize_on_create_and_resume() {
6803 let feature_flags = HashMap::from([
6804 ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true),
6805 ("DISABLED_TEST_FLAG".to_string(), false),
6806 ]);
6807 let expected = serde_json::json!({
6808 "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true,
6809 "DISABLED_TEST_FLAG": false,
6810 });
6811
6812 let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone());
6813 assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags));
6814 let (create_wire, _) = create_config
6815 .into_wire(Some(SessionId::from("feature-flags-create")))
6816 .expect("no duplicate handlers");
6817 let create_json = serde_json::to_value(&create_wire).unwrap();
6818 assert_eq!(create_json["featureFlags"], expected);
6819
6820 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume"))
6821 .with_feature_flags(feature_flags)
6822 .into_wire()
6823 .expect("no duplicate handlers");
6824 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6825 assert_eq!(resume_json["featureFlags"], expected);
6826
6827 let (unset_create_wire, _) = SessionConfig::default()
6828 .into_wire(Some(SessionId::from("feature-flags-create-unset")))
6829 .expect("no duplicate handlers");
6830 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6831 assert!(unset_create_json.get("featureFlags").is_none());
6832
6833 let (unset_resume_wire, _) =
6834 ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset"))
6835 .into_wire()
6836 .expect("no duplicate handlers");
6837 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6838 assert!(unset_resume_json.get("featureFlags").is_none());
6839 }
6840
6841 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6842 CopilotExpAssignmentResponse {
6843 features: vec!["copilot_exp_flag".to_string()],
6844 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6845 configs: vec![ExpConfigEntry {
6846 id: "cfg-1".to_string(),
6847 parameters: HashMap::from([
6848 ("threshold".to_string(), ExpFlagValue::Integer(5)),
6849 ("enabled".to_string(), ExpFlagValue::Bool(true)),
6850 ]),
6851 }],
6852 assignment_context: context.to_string(),
6853 ..Default::default()
6854 }
6855 }
6856
6857 #[test]
6858 fn exp_flag_value_round_trips_all_variants() {
6859 let values = serde_json::json!({
6860 "s": "text",
6861 "i": 7,
6862 "f": 1.5,
6863 "b": true,
6864 "n": null,
6865 });
6866 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6867 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6868 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6869 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6870 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6871 assert_eq!(parsed["n"], ExpFlagValue::Null);
6872 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6873 }
6874
6875 #[test]
6876 fn session_config_with_exp_assignments_serializes() {
6877 let assignments = sample_exp_assignments("ctx-123");
6878 let expected = serde_json::to_value(&assignments).unwrap();
6879 let (wire, _runtime) = SessionConfig::default()
6880 .with_exp_assignments(assignments)
6881 .into_wire(Some(SessionId::from("exp-on")))
6882 .expect("no duplicate handlers");
6883 let json = serde_json::to_value(&wire).unwrap();
6884 assert_eq!(json["expAssignments"], expected);
6885 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6886 assert_eq!(
6887 json["expAssignments"]["Flights"]["copilot_exp_flag"],
6888 "treatment"
6889 );
6890
6891 let (empty_wire, _) = SessionConfig::default()
6893 .into_wire(Some(SessionId::from("exp-unset")))
6894 .expect("no duplicate handlers");
6895 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6896 assert!(empty_json.get("expAssignments").is_none());
6897 }
6898
6899 #[test]
6900 fn resume_session_config_with_exp_assignments_serializes() {
6901 let assignments = sample_exp_assignments("ctx-456");
6902 let expected = serde_json::to_value(&assignments).unwrap();
6903 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6904 .with_exp_assignments(assignments)
6905 .into_wire()
6906 .expect("no duplicate handlers");
6907 let json = serde_json::to_value(&wire).unwrap();
6908 assert_eq!(json["expAssignments"], expected);
6909
6910 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6912 .into_wire()
6913 .expect("no duplicate handlers");
6914 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6915 assert!(empty_json.get("expAssignments").is_none());
6916 }
6917
6918 #[test]
6919 fn session_config_clone_preserves_exp_assignments() {
6920 let assignments = sample_exp_assignments("ctx-clone");
6921 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6922 let cloned = config.clone();
6923
6924 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6925
6926 let (wire, _runtime) = cloned
6927 .into_wire(Some(SessionId::from("exp-clone")))
6928 .expect("no duplicate handlers");
6929 let json = serde_json::to_value(&wire).unwrap();
6930 assert_eq!(
6931 json["expAssignments"],
6932 serde_json::to_value(&assignments).unwrap()
6933 );
6934 }
6935
6936 #[test]
6937 fn resume_session_config_clone_preserves_exp_assignments() {
6938 let assignments = sample_exp_assignments("ctx-clone-resume");
6939 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6940 .with_exp_assignments(assignments.clone());
6941 let cloned = config.clone();
6942
6943 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6944
6945 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6946 let json = serde_json::to_value(&wire).unwrap();
6947 assert_eq!(
6948 json["expAssignments"],
6949 serde_json::to_value(&assignments).unwrap()
6950 );
6951 }
6952
6953 #[test]
6954 #[allow(clippy::field_reassign_with_default)]
6955 fn session_config_into_wire_serializes_bucket_b_fields() {
6956 use std::path::PathBuf;
6957
6958 use super::{CloudSessionOptions, CloudSessionRepository};
6959
6960 let mut cfg = SessionConfig::default();
6961 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6962 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6963 cfg.github_token = Some("ghs_secret".to_string());
6964 cfg.include_sub_agent_streaming_events = Some(false);
6965 cfg.enable_session_telemetry = Some(false);
6966 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
6967 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
6968 cfg.enable_on_demand_instruction_discovery = Some(false);
6969 cfg.cloud = Some(CloudSessionOptions::with_repository(
6970 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
6971 ));
6972
6973 let (wire, _runtime) = cfg
6974 .into_wire(Some(SessionId::from("custom-id")))
6975 .expect("no duplicate handlers");
6976 let wire_json = serde_json::to_value(&wire).unwrap();
6977 assert_eq!(wire_json["sessionId"], "custom-id");
6978 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6979 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6980 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6981 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6982 assert_eq!(wire_json["enableSessionTelemetry"], false);
6983 assert_eq!(wire_json["reasoningSummary"], "concise");
6984 assert_eq!(wire_json["remoteSession"], "export");
6985 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6986 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6987 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6988 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6989
6990 let (empty_wire, _) = SessionConfig::default()
6992 .into_wire(Some(SessionId::from("empty")))
6993 .expect("default has no duplicate handlers");
6994 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6995 assert!(empty_json.get("gitHubToken").is_none());
6996 assert!(empty_json.get("enableSessionTelemetry").is_none());
6997 assert!(empty_json.get("reasoningSummary").is_none());
6998 assert!(empty_json.get("remoteSession").is_none());
6999 assert!(
7000 empty_json
7001 .get("enableOnDemandInstructionDiscovery")
7002 .is_none()
7003 );
7004 assert!(empty_json.get("cloud").is_none());
7005 }
7006
7007 #[test]
7008 fn session_config_into_wire_serializes_named_providers_and_models() {
7009 let cfg = SessionConfig::default()
7010 .with_providers(vec![
7011 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
7012 .with_provider_type("openai")
7013 .with_wire_api("responses")
7014 .with_api_key("sk-test"),
7015 ])
7016 .with_models(vec![
7017 ProviderModelConfig::new("gpt-x", "my-openai")
7018 .with_wire_model("gpt-x-2025")
7019 .with_max_output_tokens(2048),
7020 ]);
7021
7022 let (wire, _) = cfg
7023 .into_wire(Some(SessionId::from("sess-providers")))
7024 .expect("no duplicate handlers");
7025 let wire_json = serde_json::to_value(&wire).unwrap();
7026 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
7027 assert_eq!(
7028 wire_json["providers"][0]["baseUrl"],
7029 "https://api.example.com/v1"
7030 );
7031 assert_eq!(wire_json["providers"][0]["type"], "openai");
7032 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
7033 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
7034 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
7035 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
7036 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
7037 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
7038
7039 let (empty_wire, _) = SessionConfig::default()
7040 .into_wire(Some(SessionId::from("empty")))
7041 .expect("default has no duplicate handlers");
7042 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7043 assert!(empty_json.get("providers").is_none());
7044 assert!(empty_json.get("models").is_none());
7045 }
7046
7047 #[test]
7048 fn resume_config_into_wire_serializes_named_providers_and_models() {
7049 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
7050 .with_providers(vec![
7051 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
7052 .with_provider_type("azure")
7053 .with_azure(AzureProviderOptions {
7054 api_version: Some("2024-10-21".to_string()),
7055 }),
7056 ])
7057 .with_models(vec![
7058 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
7059 ]);
7060
7061 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7062 let wire_json = serde_json::to_value(&wire).unwrap();
7063 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
7064 assert_eq!(wire_json["providers"][0]["type"], "azure");
7065 assert_eq!(
7066 wire_json["providers"][0]["azure"]["apiVersion"],
7067 "2024-10-21"
7068 );
7069 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
7070 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
7071 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
7072
7073 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
7074 .into_wire()
7075 .expect("default has no duplicate handlers");
7076 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7077 assert!(empty_json.get("providers").is_none());
7078 assert!(empty_json.get("models").is_none());
7079 }
7080
7081 #[test]
7082 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
7083 use std::path::PathBuf;
7084
7085 let cfg = SessionConfig {
7086 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
7087 disabled_mcp_servers: Some(vec![
7088 "local-files".to_string(),
7089 "remote-github".to_string(),
7090 ]),
7091 large_output: Some(
7092 LargeToolOutputConfig::new()
7093 .with_enabled(true)
7094 .with_max_size_bytes(1024)
7095 .with_output_directory(PathBuf::from("/tmp/large-output")),
7096 ),
7097 ..Default::default()
7098 };
7099
7100 let (wire, _) = cfg
7101 .into_wire(Some(SessionId::from("sess-1")))
7102 .expect("no duplicate handlers");
7103 let wire_json = serde_json::to_value(&wire).unwrap();
7104 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
7105 assert_eq!(
7106 wire_json["disabledMcpServers"],
7107 serde_json::json!(["local-files", "remote-github"])
7108 );
7109 assert_eq!(wire_json["largeOutput"]["enabled"], true);
7110 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
7111 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
7112
7113 let (empty_wire, _) = SessionConfig::default()
7114 .into_wire(Some(SessionId::from("empty")))
7115 .expect("default has no duplicate handlers");
7116 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7117 assert!(empty_json.get("pluginDirectories").is_none());
7118 assert!(empty_json.get("disabledMcpServers").is_none());
7119 assert!(empty_json.get("largeOutput").is_none());
7120 }
7121
7122 #[test]
7123 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
7124 use std::path::PathBuf;
7125
7126 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
7127 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
7128 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
7129 cfg.github_token = Some("ghs_secret".to_string());
7130 cfg.include_sub_agent_streaming_events = Some(true);
7131 cfg.enable_session_telemetry = Some(false);
7132 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
7133 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
7134 cfg.enable_on_demand_instruction_discovery = Some(false);
7135
7136 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7137 let wire_json = serde_json::to_value(&wire).unwrap();
7138 assert_eq!(wire_json["sessionId"], "sess-1");
7139 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
7140 assert_eq!(wire_json["configDir"], "/tmp/cfg");
7141 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
7142 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
7143 assert_eq!(wire_json["enableSessionTelemetry"], false);
7144 assert_eq!(wire_json["reasoningSummary"], "detailed");
7145 assert_eq!(wire_json["remoteSession"], "on");
7146 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
7147
7148 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7150 .into_wire()
7151 .expect("default resume has no duplicate handlers");
7152 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7153 assert!(empty_json.get("reasoningSummary").is_none());
7154 assert!(empty_json.get("remoteSession").is_none());
7155 assert!(
7156 empty_json
7157 .get("enableOnDemandInstructionDiscovery")
7158 .is_none()
7159 );
7160 }
7161
7162 #[test]
7163 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
7164 use std::path::PathBuf;
7165
7166 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
7167 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
7168 cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]);
7169 cfg.large_output = Some(
7170 LargeToolOutputConfig::new()
7171 .with_enabled(false)
7172 .with_max_size_bytes(2048)
7173 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
7174 );
7175
7176 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7177 let wire_json = serde_json::to_value(&wire).unwrap();
7178 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
7179 assert_eq!(
7180 wire_json["disabledMcpServers"],
7181 serde_json::json!(["local-files-r"])
7182 );
7183 assert_eq!(wire_json["largeOutput"]["enabled"], false);
7184 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
7185 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
7186
7187 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7188 .into_wire()
7189 .expect("default resume has no duplicate handlers");
7190 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7191 assert!(empty_json.get("pluginDirectories").is_none());
7192 assert!(empty_json.get("disabledMcpServers").is_none());
7193 assert!(empty_json.get("largeOutput").is_none());
7194 }
7195
7196 #[test]
7197 fn auth_client_id_metadata_url_reaches_create_and_resume_wire_payloads() {
7198 let url = "https://example.com/oauth/client-metadata.json";
7199
7200 let (create_wire, _) = SessionConfig::default()
7201 .with_auth_client_id_metadata_url(url)
7202 .into_wire(None)
7203 .expect("default create has no duplicate handlers");
7204 let create_json = serde_json::to_value(&create_wire).unwrap();
7205 assert_eq!(create_json["authClientIdMetadataUrl"], url);
7206
7207 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-1"))
7208 .with_auth_client_id_metadata_url(url)
7209 .into_wire()
7210 .expect("default resume has no duplicate handlers");
7211 let resume_json = serde_json::to_value(&resume_wire).unwrap();
7212 assert_eq!(resume_json["authClientIdMetadataUrl"], url);
7213
7214 let (empty_create_wire, _) = SessionConfig::default()
7215 .into_wire(None)
7216 .expect("default create has no duplicate handlers");
7217 let empty_create_json = serde_json::to_value(&empty_create_wire).unwrap();
7218 assert!(empty_create_json.get("authClientIdMetadataUrl").is_none());
7219
7220 let (empty_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7221 .into_wire()
7222 .expect("default resume has no duplicate handlers");
7223 let empty_resume_json = serde_json::to_value(&empty_resume_wire).unwrap();
7224 assert!(empty_resume_json.get("authClientIdMetadataUrl").is_none());
7225 }
7226
7227 #[test]
7228 fn session_config_clones_disabled_mcp_servers() {
7229 let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]);
7230 let mut create_clone = create.clone();
7231 create_clone
7232 .disabled_mcp_servers
7233 .as_mut()
7234 .expect("configured disabled MCP servers")
7235 .push("remote-github".to_string());
7236 assert_eq!(
7237 create.disabled_mcp_servers.as_deref(),
7238 Some(&["local-files".to_string()][..])
7239 );
7240
7241 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
7242 .with_disabled_mcp_servers(["local-files"]);
7243 let mut resume_clone = resume.clone();
7244 resume_clone
7245 .disabled_mcp_servers
7246 .as_mut()
7247 .expect("configured disabled MCP servers")
7248 .push("remote-github".to_string());
7249 assert_eq!(
7250 resume.disabled_mcp_servers.as_deref(),
7251 Some(&["local-files".to_string()][..])
7252 );
7253 }
7254
7255 #[test]
7256 fn session_config_builder_composes() {
7257 use indexmap::IndexMap;
7258
7259 let cfg = SessionConfig::default()
7260 .with_session_id(SessionId::from("sess-1"))
7261 .with_model("claude-sonnet-4")
7262 .with_client_name("test-app")
7263 .with_reasoning_effort("medium")
7264 .with_reasoning_summary(ReasoningSummary::Concise)
7265 .with_context_tier("long_context")
7266 .with_streaming(true)
7267 .with_tools([Tool::new("greet")])
7268 .with_available_tools(["bash", "view"])
7269 .with_excluded_tools(["dangerous"])
7270 .with_mcp_servers(IndexMap::new())
7271 .with_mcp_oauth_token_storage("persistent")
7272 .with_enable_config_discovery(true)
7273 .with_enable_on_demand_instruction_discovery(true)
7274 .with_skill_directories([PathBuf::from("/tmp/skills")])
7275 .with_disabled_skills(["broken-skill"])
7276 .with_disabled_mcp_servers(["local-files"])
7277 .with_agent("researcher")
7278 .with_config_directory(PathBuf::from("/tmp/config"))
7279 .with_working_directory(PathBuf::from("/tmp/work"))
7280 .with_additional_directories([PathBuf::from("/tmp/shared")])
7281 .with_github_token("ghp_test")
7282 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7283 .with_enable_session_telemetry(false)
7284 .with_include_sub_agent_streaming_events(false)
7285 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
7286
7287 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
7288 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
7289 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
7290 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
7291 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
7292 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
7293 assert_eq!(cfg.streaming, Some(true));
7294 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
7295 assert_eq!(
7296 cfg.available_tools.as_deref(),
7297 Some(&["bash".to_string(), "view".to_string()][..])
7298 );
7299 assert_eq!(
7300 cfg.excluded_tools.as_deref(),
7301 Some(&["dangerous".to_string()][..])
7302 );
7303 assert!(cfg.mcp_servers.is_some());
7304 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
7305 assert_eq!(cfg.enable_config_discovery, Some(true));
7306 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
7307 assert_eq!(
7308 cfg.skill_directories.as_deref(),
7309 Some(&[PathBuf::from("/tmp/skills")][..])
7310 );
7311 assert_eq!(
7312 cfg.disabled_skills.as_deref(),
7313 Some(&["broken-skill".to_string()][..])
7314 );
7315 assert_eq!(
7316 cfg.disabled_mcp_servers.as_deref(),
7317 Some(&["local-files".to_string()][..])
7318 );
7319 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
7320 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
7321 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
7322 assert_eq!(
7323 cfg.additional_directories.as_deref(),
7324 Some(&[PathBuf::from("/tmp/shared")][..])
7325 );
7326 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
7327 assert_eq!(
7328 cfg.capi,
7329 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7330 );
7331 assert_eq!(cfg.enable_session_telemetry, Some(false));
7332 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
7333 assert_eq!(
7334 cfg.extension_info,
7335 Some(ExtensionInfo::new("github-app", "counter"))
7336 );
7337 }
7338
7339 #[test]
7340 fn resume_session_config_builder_composes() {
7341 use indexmap::IndexMap;
7342
7343 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
7344 .with_client_name("test-app")
7345 .with_reasoning_summary(ReasoningSummary::None)
7346 .with_context_tier("default")
7347 .with_streaming(true)
7348 .with_tools([Tool::new("greet")])
7349 .with_available_tools(["bash", "view"])
7350 .with_excluded_tools(["dangerous"])
7351 .with_mcp_servers(IndexMap::new())
7352 .with_mcp_oauth_token_storage("persistent")
7353 .with_enable_config_discovery(true)
7354 .with_enable_on_demand_instruction_discovery(false)
7355 .with_skill_directories([PathBuf::from("/tmp/skills")])
7356 .with_disabled_skills(["broken-skill"])
7357 .with_disabled_mcp_servers(["local-files"])
7358 .with_agent("researcher")
7359 .with_config_directory(PathBuf::from("/tmp/config"))
7360 .with_working_directory(PathBuf::from("/tmp/work"))
7361 .with_additional_directories([PathBuf::from("/tmp/shared")])
7362 .with_github_token("ghp_test")
7363 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7364 .with_enable_session_telemetry(false)
7365 .with_include_sub_agent_streaming_events(true)
7366 .with_suppress_resume_event(true)
7367 .with_continue_pending_work(true)
7368 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
7369
7370 assert_eq!(cfg.session_id.as_str(), "sess-2");
7371 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
7372 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
7373 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
7374 assert_eq!(cfg.streaming, Some(true));
7375 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
7376 assert_eq!(
7377 cfg.available_tools.as_deref(),
7378 Some(&["bash".to_string(), "view".to_string()][..])
7379 );
7380 assert_eq!(
7381 cfg.excluded_tools.as_deref(),
7382 Some(&["dangerous".to_string()][..])
7383 );
7384 assert!(cfg.mcp_servers.is_some());
7385 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
7386 assert_eq!(cfg.enable_config_discovery, Some(true));
7387 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
7388 assert_eq!(
7389 cfg.skill_directories.as_deref(),
7390 Some(&[PathBuf::from("/tmp/skills")][..])
7391 );
7392 assert_eq!(
7393 cfg.disabled_skills.as_deref(),
7394 Some(&["broken-skill".to_string()][..])
7395 );
7396 assert_eq!(
7397 cfg.disabled_mcp_servers.as_deref(),
7398 Some(&["local-files".to_string()][..])
7399 );
7400 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
7401 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
7402 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
7403 assert_eq!(
7404 cfg.additional_directories.as_deref(),
7405 Some(&[PathBuf::from("/tmp/shared")][..])
7406 );
7407 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
7408 assert_eq!(
7409 cfg.capi,
7410 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7411 );
7412 assert_eq!(cfg.enable_session_telemetry, Some(false));
7413 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
7414 assert_eq!(cfg.suppress_resume_event, Some(true));
7415 assert_eq!(cfg.continue_pending_work, Some(true));
7416 assert_eq!(
7417 cfg.extension_info,
7418 Some(ExtensionInfo::new("github-app", "counter"))
7419 );
7420 }
7421
7422 #[test]
7426 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
7427 let cfg =
7428 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
7429 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7430 let json = serde_json::to_value(&wire).unwrap();
7431 assert_eq!(json["continuePendingWork"], true);
7432
7433 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7435 .into_wire()
7436 .expect("no duplicate handlers");
7437 let json = serde_json::to_value(&wire).unwrap();
7438 assert!(json.get("continuePendingWork").is_none());
7439 }
7440
7441 #[test]
7442 fn session_configs_serialize_additional_directories() {
7443 let create = SessionConfig::default().with_additional_directories([
7444 PathBuf::from("/tmp/shared"),
7445 PathBuf::from("/tmp/generated"),
7446 ]);
7447 let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
7448 let create_json = serde_json::to_value(&create_wire).unwrap();
7449 assert_eq!(
7450 create_json["additionalDirectories"],
7451 serde_json::json!(["/tmp/shared", "/tmp/generated"])
7452 );
7453
7454 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
7455 .with_additional_directories([PathBuf::from("/tmp/resumed")]);
7456 let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
7457 let resume_json = serde_json::to_value(&resume_wire).unwrap();
7458 assert_eq!(
7459 resume_json["additionalDirectories"],
7460 serde_json::json!(["/tmp/resumed"])
7461 );
7462 }
7463
7464 #[test]
7468 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
7469 let cfg =
7470 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
7471 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7472 let json = serde_json::to_value(&wire).unwrap();
7473 assert_eq!(json["disableResume"], true);
7474 assert!(json.get("suppressResumeEvent").is_none());
7475 }
7476
7477 #[test]
7480 fn session_config_serializes_instruction_directories_to_camel_case() {
7481 let cfg =
7482 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
7483 let (wire, _) = cfg
7484 .into_wire(Some(SessionId::from("instr-on")))
7485 .expect("no duplicate handlers");
7486 let json = serde_json::to_value(&wire).unwrap();
7487 assert_eq!(
7488 json["instructionDirectories"],
7489 serde_json::json!(["/tmp/instr"])
7490 );
7491
7492 let (wire, _) = SessionConfig::default()
7494 .into_wire(Some(SessionId::from("instr-off")))
7495 .expect("no duplicate handlers");
7496 let json = serde_json::to_value(&wire).unwrap();
7497 assert!(json.get("instructionDirectories").is_none());
7498 }
7499
7500 #[test]
7503 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
7504 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
7505 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
7506 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7507 let json = serde_json::to_value(&wire).unwrap();
7508 assert_eq!(
7509 json["instructionDirectories"],
7510 serde_json::json!(["/tmp/instr"])
7511 );
7512
7513 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7514 .into_wire()
7515 .expect("no duplicate handlers");
7516 let json = serde_json::to_value(&wire).unwrap();
7517 assert!(json.get("instructionDirectories").is_none());
7518 }
7519
7520 #[test]
7521 fn custom_agent_config_builder_composes() {
7522 use indexmap::IndexMap;
7523
7524 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
7525 .with_display_name("Research Assistant")
7526 .with_description("Investigates technical questions.")
7527 .with_tools(["bash", "view"])
7528 .with_mcp_servers(IndexMap::new())
7529 .with_infer(true)
7530 .with_skills(["rust-coding-skill"]);
7531
7532 assert_eq!(cfg.name, "researcher");
7533 assert_eq!(cfg.prompt, "You are a research assistant.");
7534 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
7535 assert_eq!(
7536 cfg.description.as_deref(),
7537 Some("Investigates technical questions.")
7538 );
7539 assert_eq!(
7540 cfg.tools.as_deref(),
7541 Some(&["bash".to_string(), "view".to_string()][..])
7542 );
7543 assert!(cfg.mcp_servers.is_some());
7544 assert_eq!(cfg.infer, Some(true));
7545 assert_eq!(
7546 cfg.skills.as_deref(),
7547 Some(&["rust-coding-skill".to_string()][..])
7548 );
7549 }
7550
7551 #[test]
7552 fn mcp_servers_serialize_in_insertion_order() {
7553 use indexmap::IndexMap;
7554
7555 let order = [
7561 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
7562 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
7563 ];
7564 let mut servers = IndexMap::new();
7565 for name in order {
7566 servers.insert(
7567 name.to_string(),
7568 McpServerConfig::Stdio(McpStdioServerConfig {
7569 command: "run".to_string(),
7570 ..Default::default()
7571 }),
7572 );
7573 }
7574
7575 let (wire, _runtime) = SessionConfig::default()
7576 .with_mcp_servers(servers)
7577 .into_wire(None)
7578 .expect("into_wire should succeed");
7579 let json = serde_json::to_string(&wire).expect("serialize wire");
7580
7581 let positions: Vec<usize> = order
7582 .iter()
7583 .map(|name| {
7584 json.find(&format!("\"{name}\""))
7585 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
7586 })
7587 .collect();
7588 let mut ascending = positions.clone();
7589 ascending.sort_unstable();
7590 assert_eq!(
7591 positions, ascending,
7592 "mcp server keys must serialize in insertion order: {json}"
7593 );
7594 }
7595
7596 #[test]
7597 fn infinite_session_config_builder_composes() {
7598 let cfg = InfiniteSessionConfig::new()
7599 .with_enabled(true)
7600 .with_background_compaction_threshold(0.75)
7601 .with_buffer_exhaustion_threshold(0.92);
7602
7603 assert_eq!(cfg.enabled, Some(true));
7604 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
7605 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
7606 }
7607
7608 #[test]
7609 fn provider_config_builder_composes() {
7610 use std::collections::HashMap;
7611
7612 let mut headers = HashMap::new();
7613 headers.insert("X-Custom".to_string(), "value".to_string());
7614
7615 let cfg = ProviderConfig::new("https://api.example.com")
7616 .with_provider_type("openai")
7617 .with_wire_api("completions")
7618 .with_transport("websockets")
7619 .with_api_key("sk-test")
7620 .with_bearer_token("bearer-test")
7621 .with_headers(headers)
7622 .with_model_id("gpt-4")
7623 .with_wire_model("azure-gpt-4-deployment")
7624 .with_max_prompt_tokens(8192)
7625 .with_max_output_tokens(2048);
7626
7627 assert_eq!(cfg.base_url, "https://api.example.com");
7628 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
7629 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
7630 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
7631 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
7632 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
7633 assert_eq!(
7634 cfg.headers
7635 .as_ref()
7636 .and_then(|h| h.get("X-Custom"))
7637 .map(String::as_str),
7638 Some("value"),
7639 );
7640 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
7641 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
7642 assert_eq!(cfg.max_prompt_tokens, Some(8192));
7643 assert_eq!(cfg.max_output_tokens, Some(2048));
7644
7645 let wire = serde_json::to_value(&cfg).unwrap();
7647 assert_eq!(wire["modelId"], "gpt-4");
7648 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
7649 assert_eq!(wire["maxPromptTokens"], 8192);
7650 assert_eq!(wire["maxOutputTokens"], 2048);
7651
7652 let unset = ProviderConfig::new("https://api.example.com");
7653 let wire_unset = serde_json::to_value(&unset).unwrap();
7654 assert!(wire_unset.get("modelId").is_none());
7655 assert!(wire_unset.get("wireModel").is_none());
7656 assert!(wire_unset.get("maxPromptTokens").is_none());
7657 assert!(wire_unset.get("maxOutputTokens").is_none());
7658 }
7659
7660 #[test]
7661 fn capi_session_options_builder_composes_and_serializes() {
7662 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
7663
7664 assert_eq!(cfg.enable_web_socket_responses, Some(false));
7665
7666 let wire = serde_json::to_value(&cfg).unwrap();
7667 assert_eq!(
7668 wire,
7669 serde_json::json!({ "enableWebSocketResponses": false })
7670 );
7671
7672 let unset = CapiSessionOptions::new();
7673 let wire_unset = serde_json::to_value(&unset).unwrap();
7674 assert!(wire_unset.get("enableWebSocketResponses").is_none());
7675 assert!(wire_unset.get("autoTier").is_none());
7676 assert_eq!(wire_unset, json!({}));
7677 }
7678
7679 #[test]
7680 fn capi_auto_tier_canonical_values_round_trip_and_forward() {
7681 for (tier, value) in [
7682 (AutoTier::Efficiency, "efficiency"),
7683 (AutoTier::Balance, "balance"),
7684 (AutoTier::Intelligence, "intelligence"),
7685 (AutoTier::Fast, "fast"),
7686 ] {
7687 let exported: crate::AutoTier = tier.clone();
7688 let capi = CapiSessionOptions::new().with_auto_tier(exported);
7689 assert_eq!(capi.auto_tier, Some(tier));
7690 assert_eq!(
7691 serde_json::to_value(&capi).unwrap(),
7692 json!({"autoTier": value})
7693 );
7694 assert_eq!(
7695 serde_json::from_value::<CapiSessionOptions>(json!({"autoTier": value})).unwrap(),
7696 capi
7697 );
7698
7699 let capi = capi.with_enable_web_socket_responses(false);
7700 let expected = json!({"autoTier": value, "enableWebSocketResponses": false});
7701 let (create, _) = SessionConfig::default()
7702 .with_model("auto")
7703 .with_capi(capi.clone())
7704 .into_wire(Some(SessionId::from("capi-create")))
7705 .unwrap();
7706 assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected);
7707
7708 let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7709 .with_capi(capi)
7710 .into_wire()
7711 .unwrap();
7712 assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected);
7713 }
7714 }
7715
7716 #[test]
7717 fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() {
7718 for value in ["balanced", "Balance", "unknown"] {
7719 assert_eq!(
7720 serde_json::from_value::<AutoTier>(json!(value)).unwrap(),
7721 AutoTier::Unknown
7722 );
7723 }
7724 let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap();
7725 assert_eq!(capi.auto_tier, None);
7726 }
7727
7728 #[test]
7729 fn session_config_with_capi_serializes() {
7730 let (wire, _) = SessionConfig::default()
7731 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7732 .into_wire(Some(SessionId::from("capi-create")))
7733 .expect("no duplicate handlers");
7734 let json = serde_json::to_value(&wire).unwrap();
7735 assert_eq!(
7736 json["capi"],
7737 serde_json::json!({ "enableWebSocketResponses": false })
7738 );
7739
7740 let (empty_wire, _) = SessionConfig::default()
7741 .into_wire(Some(SessionId::from("capi-create-unset")))
7742 .expect("no duplicate handlers");
7743 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7744 assert!(empty_json.get("capi").is_none());
7745 }
7746
7747 #[test]
7748 fn resume_session_config_with_capi_serializes() {
7749 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7750 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7751 .into_wire()
7752 .expect("no duplicate handlers");
7753 let json = serde_json::to_value(&wire).unwrap();
7754 assert_eq!(
7755 json["capi"],
7756 serde_json::json!({ "enableWebSocketResponses": false })
7757 );
7758
7759 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
7760 .into_wire()
7761 .expect("no duplicate handlers");
7762 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7763 assert!(empty_json.get("capi").is_none());
7764 }
7765
7766 #[test]
7767 fn system_message_config_builder_composes() {
7768 use std::collections::HashMap;
7769
7770 let cfg = SystemMessageConfig::new()
7771 .with_mode("replace")
7772 .with_content("Custom system message.")
7773 .with_sections(HashMap::new());
7774
7775 assert_eq!(cfg.mode.as_deref(), Some("replace"));
7776 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
7777 assert!(cfg.sections.is_some());
7778 }
7779
7780 #[test]
7781 fn delivery_mode_serializes_to_kebab_case_strings() {
7782 assert_eq!(
7783 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
7784 "\"enqueue\""
7785 );
7786 assert_eq!(
7787 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
7788 "\"immediate\""
7789 );
7790 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
7791 assert_eq!(parsed, DeliveryMode::Immediate);
7792 }
7793
7794 #[test]
7795 fn agent_mode_serializes_to_kebab_case_strings() {
7796 assert_eq!(
7797 serde_json::to_string(&AgentMode::Interactive).unwrap(),
7798 "\"interactive\""
7799 );
7800 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
7801 assert_eq!(
7802 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
7803 "\"autopilot\""
7804 );
7805 assert_eq!(
7806 serde_json::to_string(&AgentMode::Shell).unwrap(),
7807 "\"shell\""
7808 );
7809 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
7810 assert_eq!(parsed, AgentMode::Plan);
7811 }
7812
7813 #[test]
7814 fn connection_state_distinguishes_variants() {
7815 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
7818 }
7819
7820 #[test]
7826 fn session_event_round_trips_agent_id_on_envelope() {
7827 let wire = json!({
7828 "id": "evt-1",
7829 "timestamp": "2026-04-30T12:00:00Z",
7830 "parentId": null,
7831 "agentId": "sub-agent-42",
7832 "type": "assistant.message",
7833 "data": { "message": "hi" }
7834 });
7835
7836 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
7837 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7838
7839 let roundtripped = serde_json::to_value(&event).unwrap();
7841 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7842
7843 let main_agent_event: SessionEvent = serde_json::from_value(json!({
7845 "id": "evt-2",
7846 "timestamp": "2026-04-30T12:00:01Z",
7847 "parentId": null,
7848 "type": "session.idle",
7849 "data": {}
7850 }))
7851 .unwrap();
7852 assert!(main_agent_event.agent_id.is_none());
7853 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
7854 assert!(roundtripped.get("agentId").is_none());
7855 }
7856
7857 #[test]
7859 fn typed_session_event_round_trips_agent_id_on_envelope() {
7860 let wire = json!({
7861 "id": "evt-1",
7862 "timestamp": "2026-04-30T12:00:00Z",
7863 "parentId": null,
7864 "agentId": "sub-agent-42",
7865 "type": "session.idle",
7866 "data": {}
7867 });
7868
7869 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
7870 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7871
7872 let roundtripped = serde_json::to_value(&event).unwrap();
7873 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7874 }
7875
7876 #[test]
7877 fn connection_state_variants_compile() {
7878 let _ = ConnectionState::Disconnected;
7882 let _ = ConnectionState::Connecting;
7883 let _ = ConnectionState::Connected;
7884 let _ = ConnectionState::Error;
7885 }
7886
7887 #[test]
7888 fn deserializes_runtime_attachment_variants() {
7889 let attachments: Vec<Attachment> = serde_json::from_value(json!([
7890 {
7891 "type": "file",
7892 "path": "/tmp/file.rs",
7893 "displayName": "file.rs",
7894 "lineRange": { "start": 7, "end": 12 }
7895 },
7896 {
7897 "type": "directory",
7898 "path": "/tmp/project",
7899 "displayName": "project"
7900 },
7901 {
7902 "type": "selection",
7903 "filePath": "/tmp/lib.rs",
7904 "displayName": "lib.rs",
7905 "text": "fn main() {}",
7906 "selection": {
7907 "start": { "line": 1, "character": 2 },
7908 "end": { "line": 3, "character": 4 }
7909 }
7910 },
7911 {
7912 "type": "blob",
7913 "data": "Zm9v",
7914 "mimeType": "image/png",
7915 "displayName": "image.png"
7916 },
7917 {
7918 "type": "github_reference",
7919 "number": 42,
7920 "title": "Fix rendering",
7921 "referenceType": "issue",
7922 "state": "open",
7923 "url": "https://github.com/example/repo/issues/42"
7924 },
7925 {
7926 "type": "extension_context",
7927 "capturedAt": "2026-09-18T11:00:00Z",
7928 "extensionId": "example:extension",
7929 "title": "Unbound context"
7930 }
7931 ]))
7932 .expect("attachments should deserialize");
7933
7934 assert_eq!(attachments.len(), 6);
7935 assert!(matches!(
7936 &attachments[0],
7937 Attachment::File {
7938 path,
7939 display_name,
7940 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7941 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7942 ));
7943 assert!(matches!(
7944 &attachments[1],
7945 Attachment::Directory { path, display_name }
7946 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7947 ));
7948 assert!(matches!(
7949 &attachments[2],
7950 Attachment::Selection {
7951 file_path,
7952 display_name,
7953 selection:
7954 AttachmentSelectionRange {
7955 start: AttachmentSelectionPosition { line: 1, character: 2 },
7956 end: AttachmentSelectionPosition { line: 3, character: 4 },
7957 },
7958 ..
7959 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
7960 ));
7961 assert!(matches!(
7962 &attachments[3],
7963 Attachment::Blob {
7964 data,
7965 mime_type,
7966 display_name,
7967 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
7968 ));
7969 assert!(matches!(
7970 &attachments[4],
7971 Attachment::GitHubReference {
7972 number: 42,
7973 title,
7974 reference_type: GitHubReferenceType::Issue,
7975 state,
7976 url,
7977 } if title == "Fix rendering"
7978 && state == "open"
7979 && url == "https://github.com/example/repo/issues/42"
7980 ));
7981 assert!(matches!(
7982 &attachments[5],
7983 Attachment::ExtensionContext {
7984 captured_at,
7985 extension_id,
7986 canvas_id: None,
7987 instance_id: None,
7988 title,
7989 payload: None,
7990 } if captured_at == "2026-09-18T11:00:00Z"
7991 && extension_id == "example:extension"
7992 && title == "Unbound context"
7993 ));
7994 assert_eq!(
7995 serde_json::to_value(&attachments[5]).expect("serialize extension context"),
7996 json!({
7997 "type": "extension_context",
7998 "capturedAt": "2026-09-18T11:00:00Z",
7999 "extensionId": "example:extension",
8000 "title": "Unbound context"
8001 })
8002 );
8003 }
8004
8005 #[test]
8006 fn ensures_display_names_for_variants_that_support_them() {
8007 let mut attachments = vec![
8008 Attachment::File {
8009 path: PathBuf::from("/tmp/file.rs"),
8010 display_name: None,
8011 line_range: None,
8012 },
8013 Attachment::Selection {
8014 file_path: PathBuf::from("/tmp/src/lib.rs"),
8015 display_name: None,
8016 text: "fn main() {}".to_string(),
8017 selection: AttachmentSelectionRange {
8018 start: AttachmentSelectionPosition {
8019 line: 0,
8020 character: 0,
8021 },
8022 end: AttachmentSelectionPosition {
8023 line: 0,
8024 character: 10,
8025 },
8026 },
8027 },
8028 Attachment::Blob {
8029 data: "Zm9v".to_string(),
8030 mime_type: "image/png".to_string(),
8031 display_name: None,
8032 },
8033 Attachment::GitHubReference {
8034 number: 7,
8035 title: "Track regressions".to_string(),
8036 reference_type: GitHubReferenceType::Issue,
8037 state: "open".to_string(),
8038 url: "https://example.com/issues/7".to_string(),
8039 },
8040 ];
8041
8042 ensure_attachment_display_names(&mut attachments);
8043
8044 assert_eq!(attachments[0].display_name(), Some("file.rs"));
8045 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
8046 assert_eq!(attachments[2].display_name(), Some("attachment"));
8047 assert_eq!(attachments[3].display_name(), None);
8048 assert_eq!(
8049 attachments[3].label(),
8050 Some("Track regressions".to_string())
8051 );
8052 }
8053
8054 #[test]
8055 fn github_anchored_attachment_variants_round_trip() {
8056 let cases = vec![
8057 (
8058 "github_commit",
8059 json!({
8060 "type": "github_commit",
8061 "message": "Fix the thing",
8062 "oid": "abc123",
8063 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
8064 "url": "https://github.com/octocat/repo/commit/abc123"
8065 }),
8066 ),
8067 (
8068 "github_release",
8069 json!({
8070 "type": "github_release",
8071 "name": "v1.2.3",
8072 "repo": { "name": "repo", "owner": "octocat" },
8073 "tagName": "v1.2.3",
8074 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
8075 }),
8076 ),
8077 (
8078 "github_actions_job",
8079 json!({
8080 "type": "github_actions_job",
8081 "conclusion": "failure",
8082 "jobId": 99,
8083 "jobName": "build",
8084 "repo": { "name": "repo", "owner": "octocat" },
8085 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
8086 "workflowName": "CI"
8087 }),
8088 ),
8089 (
8090 "github_repository",
8091 json!({
8092 "type": "github_repository",
8093 "description": "An example repository",
8094 "ref": "main",
8095 "repo": { "name": "repo", "owner": "octocat" },
8096 "url": "https://github.com/octocat/repo"
8097 }),
8098 ),
8099 (
8100 "github_file_diff",
8101 json!({
8102 "type": "github_file_diff",
8103 "base": {
8104 "path": "src/lib.rs",
8105 "ref": "main",
8106 "repo": { "name": "repo", "owner": "octocat" }
8107 },
8108 "head": {
8109 "path": "src/lib.rs",
8110 "ref": "feature",
8111 "repo": { "name": "repo", "owner": "octocat" }
8112 },
8113 "url": "https://github.com/octocat/repo/compare/main...feature"
8114 }),
8115 ),
8116 (
8117 "github_tree_comparison",
8118 json!({
8119 "type": "github_tree_comparison",
8120 "base": {
8121 "repo": { "name": "repo", "owner": "octocat" },
8122 "revision": "main"
8123 },
8124 "head": {
8125 "repo": { "name": "repo", "owner": "octocat" },
8126 "revision": "feature"
8127 },
8128 "url": "https://github.com/octocat/repo/compare/main...feature"
8129 }),
8130 ),
8131 (
8132 "github_url",
8133 json!({
8134 "type": "github_url",
8135 "url": "https://github.com/octocat/repo/wiki"
8136 }),
8137 ),
8138 (
8139 "github_file",
8140 json!({
8141 "type": "github_file",
8142 "path": "src/main.rs",
8143 "ref": "main",
8144 "repo": { "name": "repo", "owner": "octocat" },
8145 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
8146 }),
8147 ),
8148 (
8149 "github_snippet",
8150 json!({
8151 "type": "github_snippet",
8152 "lineRange": { "start": 10, "end": 20 },
8153 "path": "src/main.rs",
8154 "ref": "main",
8155 "repo": { "name": "repo", "owner": "octocat" },
8156 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
8157 }),
8158 ),
8159 ];
8160
8161 for (expected_type, input) in cases {
8162 let attachment: Attachment = serde_json::from_value(input.clone())
8163 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
8164
8165 let serialized_string = serde_json::to_string(&attachment)
8170 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
8171
8172 assert_eq!(
8174 serialized_string.matches("\"type\":").count(),
8175 1,
8176 "{expected_type} must serialize a single `type` key"
8177 );
8178
8179 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
8180 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
8181 assert_eq!(
8182 serialized.get("type").and_then(|value| value.as_str()),
8183 Some(expected_type),
8184 "{expected_type} must serialize the correct discriminator"
8185 );
8186
8187 assert_eq!(
8189 serialized, input,
8190 "{expected_type} should round-trip without data loss"
8191 );
8192 let reparsed: Attachment = serde_json::from_value(serialized)
8193 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
8194 assert_eq!(
8195 reparsed, attachment,
8196 "{expected_type} should re-deserialize to the same value"
8197 );
8198 }
8199 }
8200}
8201
8202#[cfg(test)]
8203mod permission_builder_tests {
8204 use std::sync::Arc;
8205
8206 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
8207 use crate::permission;
8208 use crate::types::{
8209 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
8210 SessionId,
8211 };
8212
8213 fn data() -> PermissionRequestData {
8214 PermissionRequestData {
8215 extra: serde_json::json!({"tool": "shell"}),
8216 ..Default::default()
8217 }
8218 }
8219
8220 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
8223 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
8224 }
8225
8226 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
8227 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
8228 }
8229
8230 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
8231 handler
8232 .handle(SessionId::from("s1"), RequestId::new("1"), data())
8233 .await
8234 }
8235
8236 #[tokio::test]
8237 async fn approve_all_with_handler_present_approves() {
8238 let cfg = SessionConfig::default()
8239 .with_permission_handler(Arc::new(ApproveAllHandler))
8240 .approve_all_permissions();
8241 let h = resolve_create(cfg).expect("policy + handler yields handler");
8242 assert!(matches!(
8243 dispatch(&h).await,
8244 PermissionResult::Decision {
8245 decision: PermissionDecision::ApproveOnce(_),
8246 ..
8247 }
8248 ));
8249 }
8250
8251 #[tokio::test]
8252 async fn approve_all_standalone_produces_handler() {
8253 let cfg = SessionConfig::default().approve_all_permissions();
8254 let h = resolve_create(cfg).expect("policy alone yields handler");
8255 assert!(matches!(
8256 dispatch(&h).await,
8257 PermissionResult::Decision {
8258 decision: PermissionDecision::ApproveOnce(_),
8259 ..
8260 }
8261 ));
8262 }
8263
8264 #[tokio::test]
8267 async fn approve_all_is_order_independent() {
8268 let a = SessionConfig::default()
8269 .with_permission_handler(Arc::new(ApproveAllHandler))
8270 .approve_all_permissions();
8271 let b = SessionConfig::default()
8272 .approve_all_permissions()
8273 .with_permission_handler(Arc::new(ApproveAllHandler));
8274 let ha = resolve_create(a).unwrap();
8275 let hb = resolve_create(b).unwrap();
8276 assert!(matches!(
8277 dispatch(&ha).await,
8278 PermissionResult::Decision {
8279 decision: PermissionDecision::ApproveOnce(_),
8280 ..
8281 }
8282 ));
8283 assert!(matches!(
8284 dispatch(&hb).await,
8285 PermissionResult::Decision {
8286 decision: PermissionDecision::ApproveOnce(_),
8287 ..
8288 }
8289 ));
8290 }
8291
8292 #[tokio::test]
8293 async fn deny_all_is_order_independent() {
8294 let a = SessionConfig::default()
8295 .with_permission_handler(Arc::new(ApproveAllHandler))
8296 .deny_all_permissions();
8297 let b = SessionConfig::default()
8298 .deny_all_permissions()
8299 .with_permission_handler(Arc::new(ApproveAllHandler));
8300 let ha = resolve_create(a).unwrap();
8301 let hb = resolve_create(b).unwrap();
8302 assert!(matches!(
8303 dispatch(&ha).await,
8304 PermissionResult::Decision {
8305 decision: PermissionDecision::Reject(_),
8306 ..
8307 }
8308 ));
8309 assert!(matches!(
8310 dispatch(&hb).await,
8311 PermissionResult::Decision {
8312 decision: PermissionDecision::Reject(_),
8313 ..
8314 }
8315 ));
8316 }
8317
8318 #[tokio::test]
8319 async fn approve_permissions_if_consults_predicate() {
8320 let cfg = SessionConfig::default().approve_permissions_if(|d| {
8321 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
8322 });
8323 let h = resolve_create(cfg).unwrap();
8324 assert!(matches!(
8325 dispatch(&h).await,
8326 PermissionResult::Decision {
8327 decision: PermissionDecision::Reject(_),
8328 ..
8329 }
8330 ));
8331 }
8332
8333 #[tokio::test]
8334 async fn approve_permissions_if_is_order_independent() {
8335 let predicate = |d: &PermissionRequestData| {
8336 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
8337 };
8338 let a = SessionConfig::default()
8339 .with_permission_handler(Arc::new(ApproveAllHandler))
8340 .approve_permissions_if(predicate);
8341 let b = SessionConfig::default()
8342 .approve_permissions_if(predicate)
8343 .with_permission_handler(Arc::new(ApproveAllHandler));
8344 let ha = resolve_create(a).unwrap();
8345 let hb = resolve_create(b).unwrap();
8346 assert!(matches!(
8347 dispatch(&ha).await,
8348 PermissionResult::Decision {
8349 decision: PermissionDecision::Reject(_),
8350 ..
8351 }
8352 ));
8353 assert!(matches!(
8354 dispatch(&hb).await,
8355 PermissionResult::Decision {
8356 decision: PermissionDecision::Reject(_),
8357 ..
8358 }
8359 ));
8360 }
8361
8362 #[tokio::test]
8363 async fn resume_session_config_approve_all_works() {
8364 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
8365 .with_permission_handler(Arc::new(ApproveAllHandler))
8366 .approve_all_permissions();
8367 let h = resolve_resume(cfg).unwrap();
8368 assert!(matches!(
8369 dispatch(&h).await,
8370 PermissionResult::Decision {
8371 decision: PermissionDecision::ApproveOnce(_),
8372 ..
8373 }
8374 ));
8375 }
8376
8377 #[tokio::test]
8378 async fn resume_session_config_approve_all_is_order_independent() {
8379 let a = ResumeSessionConfig::new(SessionId::from("s1"))
8380 .with_permission_handler(Arc::new(ApproveAllHandler))
8381 .approve_all_permissions();
8382 let b = ResumeSessionConfig::new(SessionId::from("s1"))
8383 .approve_all_permissions()
8384 .with_permission_handler(Arc::new(ApproveAllHandler));
8385 let ha = resolve_resume(a).unwrap();
8386 let hb = resolve_resume(b).unwrap();
8387 assert!(matches!(
8388 dispatch(&ha).await,
8389 PermissionResult::Decision {
8390 decision: PermissionDecision::ApproveOnce(_),
8391 ..
8392 }
8393 ));
8394 assert!(matches!(
8395 dispatch(&hb).await,
8396 PermissionResult::Decision {
8397 decision: PermissionDecision::ApproveOnce(_),
8398 ..
8399 }
8400 ));
8401 }
8402
8403 #[test]
8404 fn session_config_enable_experimental_mode_serializes_when_set() {
8405 let cfg = SessionConfig::default().with_enable_experimental_mode(false);
8406 assert_eq!(cfg.enable_experimental_mode, Some(false));
8407
8408 let (wire, _runtime) = cfg
8409 .into_wire(Some(SessionId::from("experimental-mode")))
8410 .expect("enable_experimental_mode config has no duplicate handlers");
8411 assert_eq!(wire.is_experimental_mode, Some(false));
8412
8413 let json = serde_json::to_value(&wire).unwrap();
8414 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
8415 }
8416
8417 #[test]
8418 fn session_config_enable_experimental_mode_omitted_when_none() {
8419 let cfg = SessionConfig::default();
8420 assert_eq!(cfg.enable_experimental_mode, None);
8421
8422 let (wire, _runtime) = cfg
8423 .into_wire(Some(SessionId::from("no-experimental-mode")))
8424 .expect("default config has no duplicate handlers");
8425 assert_eq!(wire.is_experimental_mode, None);
8426
8427 let json = serde_json::to_value(&wire).unwrap();
8428 assert!(json.get("isExperimentalMode").is_none());
8429 }
8430
8431 #[test]
8432 fn resume_session_config_enable_experimental_mode_serializes_when_set() {
8433 let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
8434 .with_enable_experimental_mode(false);
8435 assert_eq!(cfg.enable_experimental_mode, Some(false));
8436
8437 let (wire, _runtime) = cfg
8438 .into_wire()
8439 .expect("resume enable_experimental_mode config has no duplicate handlers");
8440 assert_eq!(wire.is_experimental_mode, Some(false));
8441
8442 let json = serde_json::to_value(&wire).unwrap();
8443 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
8444 }
8445
8446 #[test]
8447 fn resume_session_config_enable_experimental_mode_omitted_when_none() {
8448 let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
8449 assert_eq!(cfg.enable_experimental_mode, None);
8450
8451 let (wire, _runtime) = cfg
8452 .into_wire()
8453 .expect("default resume config has no duplicate handlers");
8454 assert_eq!(wire.is_experimental_mode, None);
8455
8456 let json = serde_json::to_value(&wire).unwrap();
8457 assert!(json.get("isExperimentalMode").is_none());
8458 }
8459}
8460
8461#[cfg(test)]
8462mod is_terminal_tests {
8463 use super::Tool;
8464
8465 #[test]
8466 fn is_terminal_serializes_as_camel_case_when_set() {
8467 let tool = Tool {
8468 name: "clear_context".to_owned(),
8469 is_terminal: true,
8470 ..Default::default()
8471 };
8472 let value = serde_json::to_value(&tool).expect("tool serializes");
8473 assert_eq!(
8474 value.get("isTerminal"),
8475 Some(&serde_json::Value::Bool(true))
8476 );
8477 }
8478
8479 #[test]
8480 fn is_terminal_is_omitted_when_false() {
8481 let tool = Tool {
8482 name: "plain".to_owned(),
8483 ..Default::default()
8484 };
8485 let value = serde_json::to_value(&tool).expect("tool serializes");
8486 assert!(value.get("isTerminal").is_none());
8487 }
8488
8489 #[test]
8492 fn is_terminal_appears_in_debug_output() {
8493 let terminal = Tool {
8494 name: "clear_context".to_owned(),
8495 is_terminal: true,
8496 ..Default::default()
8497 };
8498 assert!(format!("{terminal:?}").contains("is_terminal: true"));
8499
8500 let plain = Tool {
8501 name: "plain".to_owned(),
8502 ..Default::default()
8503 };
8504 assert!(format!("{plain:?}").contains("is_terminal: false"));
8505 }
8506}