1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11
12use indexmap::IndexMap;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16use crate::canvas::{CanvasDeclaration, CanvasHandler};
17pub use crate::copilot_request_handler::{
18 CopilotHttpRequest, CopilotHttpResponse, CopilotHttpResponseBody, CopilotRequestContext,
19 CopilotRequestError, CopilotRequestHandler, CopilotRequestTransport, CopilotWebSocketForwarder,
20 CopilotWebSocketForwarderBuilder, CopilotWebSocketHandler, CopilotWebSocketMessage,
21 CopilotWebSocketResponse, WebSocketTransform, forward_http,
22};
23use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance};
24pub use crate::generated::api_types::{ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus};
26pub use crate::generated::session_events::AutoTier;
28use crate::generated::session_events::ReasoningSummary;
29pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig};
31use crate::github_token::GitHubTokenProvider;
32use crate::handler::{
33 AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler,
34 PermissionHandler, UserInputHandler,
35};
36use crate::hooks::SessionHooks;
37use crate::provider_token::BearerTokenProvider;
38pub use crate::session_fs::{
39 DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig,
40 SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult,
41 SessionFsSqliteQueryType, SessionFsSqliteTransactionError,
42 SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionStatement,
43};
44pub use crate::trace_context::{TraceContext, TraceContextProvider};
45use crate::transforms::SystemMessageTransform;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
50#[allow(dead_code)]
51#[non_exhaustive]
52pub(crate) enum ConnectionState {
53 Disconnected,
55 Connecting,
57 Connected,
59 Error,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
68#[non_exhaustive]
69pub enum SessionLifecycleEventType {
70 #[serde(rename = "session.created")]
72 Created,
73 #[serde(rename = "session.deleted")]
75 Deleted,
76 #[serde(rename = "session.updated")]
78 Updated,
79 #[serde(rename = "session.foreground")]
81 Foreground,
82 #[serde(rename = "session.background")]
84 Background,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub struct SessionLifecycleEventMetadata {
90 #[serde(rename = "startTime")]
92 pub start_time: String,
93 #[serde(rename = "modifiedTime")]
95 pub modified_time: String,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub summary: Option<String>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct SessionLifecycleEvent {
105 #[serde(rename = "type")]
107 pub event_type: SessionLifecycleEventType,
108 #[serde(rename = "sessionId")]
110 pub session_id: SessionId,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub metadata: Option<SessionLifecycleEventMetadata>,
114}
115
116#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
122#[serde(transparent)]
123pub struct SessionId(String);
124
125impl SessionId {
126 pub fn new(id: impl Into<String>) -> Self {
128 Self(id.into())
129 }
130
131 pub fn as_str(&self) -> &str {
133 &self.0
134 }
135
136 pub fn into_inner(self) -> String {
138 self.0
139 }
140}
141
142impl std::ops::Deref for SessionId {
143 type Target = str;
144
145 fn deref(&self) -> &str {
146 &self.0
147 }
148}
149
150impl std::fmt::Display for SessionId {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 f.write_str(&self.0)
153 }
154}
155
156impl From<String> for SessionId {
157 fn from(s: String) -> Self {
158 Self(s)
159 }
160}
161
162impl From<&str> for SessionId {
163 fn from(s: &str) -> Self {
164 Self(s.to_owned())
165 }
166}
167
168impl AsRef<str> for SessionId {
169 fn as_ref(&self) -> &str {
170 &self.0
171 }
172}
173
174impl std::borrow::Borrow<str> for SessionId {
175 fn borrow(&self) -> &str {
176 &self.0
177 }
178}
179
180impl From<SessionId> for String {
181 fn from(id: SessionId) -> String {
182 id.0
183 }
184}
185
186impl PartialEq<str> for SessionId {
187 fn eq(&self, other: &str) -> bool {
188 self.0 == other
189 }
190}
191
192impl PartialEq<String> for SessionId {
193 fn eq(&self, other: &String) -> bool {
194 &self.0 == other
195 }
196}
197
198impl PartialEq<SessionId> for String {
199 fn eq(&self, other: &SessionId) -> bool {
200 self == &other.0
201 }
202}
203
204impl PartialEq<&str> for SessionId {
205 fn eq(&self, other: &&str) -> bool {
206 self.0 == *other
207 }
208}
209
210impl PartialEq<&SessionId> for SessionId {
211 fn eq(&self, other: &&SessionId) -> bool {
212 self.0 == other.0
213 }
214}
215
216impl PartialEq<SessionId> for &SessionId {
217 fn eq(&self, other: &SessionId) -> bool {
218 self.0 == other.0
219 }
220}
221
222#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
228#[serde(transparent)]
229pub struct RequestId(String);
230
231impl RequestId {
232 pub fn new(id: impl Into<String>) -> Self {
234 Self(id.into())
235 }
236
237 pub fn into_inner(self) -> String {
239 self.0
240 }
241}
242
243impl std::ops::Deref for RequestId {
244 type Target = str;
245
246 fn deref(&self) -> &str {
247 &self.0
248 }
249}
250
251impl std::fmt::Display for RequestId {
252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253 f.write_str(&self.0)
254 }
255}
256
257impl From<String> for RequestId {
258 fn from(s: String) -> Self {
259 Self(s)
260 }
261}
262
263impl From<&str> for RequestId {
264 fn from(s: &str) -> Self {
265 Self(s.to_owned())
266 }
267}
268
269impl AsRef<str> for RequestId {
270 fn as_ref(&self) -> &str {
271 &self.0
272 }
273}
274
275impl std::borrow::Borrow<str> for RequestId {
276 fn borrow(&self) -> &str {
277 &self.0
278 }
279}
280
281impl From<RequestId> for String {
282 fn from(id: RequestId) -> String {
283 id.0
284 }
285}
286
287impl PartialEq<str> for RequestId {
288 fn eq(&self, other: &str) -> bool {
289 self.0 == other
290 }
291}
292
293impl PartialEq<String> for RequestId {
294 fn eq(&self, other: &String) -> bool {
295 &self.0 == other
296 }
297}
298
299impl PartialEq<RequestId> for String {
300 fn eq(&self, other: &RequestId) -> bool {
301 self == &other.0
302 }
303}
304
305impl PartialEq<&str> for RequestId {
306 fn eq(&self, other: &&str) -> bool {
307 self.0 == *other
308 }
309}
310
311#[derive(Clone, Default, Serialize, Deserialize)]
326#[serde(rename_all = "camelCase")]
327#[non_exhaustive]
328pub struct Tool {
329 pub name: String,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub namespaced_name: Option<String>,
335 #[serde(default)]
337 pub description: String,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
340 pub instructions: Option<String>,
341 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
343 pub parameters: IndexMap<String, Value>,
344 #[serde(default, skip_serializing_if = "is_false")]
348 pub overrides_built_in_tool: bool,
349 #[serde(default, skip_serializing_if = "is_false")]
353 pub skip_permission: bool,
354 #[serde(default, skip_serializing_if = "is_false")]
359 pub is_terminal: bool,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
366 pub defer: Option<DeferMode>,
367 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
372 pub metadata: IndexMap<String, Value>,
373 #[serde(skip)]
385 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
386}
387
388#[inline]
389fn is_false(b: &bool) -> bool {
390 !*b
391}
392
393#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
396#[serde(rename_all = "lowercase")]
397pub enum DeferMode {
398 Auto,
400 Never,
402}
403
404impl Tool {
405 pub fn new(name: impl Into<String>) -> Self {
425 Self {
426 name: name.into(),
427 ..Default::default()
428 }
429 }
430
431 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
434 self.namespaced_name = Some(namespaced_name.into());
435 self
436 }
437
438 pub fn with_description(mut self, description: impl Into<String>) -> Self {
440 self.description = description.into();
441 self
442 }
443
444 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
446 self.instructions = Some(instructions.into());
447 self
448 }
449
450 pub fn with_parameters(mut self, parameters: Value) -> Self {
464 self.parameters = crate::tool::tool_parameters(parameters);
465 self
466 }
467
468 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
472 self.overrides_built_in_tool = overrides;
473 self
474 }
475
476 pub fn with_skip_permission(mut self, skip: bool) -> Self {
480 self.skip_permission = skip;
481 self
482 }
483
484 #[must_use]
491 pub fn with_is_terminal(mut self, is_terminal: bool) -> Self {
492 self.is_terminal = is_terminal;
493 self
494 }
495
496 pub fn with_defer(mut self, defer: DeferMode) -> Self {
500 self.defer = Some(defer);
501 self
502 }
503
504 pub fn with_metadata(mut self, metadata: IndexMap<String, Value>) -> Self {
507 self.metadata = metadata;
508 self
509 }
510
511 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
515 self.handler = Some(handler);
516 self
517 }
518
519 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
524 self.handler.as_ref()
525 }
526}
527
528impl std::fmt::Debug for Tool {
529 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
530 f.debug_struct("Tool")
531 .field("name", &self.name)
532 .field("namespaced_name", &self.namespaced_name)
533 .field("description", &self.description)
534 .field("instructions", &self.instructions)
535 .field("parameters", &self.parameters)
536 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
537 .field("skip_permission", &self.skip_permission)
538 .field("is_terminal", &self.is_terminal)
539 .field("defer", &self.defer)
540 .field("metadata", &self.metadata)
541 .field(
542 "handler",
543 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
544 )
545 .finish()
546 }
547}
548
549#[non_exhaustive]
552#[derive(Debug, Clone)]
553pub struct CommandContext {
554 pub session_id: SessionId,
556 pub command: String,
558 pub command_name: String,
560 pub args: String,
562}
563
564#[async_trait::async_trait]
570pub trait CommandHandler: Send + Sync {
571 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
573}
574
575#[non_exhaustive]
581#[derive(Clone)]
582pub struct CommandDefinition {
583 pub name: String,
585 pub description: Option<String>,
587 pub handler: Arc<dyn CommandHandler>,
589}
590
591impl CommandDefinition {
592 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
595 Self {
596 name: name.into(),
597 description: None,
598 handler,
599 }
600 }
601
602 pub fn with_description(mut self, description: impl Into<String>) -> Self {
604 self.description = Some(description.into());
605 self
606 }
607}
608
609impl std::fmt::Debug for CommandDefinition {
610 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
611 f.debug_struct("CommandDefinition")
612 .field("name", &self.name)
613 .field("description", &self.description)
614 .field("handler", &"<set>")
615 .finish()
616 }
617}
618
619impl Serialize for CommandDefinition {
620 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
621 use serde::ser::SerializeStruct;
622 let mut state = serializer.serialize_struct("CommandDefinition", 2)?;
623 state.serialize_field("name", &self.name)?;
624 state.serialize_field("description", self.description.as_deref().unwrap_or(""))?;
625 state.end()
626 }
627}
628
629#[derive(Debug, Clone, Default, Serialize, Deserialize)]
636#[serde(rename_all = "camelCase")]
637#[non_exhaustive]
638pub struct CustomAgentConfig {
639 pub name: String,
641 #[serde(default, skip_serializing_if = "Option::is_none")]
643 pub display_name: Option<String>,
644 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub description: Option<String>,
647 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub tools: Option<Vec<String>>,
650 pub prompt: String,
652 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
655 #[serde(default, skip_serializing_if = "Option::is_none")]
657 pub infer: Option<bool>,
658 #[serde(default, skip_serializing_if = "Option::is_none")]
660 pub skills: Option<Vec<String>>,
661 #[serde(default, skip_serializing_if = "Option::is_none")]
666 pub model: Option<String>,
667 #[serde(default, skip_serializing_if = "Option::is_none")]
672 pub reasoning_effort: Option<String>,
673}
674
675impl CustomAgentConfig {
676 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
683 Self {
684 name: name.into(),
685 prompt: prompt.into(),
686 ..Self::default()
687 }
688 }
689
690 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
692 self.display_name = Some(display_name.into());
693 self
694 }
695
696 pub fn with_description(mut self, description: impl Into<String>) -> Self {
698 self.description = Some(description.into());
699 self
700 }
701
702 pub fn with_tools<I, S>(mut self, tools: I) -> Self
705 where
706 I: IntoIterator<Item = S>,
707 S: Into<String>,
708 {
709 self.tools = Some(tools.into_iter().map(Into::into).collect());
710 self
711 }
712
713 pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
715 self.mcp_servers = Some(mcp_servers);
716 self
717 }
718
719 pub fn with_infer(mut self, infer: bool) -> Self {
721 self.infer = Some(infer);
722 self
723 }
724
725 pub fn with_skills<I, S>(mut self, skills: I) -> Self
727 where
728 I: IntoIterator<Item = S>,
729 S: Into<String>,
730 {
731 self.skills = Some(skills.into_iter().map(Into::into).collect());
732 self
733 }
734
735 pub fn with_model(mut self, model: impl Into<String>) -> Self {
737 self.model = Some(model.into());
738 self
739 }
740
741 pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into<String>) -> Self {
743 self.reasoning_effort = Some(reasoning_effort.into());
744 self
745 }
746}
747
748#[derive(Debug, Clone, Default, Serialize, Deserialize)]
755#[serde(rename_all = "camelCase")]
756pub struct DefaultAgentConfig {
757 #[serde(default, skip_serializing_if = "Option::is_none")]
759 pub excluded_tools: Option<Vec<String>>,
760}
761
762#[derive(Debug, Clone, Default, Serialize, Deserialize)]
768#[serde(rename_all = "camelCase")]
769#[non_exhaustive]
770pub struct LargeToolOutputConfig {
771 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub enabled: Option<bool>,
774 #[serde(default, skip_serializing_if = "Option::is_none")]
777 pub max_size_bytes: Option<u64>,
778 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
781 pub output_directory: Option<PathBuf>,
782}
783
784impl LargeToolOutputConfig {
785 pub fn new() -> Self {
788 Self::default()
789 }
790
791 pub fn with_enabled(mut self, enabled: bool) -> Self {
793 self.enabled = Some(enabled);
794 self
795 }
796
797 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
799 self.max_size_bytes = Some(max_size_bytes);
800 self
801 }
802
803 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
805 self.output_directory = Some(output_directory.into());
806 self
807 }
808}
809
810#[derive(Debug, Clone, Default, Serialize, Deserialize)]
816#[serde(rename_all = "camelCase")]
817#[non_exhaustive]
818pub struct ToolSearchConfig {
819 #[serde(default, skip_serializing_if = "Option::is_none")]
821 pub enabled: Option<bool>,
822 #[serde(default, skip_serializing_if = "Option::is_none")]
825 pub defer_threshold: Option<u32>,
826}
827
828impl ToolSearchConfig {
829 pub fn new() -> Self {
832 Self::default()
833 }
834
835 pub fn with_enabled(mut self, enabled: bool) -> Self {
837 self.enabled = Some(enabled);
838 self
839 }
840
841 pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
844 self.defer_threshold = Some(defer_threshold);
845 self
846 }
847}
848
849#[derive(Debug, Clone, Default, Serialize, Deserialize)]
854#[serde(rename_all = "camelCase")]
855#[non_exhaustive]
856pub struct GitHubMcpToolConfig {
857 #[serde(default, skip_serializing_if = "Option::is_none")]
859 pub enable_all_tools: Option<bool>,
860 #[serde(default, skip_serializing_if = "Option::is_none")]
862 pub additional_toolsets: Option<Vec<String>>,
863 #[serde(default, skip_serializing_if = "Option::is_none")]
865 pub additional_tools: Option<Vec<String>>,
866 #[serde(default, skip_serializing_if = "Option::is_none")]
868 pub enable_insiders_mode: Option<bool>,
869 #[serde(default, skip_serializing_if = "Option::is_none")]
873 pub disable_form_deferral: Option<bool>,
874}
875
876impl GitHubMcpToolConfig {
877 pub fn new() -> Self {
879 Self::default()
880 }
881
882 pub fn with_enable_all_tools(mut self, value: bool) -> Self {
884 self.enable_all_tools = Some(value);
885 self
886 }
887
888 pub fn with_additional_toolsets<I, S>(mut self, values: I) -> Self
890 where
891 I: IntoIterator<Item = S>,
892 S: Into<String>,
893 {
894 self.additional_toolsets = Some(values.into_iter().map(Into::into).collect());
895 self
896 }
897
898 pub fn with_additional_tools<I, S>(mut self, values: I) -> Self
900 where
901 I: IntoIterator<Item = S>,
902 S: Into<String>,
903 {
904 self.additional_tools = Some(values.into_iter().map(Into::into).collect());
905 self
906 }
907
908 pub fn with_enable_insiders_mode(mut self, value: bool) -> Self {
910 self.enable_insiders_mode = Some(value);
911 self
912 }
913
914 pub fn with_disable_form_deferral(mut self, value: bool) -> Self {
918 self.disable_form_deferral = Some(value);
919 self
920 }
921}
922
923#[derive(Debug, Clone, Default, Serialize, Deserialize)]
930#[serde(rename_all = "camelCase")]
931#[non_exhaustive]
932pub struct InfiniteSessionConfig {
933 #[serde(default, skip_serializing_if = "Option::is_none")]
935 pub enabled: Option<bool>,
936 #[serde(default, skip_serializing_if = "Option::is_none")]
939 pub background_compaction_threshold: Option<f64>,
940 #[serde(default, skip_serializing_if = "Option::is_none")]
943 pub buffer_exhaustion_threshold: Option<f64>,
944}
945
946impl InfiniteSessionConfig {
947 pub fn new() -> Self {
950 Self::default()
951 }
952
953 pub fn with_enabled(mut self, enabled: bool) -> Self {
956 self.enabled = Some(enabled);
957 self
958 }
959
960 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
963 self.background_compaction_threshold = Some(threshold);
964 self
965 }
966
967 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
970 self.buffer_exhaustion_threshold = Some(threshold);
971 self
972 }
973}
974
975#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
986#[serde(rename_all = "camelCase")]
987#[non_exhaustive]
988pub struct MemoryConfiguration {
989 pub enabled: bool,
991}
992
993impl MemoryConfiguration {
994 pub fn enabled() -> Self {
996 Self { enabled: true }
997 }
998
999 pub fn disabled() -> Self {
1001 Self { enabled: false }
1002 }
1003
1004 pub fn with_enabled(mut self, enabled: bool) -> Self {
1006 self.enabled = enabled;
1007 self
1008 }
1009}
1010
1011#[derive(Debug, Clone, Serialize, Deserialize)]
1013#[serde(rename_all = "camelCase")]
1014#[non_exhaustive]
1015pub struct CloudSessionRepository {
1016 pub owner: String,
1018 pub name: String,
1020 #[serde(skip_serializing_if = "Option::is_none")]
1022 pub branch: Option<String>,
1023}
1024
1025impl CloudSessionRepository {
1026 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
1028 Self {
1029 owner: owner.into(),
1030 name: name.into(),
1031 branch: None,
1032 }
1033 }
1034
1035 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
1037 self.branch = Some(branch.into());
1038 self
1039 }
1040}
1041
1042#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1044#[serde(rename_all = "camelCase")]
1045#[non_exhaustive]
1046pub struct CloudSessionOptions {
1047 #[serde(skip_serializing_if = "Option::is_none")]
1049 pub repository: Option<CloudSessionRepository>,
1050}
1051
1052impl CloudSessionOptions {
1053 pub fn with_repository(repository: CloudSessionRepository) -> Self {
1055 Self {
1056 repository: Some(repository),
1057 }
1058 }
1059}
1060
1061#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1063#[serde(rename_all = "camelCase")]
1064pub struct ExtensionInfo {
1065 pub source: String,
1067 pub name: String,
1069}
1070
1071impl ExtensionInfo {
1072 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
1074 Self {
1075 source: source.into(),
1076 name: name.into(),
1077 }
1078 }
1079}
1080
1081#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1092#[serde(rename_all = "camelCase")]
1093pub struct CanvasProviderIdentity {
1094 pub id: String,
1096 #[serde(skip_serializing_if = "Option::is_none")]
1098 pub name: Option<String>,
1099}
1100
1101impl CanvasProviderIdentity {
1102 pub fn new(id: impl Into<String>) -> Self {
1104 Self {
1105 id: id.into(),
1106 name: None,
1107 }
1108 }
1109
1110 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1112 self.name = Some(name.into());
1113 self
1114 }
1115}
1116
1117#[derive(Debug, Clone, Serialize, Deserialize)]
1151#[serde(tag = "type", rename_all = "lowercase")]
1152#[non_exhaustive]
1153pub enum McpServerConfig {
1154 #[serde(alias = "local")]
1158 Stdio(McpStdioServerConfig),
1159 Http(McpHttpServerConfig),
1161 Sse(McpHttpServerConfig),
1163}
1164
1165#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1169#[serde(rename_all = "camelCase")]
1170pub struct McpStdioServerConfig {
1171 #[serde(default, skip_serializing_if = "Option::is_none")]
1177 pub tools: Option<Vec<String>>,
1178 #[serde(default, skip_serializing_if = "Option::is_none")]
1180 pub timeout: Option<i64>,
1181 pub command: String,
1183 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1185 pub args: Vec<String>,
1186 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1189 pub env: HashMap<String, String>,
1190 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1192 pub working_directory: Option<String>,
1193}
1194
1195#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1199#[serde(rename_all = "camelCase")]
1200pub struct McpHttpServerConfig {
1201 #[serde(default, skip_serializing_if = "Option::is_none")]
1207 pub tools: Option<Vec<String>>,
1208 #[serde(default, skip_serializing_if = "Option::is_none")]
1210 pub timeout: Option<i64>,
1211 pub url: String,
1213 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1215 pub headers: HashMap<String, String>,
1216}
1217
1218#[derive(Clone, Default, Serialize, Deserialize)]
1224#[serde(rename_all = "camelCase")]
1225#[non_exhaustive]
1226pub struct ProviderConfig {
1227 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1230 pub provider_type: Option<String>,
1231 #[serde(default, skip_serializing_if = "Option::is_none")]
1234 pub wire_api: Option<String>,
1235 #[serde(default, skip_serializing_if = "Option::is_none")]
1240 pub transport: Option<String>,
1241 pub base_url: String,
1243 #[serde(default, skip_serializing_if = "Option::is_none")]
1245 pub api_key: Option<String>,
1246 #[serde(default, skip_serializing_if = "Option::is_none")]
1250 pub bearer_token: Option<String>,
1251 #[serde(skip)]
1254 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1255 #[serde(default, skip_serializing_if = "Option::is_none")]
1256 pub(crate) has_bearer_token_provider: Option<bool>,
1257 #[serde(default, skip_serializing_if = "Option::is_none")]
1259 pub azure: Option<AzureProviderOptions>,
1260 #[serde(default, skip_serializing_if = "Option::is_none")]
1262 pub headers: Option<HashMap<String, String>>,
1263 #[serde(default, skip_serializing_if = "Option::is_none")]
1267 pub model_id: Option<String>,
1268 #[serde(default, skip_serializing_if = "Option::is_none")]
1275 pub wire_model: Option<String>,
1276 #[serde(default, skip_serializing_if = "Option::is_none")]
1281 pub max_prompt_tokens: Option<i64>,
1282 #[serde(default, skip_serializing_if = "Option::is_none")]
1285 pub max_output_tokens: Option<i64>,
1286}
1287
1288impl std::fmt::Debug for ProviderConfig {
1289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1290 f.debug_struct("ProviderConfig")
1291 .field("provider_type", &self.provider_type)
1292 .field("wire_api", &self.wire_api)
1293 .field("transport", &self.transport)
1294 .field("base_url", &self.base_url)
1295 .field("api_key", &self.api_key)
1296 .field("bearer_token", &self.bearer_token)
1297 .field(
1298 "bearer_token_provider",
1299 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1300 )
1301 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1302 .field("azure", &self.azure)
1303 .field("headers", &self.headers)
1304 .field("model_id", &self.model_id)
1305 .field("wire_model", &self.wire_model)
1306 .field("max_prompt_tokens", &self.max_prompt_tokens)
1307 .field("max_output_tokens", &self.max_output_tokens)
1308 .finish()
1309 }
1310}
1311
1312impl ProviderConfig {
1313 pub fn new(base_url: impl Into<String>) -> Self {
1316 Self {
1317 base_url: base_url.into(),
1318 ..Self::default()
1319 }
1320 }
1321
1322 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1324 self.provider_type = Some(provider_type.into());
1325 self
1326 }
1327
1328 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1330 self.wire_api = Some(wire_api.into());
1331 self
1332 }
1333
1334 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1337 self.transport = Some(transport.into());
1338 self
1339 }
1340
1341 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1343 self.api_key = Some(api_key.into());
1344 self
1345 }
1346
1347 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1350 self.bearer_token = Some(bearer_token.into());
1351 self
1352 }
1353
1354 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1360 self.bearer_token_provider = Some(provider);
1361 self
1362 }
1363
1364 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1366 self.azure = Some(azure);
1367 self
1368 }
1369
1370 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1372 self.headers = Some(headers);
1373 self
1374 }
1375
1376 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1379 self.model_id = Some(model_id.into());
1380 self
1381 }
1382
1383 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1388 self.wire_model = Some(wire_model.into());
1389 self
1390 }
1391
1392 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1396 self.max_prompt_tokens = Some(max);
1397 self
1398 }
1399
1400 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1403 self.max_output_tokens = Some(max);
1404 self
1405 }
1406}
1407
1408#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1421#[serde(rename_all = "camelCase")]
1422#[non_exhaustive]
1423pub struct CapiSessionOptions {
1424 #[serde(default, skip_serializing_if = "Option::is_none")]
1435 pub auto_tier: Option<AutoTier>,
1436
1437 #[serde(default, skip_serializing_if = "Option::is_none")]
1443 pub enable_web_socket_responses: Option<bool>,
1444}
1445
1446impl CapiSessionOptions {
1447 pub fn new() -> Self {
1449 Self::default()
1450 }
1451
1452 pub fn with_auto_tier(mut self, auto_tier: AutoTier) -> Self {
1454 self.auto_tier = Some(auto_tier);
1455 self
1456 }
1457
1458 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1460 self.enable_web_socket_responses = Some(enable);
1461 self
1462 }
1463}
1464
1465#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1467#[serde(rename_all = "camelCase")]
1468pub struct AzureProviderOptions {
1469 #[serde(default, skip_serializing_if = "Option::is_none")]
1471 pub api_version: Option<String>,
1472}
1473
1474#[derive(Clone, Default, Serialize, Deserialize)]
1485#[serde(rename_all = "camelCase")]
1486#[non_exhaustive]
1487pub struct NamedProviderConfig {
1488 pub name: String,
1491 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1494 pub provider_type: Option<String>,
1495 #[serde(default, skip_serializing_if = "Option::is_none")]
1498 pub wire_api: Option<String>,
1499 pub base_url: String,
1501 #[serde(default, skip_serializing_if = "Option::is_none")]
1503 pub api_key: Option<String>,
1504 #[serde(default, skip_serializing_if = "Option::is_none")]
1507 pub bearer_token: Option<String>,
1508 #[serde(skip)]
1511 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1512 #[serde(default, skip_serializing_if = "Option::is_none")]
1513 pub(crate) has_bearer_token_provider: Option<bool>,
1514 #[serde(default, skip_serializing_if = "Option::is_none")]
1516 pub azure: Option<AzureProviderOptions>,
1517 #[serde(default, skip_serializing_if = "Option::is_none")]
1519 pub headers: Option<HashMap<String, String>>,
1520}
1521
1522impl std::fmt::Debug for NamedProviderConfig {
1523 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1524 f.debug_struct("NamedProviderConfig")
1525 .field("name", &self.name)
1526 .field("provider_type", &self.provider_type)
1527 .field("wire_api", &self.wire_api)
1528 .field("base_url", &self.base_url)
1529 .field("api_key", &self.api_key)
1530 .field("bearer_token", &self.bearer_token)
1531 .field(
1532 "bearer_token_provider",
1533 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1534 )
1535 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1536 .field("azure", &self.azure)
1537 .field("headers", &self.headers)
1538 .finish()
1539 }
1540}
1541
1542impl NamedProviderConfig {
1543 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1546 Self {
1547 name: name.into(),
1548 base_url: base_url.into(),
1549 ..Self::default()
1550 }
1551 }
1552
1553 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1555 self.provider_type = Some(provider_type.into());
1556 self
1557 }
1558
1559 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1561 self.wire_api = Some(wire_api.into());
1562 self
1563 }
1564
1565 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1567 self.api_key = Some(api_key.into());
1568 self
1569 }
1570
1571 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1574 self.bearer_token = Some(bearer_token.into());
1575 self
1576 }
1577
1578 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1584 self.bearer_token_provider = Some(provider);
1585 self
1586 }
1587
1588 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1590 self.azure = Some(azure);
1591 self
1592 }
1593
1594 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1596 self.headers = Some(headers);
1597 self
1598 }
1599}
1600
1601fn prepare_bearer_token_providers(
1602 provider: &mut Option<ProviderConfig>,
1603 providers: &mut Option<Vec<NamedProviderConfig>>,
1604) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1605 let mut bearer_token_providers = HashMap::new();
1606
1607 if let Some(provider) = provider.as_mut()
1608 && let Some(token_provider) = provider.bearer_token_provider.take()
1609 {
1610 provider.has_bearer_token_provider = Some(true);
1611 bearer_token_providers.insert("default".to_string(), token_provider);
1612 }
1613
1614 if let Some(providers) = providers.as_mut() {
1615 for provider in providers {
1616 if let Some(token_provider) = provider.bearer_token_provider.take() {
1617 provider.has_bearer_token_provider = Some(true);
1618 bearer_token_providers.insert(provider.name.clone(), token_provider);
1619 }
1620 }
1621 }
1622
1623 bearer_token_providers
1624}
1625
1626#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1634#[serde(rename_all = "camelCase")]
1635#[non_exhaustive]
1636pub struct ProviderModelConfig {
1637 pub id: String,
1640 pub provider: String,
1642 #[serde(default, skip_serializing_if = "Option::is_none")]
1645 pub wire_model: Option<String>,
1646 #[serde(default, skip_serializing_if = "Option::is_none")]
1649 pub model_id: Option<String>,
1650 #[serde(default, skip_serializing_if = "Option::is_none")]
1652 pub name: Option<String>,
1653 #[serde(default, skip_serializing_if = "Option::is_none")]
1655 pub max_prompt_tokens: Option<i64>,
1656 #[serde(default, skip_serializing_if = "Option::is_none")]
1658 pub max_context_window_tokens: Option<i64>,
1659 #[serde(default, skip_serializing_if = "Option::is_none")]
1661 pub max_output_tokens: Option<i64>,
1662 #[serde(default, skip_serializing_if = "Option::is_none")]
1665 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1666}
1667
1668impl ProviderModelConfig {
1669 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1672 Self {
1673 id: id.into(),
1674 provider: provider.into(),
1675 ..Self::default()
1676 }
1677 }
1678
1679 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1681 self.wire_model = Some(wire_model.into());
1682 self
1683 }
1684
1685 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1688 self.model_id = Some(model_id.into());
1689 self
1690 }
1691
1692 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1694 self.name = Some(name.into());
1695 self
1696 }
1697
1698 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1700 self.max_prompt_tokens = Some(max);
1701 self
1702 }
1703
1704 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1706 self.max_context_window_tokens = Some(max);
1707 self
1708 }
1709
1710 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1712 self.max_output_tokens = Some(max);
1713 self
1714 }
1715
1716 pub fn with_capabilities(
1718 mut self,
1719 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1720 ) -> Self {
1721 self.capabilities = Some(capabilities);
1722 self
1723 }
1724}
1725
1726#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1730#[serde(untagged)]
1731pub enum ExpFlagValue {
1732 Bool(bool),
1734 Integer(i64),
1736 Float(f64),
1738 String(String),
1740 Null,
1742}
1743
1744#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1748#[serde(rename_all = "PascalCase")]
1749pub struct ExpConfigEntry {
1750 pub id: String,
1752 pub parameters: HashMap<String, ExpFlagValue>,
1754}
1755
1756#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1762#[serde(rename_all = "PascalCase")]
1763pub struct CopilotExpAssignmentResponse {
1764 #[serde(default)]
1766 pub features: Vec<String>,
1767 #[serde(default)]
1769 pub flights: HashMap<String, String>,
1770 #[serde(default)]
1772 pub configs: Vec<ExpConfigEntry>,
1773 #[serde(default, skip_serializing_if = "Option::is_none")]
1775 pub parameter_groups: Option<Value>,
1776 #[serde(default, skip_serializing_if = "Option::is_none")]
1778 pub flighting_version: Option<i64>,
1779 #[serde(default, skip_serializing_if = "Option::is_none")]
1781 pub impression_id: Option<String>,
1782 #[serde(default)]
1784 pub assignment_context: String,
1785}
1786
1787pub struct DisableBypassPermissionsModes;
1789
1790impl DisableBypassPermissionsModes {
1791 pub const ALLOW_AUTO_ONLY: &'static str = "allow-auto-only";
1793 pub const DISABLE: &'static str = "disable";
1795}
1796
1797#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1806#[serde(rename_all = "camelCase")]
1807#[non_exhaustive]
1808pub struct ManagedSettingsPermissions {
1809 #[serde(default, skip_serializing_if = "Option::is_none")]
1813 pub disable_bypass_permissions_mode: Option<String>,
1814 #[serde(default, skip_serializing_if = "Option::is_none")]
1816 pub deny: Option<Vec<String>>,
1817 #[serde(default, skip_serializing_if = "Option::is_none")]
1819 pub ask: Option<Vec<String>>,
1820 #[serde(default, skip_serializing_if = "Option::is_none")]
1822 pub allow: Option<Vec<String>>,
1823}
1824
1825impl ManagedSettingsPermissions {
1826 pub fn with_disable_bypass_permissions_mode(mut self, value: impl Into<String>) -> Self {
1828 self.disable_bypass_permissions_mode = Some(value.into());
1829 self
1830 }
1831
1832 pub fn with_deny(mut self, rules: Vec<String>) -> Self {
1834 self.deny = Some(rules);
1835 self
1836 }
1837
1838 pub fn with_ask(mut self, rules: Vec<String>) -> Self {
1840 self.ask = Some(rules);
1841 self
1842 }
1843
1844 pub fn with_allow(mut self, rules: Vec<String>) -> Self {
1846 self.allow = Some(rules);
1847 self
1848 }
1849}
1850
1851#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1861#[serde(rename_all = "camelCase")]
1862#[non_exhaustive]
1863pub struct ManagedSettings {
1864 #[serde(default, skip_serializing_if = "Option::is_none")]
1866 pub permissions: Option<ManagedSettingsPermissions>,
1867}
1868
1869impl ManagedSettings {
1870 pub fn with_permissions(mut self, permissions: ManagedSettingsPermissions) -> Self {
1872 self.permissions = Some(permissions);
1873 self
1874 }
1875}
1876
1877#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1879#[serde(rename_all = "lowercase")]
1880#[non_exhaustive]
1881pub enum AskUserVariant {
1882 #[default]
1884 Legacy,
1885 Elicitation,
1887}
1888
1889#[derive(Clone)]
1941#[non_exhaustive]
1942pub struct SessionConfig {
1943 pub session_id: Option<SessionId>,
1945 pub model: Option<String>,
1947 pub client_name: Option<String>,
1949 pub reasoning_effort: Option<String>,
1951 pub reasoning_summary: Option<ReasoningSummary>,
1955 pub context_tier: Option<String>,
1958 pub streaming: Option<bool>,
1960 pub system_message: Option<SystemMessageConfig>,
1962 pub ask_user_variant: Option<AskUserVariant>,
1967 pub tools: Option<Vec<Tool>>,
1969 pub canvases: Option<Vec<CanvasDeclaration>>,
1971 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1976 pub request_canvas_renderer: Option<bool>,
1978 pub request_extensions: Option<bool>,
1980 pub extension_sdk_path: Option<String>,
1984 pub extension_info: Option<ExtensionInfo>,
1986 pub canvas_provider: Option<CanvasProviderIdentity>,
1989 pub available_tools: Option<Vec<String>>,
1991 pub excluded_tools: Option<Vec<String>>,
1993 pub excluded_builtin_agents: Option<Vec<String>>,
1999 pub included_builtin_skills: Option<Vec<String>>,
2003 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
2005 pub mcp_oauth_token_storage: Option<String>,
2014 pub enable_config_discovery: Option<bool>,
2017 pub skip_embedding_retrieval: Option<bool>,
2019 pub embedding_cache_storage: Option<String>,
2022 pub organization_custom_instructions: Option<String>,
2024 pub enable_on_demand_instruction_discovery: Option<bool>,
2026 pub enable_file_hooks: Option<bool>,
2028 pub enable_host_git_operations: Option<bool>,
2030 pub enable_session_store: Option<bool>,
2032 pub enable_skills: Option<bool>,
2034 pub enable_mcp_apps: Option<bool>,
2061 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
2066 pub skill_directories: Option<Vec<PathBuf>>,
2068 pub instruction_directories: Option<Vec<PathBuf>>,
2071 pub plugin_directories: Option<Vec<PathBuf>>,
2073 pub large_output: Option<LargeToolOutputConfig>,
2075 pub tool_search: Option<ToolSearchConfig>,
2079 pub disabled_skills: Option<Vec<String>>,
2082 pub disabled_mcp_servers: Option<Vec<String>>,
2086 pub hooks: Option<bool>,
2090 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2092 pub default_agent: Option<DefaultAgentConfig>,
2096 pub agent: Option<String>,
2099 pub infinite_sessions: Option<InfiniteSessionConfig>,
2102 pub provider: Option<ProviderConfig>,
2106 pub capi: Option<CapiSessionOptions>,
2112 pub providers: Option<Vec<NamedProviderConfig>>,
2119 pub models: Option<Vec<ProviderModelConfig>>,
2125 pub enable_session_telemetry: Option<bool>,
2133 pub enable_citations: Option<bool>,
2135 pub enable_file_change_tracking: Option<bool>,
2138 pub session_limits: Option<SessionLimitsConfig>,
2140 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2143 pub memory: Option<MemoryConfiguration>,
2145 pub config_directory: Option<PathBuf>,
2148 pub working_directory: Option<PathBuf>,
2151 pub additional_directories: Option<Vec<PathBuf>>,
2155 pub github_token: Option<String>,
2161 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2167 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2173 pub cloud: Option<CloudSessionOptions>,
2176 pub include_sub_agent_streaming_events: Option<bool>,
2180 pub commands: Option<Vec<CommandDefinition>>,
2184 pub feature_flags: Option<HashMap<String, bool>>,
2190 #[doc(hidden)]
2197 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
2198 pub enable_managed_settings: Option<bool>,
2206 pub managed_settings: Option<ManagedSettings>,
2215 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2220 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2224 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2227 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2230 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2234 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2237 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2240 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2244 pub(crate) permission_policy: Option<crate::permission::Policy>,
2248 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2253 pub skip_custom_instructions: Option<bool>,
2257 pub custom_agents_local_only: Option<bool>,
2261 pub enable_experimental_mode: Option<bool>,
2266 pub coauthor_enabled: Option<bool>,
2270 pub manage_schedule_enabled: Option<bool>,
2274 pub event_buffer_capacity: Option<usize>,
2291}
2292
2293impl std::fmt::Debug for SessionConfig {
2294 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2295 f.debug_struct("SessionConfig")
2296 .field("session_id", &self.session_id)
2297 .field("model", &self.model)
2298 .field("client_name", &self.client_name)
2299 .field("reasoning_effort", &self.reasoning_effort)
2300 .field("reasoning_summary", &self.reasoning_summary)
2301 .field("context_tier", &self.context_tier)
2302 .field("streaming", &self.streaming)
2303 .field("system_message", &self.system_message)
2304 .field("ask_user_variant", &self.ask_user_variant)
2305 .field("tools", &self.tools)
2306 .field("canvases", &self.canvases)
2307 .field(
2308 "canvas_handler",
2309 &self.canvas_handler.as_ref().map(|_| "<set>"),
2310 )
2311 .field("request_canvas_renderer", &self.request_canvas_renderer)
2312 .field("request_extensions", &self.request_extensions)
2313 .field("extension_sdk_path", &self.extension_sdk_path)
2314 .field("extension_info", &self.extension_info)
2315 .field("canvas_provider", &self.canvas_provider)
2316 .field("available_tools", &self.available_tools)
2317 .field("excluded_tools", &self.excluded_tools)
2318 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2319 .field("included_builtin_skills", &self.included_builtin_skills)
2320 .field("mcp_servers", &self.mcp_servers)
2321 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2322 .field("embedding_cache_storage", &self.embedding_cache_storage)
2323 .field("enable_config_discovery", &self.enable_config_discovery)
2324 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2325 .field(
2326 "organization_custom_instructions",
2327 &self
2328 .organization_custom_instructions
2329 .as_ref()
2330 .map(|_| "<redacted>"),
2331 )
2332 .field(
2333 "enable_on_demand_instruction_discovery",
2334 &self.enable_on_demand_instruction_discovery,
2335 )
2336 .field("enable_file_hooks", &self.enable_file_hooks)
2337 .field(
2338 "enable_host_git_operations",
2339 &self.enable_host_git_operations,
2340 )
2341 .field("enable_session_store", &self.enable_session_store)
2342 .field("enable_skills", &self.enable_skills)
2343 .field("enable_mcp_apps", &self.enable_mcp_apps)
2344 .field("skill_directories", &self.skill_directories)
2345 .field("instruction_directories", &self.instruction_directories)
2346 .field("plugin_directories", &self.plugin_directories)
2347 .field("large_output", &self.large_output)
2348 .field("tool_search", &self.tool_search)
2349 .field("disabled_skills", &self.disabled_skills)
2350 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
2351 .field("hooks", &self.hooks)
2352 .field("custom_agents", &self.custom_agents)
2353 .field("default_agent", &self.default_agent)
2354 .field("agent", &self.agent)
2355 .field("infinite_sessions", &self.infinite_sessions)
2356 .field("provider", &self.provider)
2357 .field("capi", &self.capi)
2358 .field("enable_session_telemetry", &self.enable_session_telemetry)
2359 .field("enable_citations", &self.enable_citations)
2360 .field(
2361 "enable_file_change_tracking",
2362 &self.enable_file_change_tracking,
2363 )
2364 .field("session_limits", &self.session_limits)
2365 .field("model_capabilities", &self.model_capabilities)
2366 .field("memory", &self.memory)
2367 .field("config_directory", &self.config_directory)
2368 .field("working_directory", &self.working_directory)
2369 .field("additional_directories", &self.additional_directories)
2370 .field(
2371 "github_token",
2372 &self.github_token.as_ref().map(|_| "<redacted>"),
2373 )
2374 .field(
2375 "github_token_provider",
2376 &self.github_token_provider.as_ref().map(|_| "<set>"),
2377 )
2378 .field("remote_session", &self.remote_session)
2379 .field("cloud", &self.cloud)
2380 .field(
2381 "include_sub_agent_streaming_events",
2382 &self.include_sub_agent_streaming_events,
2383 )
2384 .field("commands", &self.commands)
2385 .field("feature_flags", &self.feature_flags)
2386 .field("exp_assignments", &self.exp_assignments)
2387 .field("enable_managed_settings", &self.enable_managed_settings)
2388 .field("enable_experimental_mode", &self.enable_experimental_mode)
2389 .field("managed_settings", &self.managed_settings)
2390 .field(
2391 "session_fs_provider",
2392 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2393 )
2394 .field(
2395 "permission_handler",
2396 &self.permission_handler.as_ref().map(|_| "<set>"),
2397 )
2398 .field(
2399 "elicitation_handler",
2400 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2401 )
2402 .field(
2403 "mcp_auth_handler",
2404 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2405 )
2406 .field(
2407 "user_input_handler",
2408 &self.user_input_handler.as_ref().map(|_| "<set>"),
2409 )
2410 .field(
2411 "exit_plan_mode_handler",
2412 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2413 )
2414 .field(
2415 "auto_mode_switch_handler",
2416 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2417 )
2418 .field(
2419 "hooks_handler",
2420 &self.hooks_handler.as_ref().map(|_| "<set>"),
2421 )
2422 .field(
2423 "system_message_transform",
2424 &self.system_message_transform.as_ref().map(|_| "<set>"),
2425 )
2426 .field("event_buffer_capacity", &self.event_buffer_capacity)
2427 .finish()
2428 }
2429}
2430
2431impl Default for SessionConfig {
2432 fn default() -> Self {
2438 Self {
2439 session_id: None,
2440 model: None,
2441 client_name: None,
2442 reasoning_effort: None,
2443 reasoning_summary: None,
2444 context_tier: None,
2445 streaming: None,
2446 system_message: None,
2447 ask_user_variant: None,
2448 tools: None,
2449 canvases: None,
2450 canvas_handler: None,
2451 request_canvas_renderer: None,
2452 request_extensions: None,
2453 extension_sdk_path: None,
2454 extension_info: None,
2455 canvas_provider: None,
2456 available_tools: None,
2457 excluded_tools: None,
2458 excluded_builtin_agents: None,
2459 included_builtin_skills: None,
2460 mcp_servers: None,
2461 mcp_oauth_token_storage: None,
2462 enable_config_discovery: None,
2463 skip_embedding_retrieval: None,
2464 organization_custom_instructions: None,
2465 enable_on_demand_instruction_discovery: None,
2466 enable_file_hooks: None,
2467 enable_host_git_operations: None,
2468 enable_session_store: None,
2469 enable_skills: None,
2470 embedding_cache_storage: None,
2471 enable_mcp_apps: None,
2472 github_mcp_tool_config: None,
2473 skill_directories: None,
2474 instruction_directories: None,
2475 plugin_directories: None,
2476 large_output: None,
2477 tool_search: None,
2478 disabled_skills: None,
2479 disabled_mcp_servers: None,
2480 hooks: None,
2481 custom_agents: None,
2482 default_agent: None,
2483 agent: None,
2484 infinite_sessions: None,
2485 provider: None,
2486 capi: None,
2487 providers: None,
2488 models: None,
2489 enable_session_telemetry: None,
2490 enable_citations: None,
2491 enable_file_change_tracking: None,
2492 session_limits: None,
2493 model_capabilities: None,
2494 memory: None,
2495 config_directory: None,
2496 working_directory: None,
2497 additional_directories: None,
2498 github_token: None,
2499 github_token_provider: None,
2500 remote_session: None,
2501 cloud: None,
2502 include_sub_agent_streaming_events: None,
2503 commands: None,
2504 feature_flags: None,
2505 exp_assignments: None,
2506 enable_managed_settings: None,
2507 managed_settings: None,
2508 session_fs_provider: None,
2509 permission_handler: None,
2510 elicitation_handler: None,
2511 mcp_auth_handler: None,
2512 user_input_handler: None,
2513 exit_plan_mode_handler: None,
2514 auto_mode_switch_handler: None,
2515 hooks_handler: None,
2516 permission_policy: None,
2517 system_message_transform: None,
2518 skip_custom_instructions: None,
2519 custom_agents_local_only: None,
2520 enable_experimental_mode: None,
2521 coauthor_enabled: None,
2522 manage_schedule_enabled: None,
2523 event_buffer_capacity: None,
2524 }
2525 }
2526}
2527
2528pub(crate) struct SessionConfigRuntime {
2534 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2535 pub permission_policy: Option<crate::permission::Policy>,
2536 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2537 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2538 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2539 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2540 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2541 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2542 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2543 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2544 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2545 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2546 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2547 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2548 pub commands: Option<Vec<CommandDefinition>>,
2549}
2550
2551impl SessionConfig {
2552 pub(crate) fn into_wire(
2564 mut self,
2565 session_id: Option<SessionId>,
2566 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2567 if self.github_token.is_some() && self.github_token_provider.is_some() {
2568 return Err(crate::Error::with_message(
2569 crate::ErrorKind::InvalidConfig,
2570 "github_token and github_token_provider are mutually exclusive",
2571 ));
2572 }
2573 let permission_active =
2574 self.permission_handler.is_some() || self.permission_policy.is_some();
2575 let request_user_input = self.user_input_handler.is_some();
2576 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2577 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2578 let request_elicitation = self.elicitation_handler.is_some();
2579 let hooks_flag = self.hooks_handler.is_some();
2580
2581 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2582 if let Some(tools) = self.tools.as_mut() {
2583 for tool in tools.iter_mut() {
2584 if let Some(handler) = tool.handler.take()
2585 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2586 {
2587 return Err(crate::Error::with_message(
2588 crate::ErrorKind::InvalidConfig,
2589 format!("duplicate tool handler registered for name {:?}", tool.name),
2590 ));
2591 }
2592 }
2593 }
2594
2595 let wire_commands = self.commands.as_ref().map(|cmds| {
2596 cmds.iter()
2597 .map(|c| crate::wire::CommandWireDefinition {
2598 name: c.name.clone(),
2599 description: c.description.clone().unwrap_or_default(),
2600 })
2601 .collect()
2602 });
2603 let wire_canvases = self.canvases.clone();
2604 let canvas_handler = self.canvas_handler.clone();
2605 let bearer_token_providers =
2606 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2607
2608 let wire = crate::wire::SessionCreateWire {
2609 session_id,
2610 model: self.model,
2611 client_name: self.client_name,
2612 reasoning_effort: self.reasoning_effort,
2613 reasoning_summary: self.reasoning_summary,
2614 context_tier: self.context_tier,
2615 streaming: self.streaming,
2616 system_message: self.system_message,
2617 ask_user_variant: self.ask_user_variant,
2618 tools: self.tools,
2619 canvases: wire_canvases,
2620 request_canvas_renderer: self.request_canvas_renderer,
2621 request_extensions: self.request_extensions,
2622 extension_sdk_path: self.extension_sdk_path,
2623 extension_info: self.extension_info,
2624 canvas_provider: self.canvas_provider,
2625 available_tools: self.available_tools,
2626 excluded_tools: self.excluded_tools,
2627 excluded_builtin_agents: self.excluded_builtin_agents,
2628 tool_filter_precedence: "excluded",
2629 mcp_servers: self.mcp_servers,
2630 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2631 embedding_cache_storage: self.embedding_cache_storage,
2632 env_value_mode: "direct",
2633 enable_config_discovery: self.enable_config_discovery,
2634 skip_embedding_retrieval: self.skip_embedding_retrieval,
2635 organization_custom_instructions: self.organization_custom_instructions,
2636 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2637 enable_file_hooks: self.enable_file_hooks,
2638 enable_host_git_operations: self.enable_host_git_operations,
2639 enable_session_store: self.enable_session_store,
2640 enable_skills: self.enable_skills,
2641 request_user_input,
2642 request_permission: permission_active,
2643 request_exit_plan_mode,
2644 request_auto_mode_switch,
2645 request_elicitation,
2646 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2647 github_mcp_tool_config: self.github_mcp_tool_config,
2648 hooks: hooks_flag,
2649 skill_directories: self.skill_directories,
2650 instruction_directories: self.instruction_directories,
2651 plugin_directories: self.plugin_directories,
2652 large_output: self.large_output,
2653 tool_search: self.tool_search,
2654 disabled_skills: self.disabled_skills,
2655 disabled_mcp_servers: self.disabled_mcp_servers,
2656 custom_agents: self.custom_agents,
2657 custom_agents_local_only: self.custom_agents_local_only,
2658 default_agent: self.default_agent,
2659 agent: self.agent,
2660 infinite_sessions: self.infinite_sessions,
2661 provider: self.provider,
2662 capi: self.capi,
2663 providers: self.providers,
2664 models: self.models,
2665 enable_session_telemetry: self.enable_session_telemetry,
2666 enable_citations: self.enable_citations,
2667 enable_file_change_tracking: self.enable_file_change_tracking,
2668 session_limits: self.session_limits,
2669 model_capabilities: self.model_capabilities,
2670 memory: self.memory,
2671 config_dir: self.config_directory,
2672 working_directory: self.working_directory,
2673 additional_directories: self.additional_directories,
2674 github_token: self.github_token,
2675 github_token_provider_registration_id: None,
2676 remote_session: self.remote_session,
2677 cloud: self.cloud,
2678 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2679 enable_github_telemetry_forwarding: None,
2680 commands: wire_commands,
2681 feature_flags: self.feature_flags,
2682 exp_assignments: self.exp_assignments,
2683 enable_managed_settings: self.enable_managed_settings,
2684 is_experimental_mode: self.enable_experimental_mode,
2685 managed_settings: self.managed_settings,
2686 };
2687
2688 let runtime = SessionConfigRuntime {
2689 permission_handler: self.permission_handler,
2690 permission_policy: self.permission_policy,
2691 elicitation_handler: self.elicitation_handler,
2692 mcp_auth_handler: self.mcp_auth_handler,
2693 user_input_handler: self.user_input_handler,
2694 exit_plan_mode_handler: self.exit_plan_mode_handler,
2695 auto_mode_switch_handler: self.auto_mode_switch_handler,
2696 hooks_handler: self.hooks_handler,
2697 system_message_transform: self.system_message_transform,
2698 tool_handlers,
2699 canvas_handler,
2700 session_fs_provider: self.session_fs_provider,
2701 bearer_token_providers,
2702 github_token_provider: self.github_token_provider,
2703 commands: self.commands,
2704 };
2705
2706 Ok((wire, runtime))
2707 }
2708
2709 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2713 self.permission_handler = Some(handler);
2714 self
2715 }
2716
2717 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2720 self.elicitation_handler = Some(handler);
2721 self
2722 }
2723
2724 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2726 self.mcp_auth_handler = Some(handler);
2727 self
2728 }
2729
2730 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2733 self.user_input_handler = Some(handler);
2734 self
2735 }
2736
2737 pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self {
2739 self.ask_user_variant = Some(variant);
2740 self
2741 }
2742
2743 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2745 self.exit_plan_mode_handler = Some(handler);
2746 self
2747 }
2748
2749 pub fn with_auto_mode_switch_handler(
2751 mut self,
2752 handler: Arc<dyn AutoModeSwitchHandler>,
2753 ) -> Self {
2754 self.auto_mode_switch_handler = Some(handler);
2755 self
2756 }
2757
2758 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2763 self.commands = Some(commands);
2764 self
2765 }
2766
2767 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2771 self.session_fs_provider = Some(provider);
2772 self
2773 }
2774
2775 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2778 self.hooks_handler = Some(hooks);
2779 self
2780 }
2781
2782 pub fn with_system_message_transform(
2786 mut self,
2787 transform: Arc<dyn SystemMessageTransform>,
2788 ) -> Self {
2789 self.system_message_transform = Some(transform);
2790 self
2791 }
2792
2793 pub fn approve_all_permissions(mut self) -> Self {
2799 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2800 self
2801 }
2802
2803 pub fn deny_all_permissions(mut self) -> Self {
2806 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2807 self
2808 }
2809
2810 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2815 where
2816 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2817 {
2818 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2819 self
2820 }
2821
2822 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2824 self.session_id = Some(id.into());
2825 self
2826 }
2827
2828 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2830 self.model = Some(model.into());
2831 self
2832 }
2833
2834 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2836 self.client_name = Some(name.into());
2837 self
2838 }
2839
2840 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2842 self.reasoning_effort = Some(effort.into());
2843 self
2844 }
2845
2846 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2848 self.reasoning_summary = Some(summary);
2849 self
2850 }
2851
2852 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2854 self.context_tier = Some(tier.into());
2855 self
2856 }
2857
2858 pub fn with_streaming(mut self, streaming: bool) -> Self {
2860 self.streaming = Some(streaming);
2861 self
2862 }
2863
2864 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2866 self.system_message = Some(system_message);
2867 self
2868 }
2869
2870 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2872 self.tools = Some(tools.into_iter().collect());
2873 self
2874 }
2875
2876 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2881 self.canvases = Some(canvases.into_iter().collect());
2882 self
2883 }
2884
2885 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2887 self.canvas_handler = Some(handler);
2888 self
2889 }
2890
2891 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2893 self.request_canvas_renderer = Some(request);
2894 self
2895 }
2896
2897 pub fn with_request_extensions(mut self, request: bool) -> Self {
2899 self.request_extensions = Some(request);
2900 self
2901 }
2902
2903 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2907 self.extension_sdk_path = Some(path.into());
2908 self
2909 }
2910
2911 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2913 self.extension_info = Some(extension_info);
2914 self
2915 }
2916
2917 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2920 self.canvas_provider = Some(canvas_provider);
2921 self
2922 }
2923
2924 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2926 where
2927 I: IntoIterator<Item = S>,
2928 S: Into<String>,
2929 {
2930 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2931 self
2932 }
2933
2934 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2936 where
2937 I: IntoIterator<Item = S>,
2938 S: Into<String>,
2939 {
2940 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2941 self
2942 }
2943
2944 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2946 where
2947 I: IntoIterator<Item = S>,
2948 S: Into<String>,
2949 {
2950 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2951 self
2952 }
2953
2954 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2956 self.mcp_servers = Some(servers);
2957 self
2958 }
2959
2960 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2968 self.mcp_oauth_token_storage = Some(mode.into());
2969 self
2970 }
2971
2972 pub fn with_embedding_cache_storage(
2974 mut self,
2975 embedding_cache_storage: impl Into<String>,
2976 ) -> Self {
2977 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2978 self
2979 }
2980
2981 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2984 self.enable_config_discovery = Some(enable);
2985 self
2986 }
2987
2988 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2990 self.skip_embedding_retrieval = Some(value);
2991 self
2992 }
2993
2994 pub fn with_organization_custom_instructions(
2996 mut self,
2997 instructions: impl Into<String>,
2998 ) -> Self {
2999 self.organization_custom_instructions = Some(instructions.into());
3000 self
3001 }
3002
3003 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3005 self.enable_on_demand_instruction_discovery = Some(value);
3006 self
3007 }
3008
3009 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3011 self.enable_file_hooks = Some(value);
3012 self
3013 }
3014
3015 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3017 self.enable_host_git_operations = Some(value);
3018 self
3019 }
3020
3021 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3023 self.enable_session_store = Some(value);
3024 self
3025 }
3026
3027 pub fn with_enable_skills(mut self, value: bool) -> Self {
3029 self.enable_skills = Some(value);
3030 self
3031 }
3032
3033 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3039 self.enable_mcp_apps = Some(enable);
3040 self
3041 }
3042
3043 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
3045 self.github_mcp_tool_config = Some(config);
3046 self
3047 }
3048
3049 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3051 where
3052 I: IntoIterator<Item = P>,
3053 P: Into<PathBuf>,
3054 {
3055 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3056 self
3057 }
3058
3059 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
3061 where
3062 I: IntoIterator<Item = S>,
3063 S: Into<String>,
3064 {
3065 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
3066 self
3067 }
3068
3069 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3073 where
3074 I: IntoIterator<Item = P>,
3075 P: Into<PathBuf>,
3076 {
3077 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3078 self
3079 }
3080
3081 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3083 where
3084 I: IntoIterator<Item = P>,
3085 P: Into<PathBuf>,
3086 {
3087 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3088 self
3089 }
3090
3091 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3093 self.large_output = Some(config);
3094 self
3095 }
3096
3097 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
3100 self.tool_search = Some(config);
3101 self
3102 }
3103
3104 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3106 where
3107 I: IntoIterator<Item = S>,
3108 S: Into<String>,
3109 {
3110 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3111 self
3112 }
3113
3114 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
3116 where
3117 I: IntoIterator<Item = S>,
3118 S: Into<String>,
3119 {
3120 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
3121 self
3122 }
3123
3124 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3126 mut self,
3127 agents: I,
3128 ) -> Self {
3129 self.custom_agents = Some(agents.into_iter().collect());
3130 self
3131 }
3132
3133 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3135 self.default_agent = Some(agent);
3136 self
3137 }
3138
3139 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3142 self.agent = Some(name.into());
3143 self
3144 }
3145
3146 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3149 self.infinite_sessions = Some(config);
3150 self
3151 }
3152
3153 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3155 self.provider = Some(provider);
3156 self
3157 }
3158
3159 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3161 self.capi = Some(capi);
3162 self
3163 }
3164
3165 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3171 self.providers = Some(providers);
3172 self
3173 }
3174
3175 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3181 self.models = Some(models);
3182 self
3183 }
3184
3185 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3189 self.enable_session_telemetry = Some(enable);
3190 self
3191 }
3192
3193 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3195 self.enable_citations = Some(enable);
3196 self
3197 }
3198
3199 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
3202 self.enable_file_change_tracking = Some(enable);
3203 self
3204 }
3205
3206 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3208 self.session_limits = Some(limits);
3209 self
3210 }
3211
3212 pub fn with_model_capabilities(
3214 mut self,
3215 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3216 ) -> Self {
3217 self.model_capabilities = Some(capabilities);
3218 self
3219 }
3220
3221 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3223 self.memory = Some(memory);
3224 self
3225 }
3226
3227 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3229 self.config_directory = Some(dir.into());
3230 self
3231 }
3232
3233 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3236 self.working_directory = Some(dir.into());
3237 self
3238 }
3239
3240 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
3242 where
3243 I: IntoIterator<Item = P>,
3244 P: Into<PathBuf>,
3245 {
3246 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
3247 self
3248 }
3249
3250 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3255 self.github_token = Some(token.into());
3256 self
3257 }
3258
3259 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
3265 self.github_token_provider = Some(provider);
3266 self
3267 }
3268
3269 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3272 self.include_sub_agent_streaming_events = Some(include);
3273 self
3274 }
3275
3276 pub fn with_remote_session(
3278 mut self,
3279 mode: crate::generated::api_types::RemoteSessionMode,
3280 ) -> Self {
3281 self.remote_session = Some(mode);
3282 self
3283 }
3284
3285 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
3287 self.cloud = Some(cloud);
3288 self
3289 }
3290
3291 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3293 self.skip_custom_instructions = Some(value);
3294 self
3295 }
3296
3297 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3299 self.custom_agents_local_only = Some(value);
3300 self
3301 }
3302
3303 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
3305 self.enable_experimental_mode = Some(enable_experimental_mode);
3306 self
3307 }
3308
3309 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3311 self.coauthor_enabled = Some(value);
3312 self
3313 }
3314
3315 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3317 self.manage_schedule_enabled = Some(value);
3318 self
3319 }
3320
3321 pub fn with_feature_flags(mut self, feature_flags: HashMap<String, bool>) -> Self {
3323 self.feature_flags = Some(feature_flags);
3324 self
3325 }
3326
3327 pub fn with_event_buffer_capacity(mut self, capacity: usize) -> Self {
3335 self.event_buffer_capacity = Some(capacity);
3336 self
3337 }
3338
3339 #[doc(hidden)]
3347 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
3348 self.exp_assignments = Some(assignments);
3349 self
3350 }
3351
3352 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3359 self.enable_managed_settings = Some(enabled);
3360 self
3361 }
3362
3363 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
3368 self.managed_settings = Some(managed_settings);
3369 self
3370 }
3371}
3372#[derive(Clone)]
3379#[non_exhaustive]
3380pub struct ResumeSessionConfig {
3381 pub session_id: SessionId,
3383 pub model: Option<String>,
3386 pub client_name: Option<String>,
3388 pub reasoning_effort: Option<String>,
3390 pub reasoning_summary: Option<ReasoningSummary>,
3394 pub context_tier: Option<String>,
3397 pub streaming: Option<bool>,
3399 pub system_message: Option<SystemMessageConfig>,
3402 pub ask_user_variant: Option<AskUserVariant>,
3407 pub tools: Option<Vec<Tool>>,
3409 pub canvases: Option<Vec<CanvasDeclaration>>,
3411 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
3414 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3416 pub request_canvas_renderer: Option<bool>,
3418 pub request_extensions: Option<bool>,
3420 pub extension_sdk_path: Option<String>,
3424 pub extension_info: Option<ExtensionInfo>,
3426 pub canvas_provider: Option<CanvasProviderIdentity>,
3429 pub available_tools: Option<Vec<String>>,
3431 pub excluded_tools: Option<Vec<String>>,
3433 pub excluded_builtin_agents: Option<Vec<String>>,
3439 pub included_builtin_skills: Option<Vec<String>>,
3443 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3445 pub mcp_oauth_token_storage: Option<String>,
3448 pub enable_config_discovery: Option<bool>,
3451 pub skip_embedding_retrieval: Option<bool>,
3453 pub embedding_cache_storage: Option<String>,
3455 pub organization_custom_instructions: Option<String>,
3457 pub enable_on_demand_instruction_discovery: Option<bool>,
3459 pub enable_file_hooks: Option<bool>,
3461 pub enable_host_git_operations: Option<bool>,
3463 pub enable_session_store: Option<bool>,
3465 pub enable_skills: Option<bool>,
3467 pub enable_mcp_apps: Option<bool>,
3473 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
3478 pub skill_directories: Option<Vec<PathBuf>>,
3480 pub instruction_directories: Option<Vec<PathBuf>>,
3483 pub plugin_directories: Option<Vec<PathBuf>>,
3485 pub large_output: Option<LargeToolOutputConfig>,
3487 pub tool_search: Option<ToolSearchConfig>,
3490 pub disabled_skills: Option<Vec<String>>,
3492 pub disabled_mcp_servers: Option<Vec<String>>,
3495 pub hooks: Option<bool>,
3497 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3499 pub default_agent: Option<DefaultAgentConfig>,
3501 pub agent: Option<String>,
3503 pub infinite_sessions: Option<InfiniteSessionConfig>,
3505 pub provider: Option<ProviderConfig>,
3507 pub capi: Option<CapiSessionOptions>,
3513 pub providers: Option<Vec<NamedProviderConfig>>,
3519 pub models: Option<Vec<ProviderModelConfig>>,
3525 pub enable_session_telemetry: Option<bool>,
3533 pub enable_citations: Option<bool>,
3535 pub enable_file_change_tracking: Option<bool>,
3539 pub session_limits: Option<SessionLimitsConfig>,
3541 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3543 pub memory: Option<MemoryConfiguration>,
3545 pub config_directory: Option<PathBuf>,
3547 pub working_directory: Option<PathBuf>,
3549 pub additional_directories: Option<Vec<PathBuf>>,
3552 pub github_token: Option<String>,
3555 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
3558 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3561 pub include_sub_agent_streaming_events: Option<bool>,
3563 pub commands: Option<Vec<CommandDefinition>>,
3567 pub feature_flags: Option<HashMap<String, bool>>,
3571 #[doc(hidden)]
3576 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3577 pub enable_managed_settings: Option<bool>,
3583 pub managed_settings: Option<ManagedSettings>,
3589 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3594 pub suppress_resume_event: Option<bool>,
3597 pub continue_pending_work: Option<bool>,
3605 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3608 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3611 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3613 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3616 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3619 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3622 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3624 pub(crate) permission_policy: Option<crate::permission::Policy>,
3626 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3628 pub skip_custom_instructions: Option<bool>,
3630 pub custom_agents_local_only: Option<bool>,
3632 pub enable_experimental_mode: Option<bool>,
3637 pub coauthor_enabled: Option<bool>,
3639 pub manage_schedule_enabled: Option<bool>,
3641 pub event_buffer_capacity: Option<usize>,
3643}
3644
3645impl std::fmt::Debug for ResumeSessionConfig {
3646 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3647 f.debug_struct("ResumeSessionConfig")
3648 .field("session_id", &self.session_id)
3649 .field("model", &self.model)
3650 .field("client_name", &self.client_name)
3651 .field("reasoning_effort", &self.reasoning_effort)
3652 .field("reasoning_summary", &self.reasoning_summary)
3653 .field("context_tier", &self.context_tier)
3654 .field("streaming", &self.streaming)
3655 .field("system_message", &self.system_message)
3656 .field("ask_user_variant", &self.ask_user_variant)
3657 .field("tools", &self.tools)
3658 .field("canvases", &self.canvases)
3659 .field(
3660 "canvas_handler",
3661 &self.canvas_handler.as_ref().map(|_| "<set>"),
3662 )
3663 .field("open_canvases", &self.open_canvases)
3664 .field("request_canvas_renderer", &self.request_canvas_renderer)
3665 .field("request_extensions", &self.request_extensions)
3666 .field("extension_sdk_path", &self.extension_sdk_path)
3667 .field("extension_info", &self.extension_info)
3668 .field("canvas_provider", &self.canvas_provider)
3669 .field("available_tools", &self.available_tools)
3670 .field("excluded_tools", &self.excluded_tools)
3671 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3672 .field("included_builtin_skills", &self.included_builtin_skills)
3673 .field("mcp_servers", &self.mcp_servers)
3674 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3675 .field("embedding_cache_storage", &self.embedding_cache_storage)
3676 .field("enable_config_discovery", &self.enable_config_discovery)
3677 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3678 .field(
3679 "organization_custom_instructions",
3680 &self
3681 .organization_custom_instructions
3682 .as_ref()
3683 .map(|_| "<redacted>"),
3684 )
3685 .field(
3686 "enable_on_demand_instruction_discovery",
3687 &self.enable_on_demand_instruction_discovery,
3688 )
3689 .field("enable_file_hooks", &self.enable_file_hooks)
3690 .field(
3691 "enable_host_git_operations",
3692 &self.enable_host_git_operations,
3693 )
3694 .field("enable_session_store", &self.enable_session_store)
3695 .field("enable_skills", &self.enable_skills)
3696 .field("enable_mcp_apps", &self.enable_mcp_apps)
3697 .field("skill_directories", &self.skill_directories)
3698 .field("instruction_directories", &self.instruction_directories)
3699 .field("plugin_directories", &self.plugin_directories)
3700 .field("large_output", &self.large_output)
3701 .field("tool_search", &self.tool_search)
3702 .field("disabled_skills", &self.disabled_skills)
3703 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
3704 .field("hooks", &self.hooks)
3705 .field("custom_agents", &self.custom_agents)
3706 .field("default_agent", &self.default_agent)
3707 .field("agent", &self.agent)
3708 .field("infinite_sessions", &self.infinite_sessions)
3709 .field("provider", &self.provider)
3710 .field("capi", &self.capi)
3711 .field("enable_session_telemetry", &self.enable_session_telemetry)
3712 .field("enable_citations", &self.enable_citations)
3713 .field(
3714 "enable_file_change_tracking",
3715 &self.enable_file_change_tracking,
3716 )
3717 .field("session_limits", &self.session_limits)
3718 .field("model_capabilities", &self.model_capabilities)
3719 .field("memory", &self.memory)
3720 .field("config_directory", &self.config_directory)
3721 .field("working_directory", &self.working_directory)
3722 .field("additional_directories", &self.additional_directories)
3723 .field(
3724 "github_token",
3725 &self.github_token.as_ref().map(|_| "<redacted>"),
3726 )
3727 .field(
3728 "github_token_provider",
3729 &self.github_token_provider.as_ref().map(|_| "<set>"),
3730 )
3731 .field("remote_session", &self.remote_session)
3732 .field(
3733 "include_sub_agent_streaming_events",
3734 &self.include_sub_agent_streaming_events,
3735 )
3736 .field("commands", &self.commands)
3737 .field("feature_flags", &self.feature_flags)
3738 .field("exp_assignments", &self.exp_assignments)
3739 .field("enable_managed_settings", &self.enable_managed_settings)
3740 .field("enable_experimental_mode", &self.enable_experimental_mode)
3741 .field("managed_settings", &self.managed_settings)
3742 .field(
3743 "session_fs_provider",
3744 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3745 )
3746 .field(
3747 "permission_handler",
3748 &self.permission_handler.as_ref().map(|_| "<set>"),
3749 )
3750 .field(
3751 "elicitation_handler",
3752 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3753 )
3754 .field(
3755 "user_input_handler",
3756 &self.user_input_handler.as_ref().map(|_| "<set>"),
3757 )
3758 .field(
3759 "exit_plan_mode_handler",
3760 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3761 )
3762 .field(
3763 "auto_mode_switch_handler",
3764 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3765 )
3766 .field(
3767 "hooks_handler",
3768 &self.hooks_handler.as_ref().map(|_| "<set>"),
3769 )
3770 .field(
3771 "system_message_transform",
3772 &self.system_message_transform.as_ref().map(|_| "<set>"),
3773 )
3774 .field("suppress_resume_event", &self.suppress_resume_event)
3775 .field("continue_pending_work", &self.continue_pending_work)
3776 .field("event_buffer_capacity", &self.event_buffer_capacity)
3777 .finish()
3778 }
3779}
3780
3781impl ResumeSessionConfig {
3782 pub(crate) fn into_wire(
3790 mut self,
3791 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3792 if self.github_token.is_some() && self.github_token_provider.is_some() {
3793 return Err(crate::Error::with_message(
3794 crate::ErrorKind::InvalidConfig,
3795 "github_token and github_token_provider are mutually exclusive",
3796 ));
3797 }
3798 let permission_active =
3799 self.permission_handler.is_some() || self.permission_policy.is_some();
3800 let request_user_input = self.user_input_handler.is_some();
3801 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3802 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3803 let request_elicitation = self.elicitation_handler.is_some();
3804 let hooks_flag = self.hooks_handler.is_some();
3805
3806 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3807 if let Some(tools) = self.tools.as_mut() {
3808 for tool in tools.iter_mut() {
3809 if let Some(handler) = tool.handler.take()
3810 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3811 {
3812 return Err(crate::Error::with_message(
3813 crate::ErrorKind::InvalidConfig,
3814 format!("duplicate tool handler registered for name {:?}", tool.name),
3815 ));
3816 }
3817 }
3818 }
3819
3820 let wire_commands = self.commands.as_ref().map(|cmds| {
3821 cmds.iter()
3822 .map(|c| crate::wire::CommandWireDefinition {
3823 name: c.name.clone(),
3824 description: c.description.clone().unwrap_or_default(),
3825 })
3826 .collect()
3827 });
3828 let wire_canvases = self.canvases.clone();
3829 let canvas_handler = self.canvas_handler.clone();
3830 let bearer_token_providers =
3831 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3832
3833 let wire = crate::wire::SessionResumeWire {
3834 session_id: self.session_id,
3835 model: self.model,
3836 client_name: self.client_name,
3837 reasoning_effort: self.reasoning_effort,
3838 reasoning_summary: self.reasoning_summary,
3839 context_tier: self.context_tier,
3840 streaming: self.streaming,
3841 system_message: self.system_message,
3842 ask_user_variant: self.ask_user_variant,
3843 tools: self.tools,
3844 canvases: wire_canvases,
3845 open_canvases: self.open_canvases,
3846 request_canvas_renderer: self.request_canvas_renderer,
3847 request_extensions: self.request_extensions,
3848 extension_sdk_path: self.extension_sdk_path,
3849 extension_info: self.extension_info,
3850 canvas_provider: self.canvas_provider,
3851 available_tools: self.available_tools,
3852 excluded_tools: self.excluded_tools,
3853 excluded_builtin_agents: self.excluded_builtin_agents,
3854 tool_filter_precedence: "excluded",
3855 mcp_servers: self.mcp_servers,
3856 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3857 embedding_cache_storage: self.embedding_cache_storage,
3858 env_value_mode: "direct",
3859 enable_config_discovery: self.enable_config_discovery,
3860 skip_embedding_retrieval: self.skip_embedding_retrieval,
3861 organization_custom_instructions: self.organization_custom_instructions,
3862 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3863 enable_file_hooks: self.enable_file_hooks,
3864 enable_host_git_operations: self.enable_host_git_operations,
3865 enable_session_store: self.enable_session_store,
3866 enable_skills: self.enable_skills,
3867 request_user_input,
3868 request_permission: permission_active,
3869 request_exit_plan_mode,
3870 request_auto_mode_switch,
3871 request_elicitation,
3872 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3873 github_mcp_tool_config: self.github_mcp_tool_config,
3874 hooks: hooks_flag,
3875 skill_directories: self.skill_directories,
3876 instruction_directories: self.instruction_directories,
3877 plugin_directories: self.plugin_directories,
3878 large_output: self.large_output,
3879 tool_search: self.tool_search,
3880 disabled_skills: self.disabled_skills,
3881 disabled_mcp_servers: self.disabled_mcp_servers,
3882 custom_agents: self.custom_agents,
3883 custom_agents_local_only: self.custom_agents_local_only,
3884 default_agent: self.default_agent,
3885 agent: self.agent,
3886 infinite_sessions: self.infinite_sessions,
3887 provider: self.provider,
3888 capi: self.capi,
3889 providers: self.providers,
3890 models: self.models,
3891 enable_session_telemetry: self.enable_session_telemetry,
3892 enable_citations: self.enable_citations,
3893 enable_file_change_tracking: self.enable_file_change_tracking,
3894 session_limits: self.session_limits,
3895 model_capabilities: self.model_capabilities,
3896 memory: self.memory,
3897 config_dir: self.config_directory,
3898 working_directory: self.working_directory,
3899 additional_directories: self.additional_directories,
3900 github_token: self.github_token,
3901 github_token_provider_registration_id: None,
3902 remote_session: self.remote_session,
3903 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3904 enable_github_telemetry_forwarding: None,
3905 commands: wire_commands,
3906 feature_flags: self.feature_flags,
3907 exp_assignments: self.exp_assignments,
3908 enable_managed_settings: self.enable_managed_settings,
3909 is_experimental_mode: self.enable_experimental_mode,
3910 managed_settings: self.managed_settings,
3911 suppress_resume_event: self.suppress_resume_event,
3912 continue_pending_work: self.continue_pending_work,
3913 };
3914
3915 let runtime = SessionConfigRuntime {
3916 permission_handler: self.permission_handler,
3917 permission_policy: self.permission_policy,
3918 elicitation_handler: self.elicitation_handler,
3919 mcp_auth_handler: self.mcp_auth_handler,
3920 user_input_handler: self.user_input_handler,
3921 exit_plan_mode_handler: self.exit_plan_mode_handler,
3922 auto_mode_switch_handler: self.auto_mode_switch_handler,
3923 hooks_handler: self.hooks_handler,
3924 system_message_transform: self.system_message_transform,
3925 tool_handlers,
3926 canvas_handler,
3927 session_fs_provider: self.session_fs_provider,
3928 bearer_token_providers,
3929 github_token_provider: self.github_token_provider,
3930 commands: self.commands,
3931 };
3932
3933 Ok((wire, runtime))
3934 }
3935
3936 pub fn new(session_id: SessionId) -> Self {
3941 Self {
3942 session_id,
3943 model: None,
3944 client_name: None,
3945 reasoning_effort: None,
3946 reasoning_summary: None,
3947 context_tier: None,
3948 streaming: None,
3949 system_message: None,
3950 ask_user_variant: None,
3951 tools: None,
3952 canvases: None,
3953 canvas_handler: None,
3954 open_canvases: None,
3955 request_canvas_renderer: None,
3956 request_extensions: None,
3957 extension_sdk_path: None,
3958 extension_info: None,
3959 canvas_provider: None,
3960 available_tools: None,
3961 excluded_tools: None,
3962 excluded_builtin_agents: None,
3963 included_builtin_skills: None,
3964 mcp_servers: None,
3965 mcp_oauth_token_storage: None,
3966 enable_config_discovery: None,
3967 skip_embedding_retrieval: None,
3968 organization_custom_instructions: None,
3969 enable_on_demand_instruction_discovery: None,
3970 enable_file_hooks: None,
3971 enable_host_git_operations: None,
3972 enable_session_store: None,
3973 enable_skills: None,
3974 embedding_cache_storage: None,
3975 enable_mcp_apps: None,
3976 github_mcp_tool_config: None,
3977 skill_directories: None,
3978 instruction_directories: None,
3979 plugin_directories: None,
3980 large_output: None,
3981 tool_search: None,
3982 disabled_skills: None,
3983 disabled_mcp_servers: None,
3984 hooks: None,
3985 custom_agents: None,
3986 default_agent: None,
3987 agent: None,
3988 infinite_sessions: None,
3989 provider: None,
3990 capi: None,
3991 providers: None,
3992 models: None,
3993 enable_session_telemetry: None,
3994 enable_citations: None,
3995 enable_file_change_tracking: None,
3996 session_limits: None,
3997 model_capabilities: None,
3998 memory: None,
3999 config_directory: None,
4000 working_directory: None,
4001 additional_directories: None,
4002 github_token: None,
4003 github_token_provider: None,
4004 remote_session: None,
4005 include_sub_agent_streaming_events: None,
4006 commands: None,
4007 feature_flags: None,
4008 exp_assignments: None,
4009 enable_managed_settings: None,
4010 managed_settings: None,
4011 session_fs_provider: None,
4012 suppress_resume_event: None,
4013 continue_pending_work: None,
4014 permission_handler: None,
4015 elicitation_handler: None,
4016 mcp_auth_handler: None,
4017 user_input_handler: None,
4018 exit_plan_mode_handler: None,
4019 auto_mode_switch_handler: None,
4020 hooks_handler: None,
4021 permission_policy: None,
4022 system_message_transform: None,
4023 skip_custom_instructions: None,
4024 custom_agents_local_only: None,
4025 enable_experimental_mode: None,
4026 coauthor_enabled: None,
4027 manage_schedule_enabled: None,
4028 event_buffer_capacity: None,
4029 }
4030 }
4031
4032 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
4034 self.permission_handler = Some(handler);
4035 self
4036 }
4037
4038 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
4040 self.elicitation_handler = Some(handler);
4041 self
4042 }
4043
4044 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
4046 self.mcp_auth_handler = Some(handler);
4047 self
4048 }
4049
4050 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
4052 self.user_input_handler = Some(handler);
4053 self
4054 }
4055
4056 pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self {
4058 self.ask_user_variant = Some(variant);
4059 self
4060 }
4061
4062 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
4064 self.exit_plan_mode_handler = Some(handler);
4065 self
4066 }
4067
4068 pub fn with_auto_mode_switch_handler(
4070 mut self,
4071 handler: Arc<dyn AutoModeSwitchHandler>,
4072 ) -> Self {
4073 self.auto_mode_switch_handler = Some(handler);
4074 self
4075 }
4076
4077 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
4080 self.hooks_handler = Some(hooks);
4081 self
4082 }
4083
4084 pub fn with_system_message_transform(
4086 mut self,
4087 transform: Arc<dyn SystemMessageTransform>,
4088 ) -> Self {
4089 self.system_message_transform = Some(transform);
4090 self
4091 }
4092
4093 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
4097 self.commands = Some(commands);
4098 self
4099 }
4100
4101 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
4104 self.session_fs_provider = Some(provider);
4105 self
4106 }
4107
4108 pub fn approve_all_permissions(mut self) -> Self {
4111 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
4112 self
4113 }
4114
4115 pub fn deny_all_permissions(mut self) -> Self {
4118 self.permission_policy = Some(crate::permission::Policy::DenyAll);
4119 self
4120 }
4121
4122 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
4125 where
4126 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
4127 {
4128 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
4129 self
4130 }
4131
4132 pub fn with_model(mut self, model: impl Into<String>) -> Self {
4134 self.model = Some(model.into());
4135 self
4136 }
4137
4138 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
4140 self.client_name = Some(name.into());
4141 self
4142 }
4143
4144 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4146 self.reasoning_effort = Some(effort.into());
4147 self
4148 }
4149
4150 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4152 self.reasoning_summary = Some(summary);
4153 self
4154 }
4155
4156 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
4159 self.context_tier = Some(tier.into());
4160 self
4161 }
4162
4163 pub fn with_streaming(mut self, streaming: bool) -> Self {
4165 self.streaming = Some(streaming);
4166 self
4167 }
4168
4169 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
4172 self.system_message = Some(system_message);
4173 self
4174 }
4175
4176 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
4178 self.tools = Some(tools.into_iter().collect());
4179 self
4180 }
4181
4182 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
4184 self.canvases = Some(canvases.into_iter().collect());
4185 self
4186 }
4187
4188 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
4190 self.canvas_handler = Some(handler);
4191 self
4192 }
4193
4194 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
4196 mut self,
4197 open_canvases: I,
4198 ) -> Self {
4199 self.open_canvases = Some(open_canvases.into_iter().collect());
4200 self
4201 }
4202
4203 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
4205 self.request_canvas_renderer = Some(request);
4206 self
4207 }
4208
4209 pub fn with_request_extensions(mut self, request: bool) -> Self {
4211 self.request_extensions = Some(request);
4212 self
4213 }
4214
4215 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
4219 self.extension_sdk_path = Some(path.into());
4220 self
4221 }
4222
4223 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
4225 self.extension_info = Some(extension_info);
4226 self
4227 }
4228
4229 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
4232 self.canvas_provider = Some(canvas_provider);
4233 self
4234 }
4235
4236 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
4238 where
4239 I: IntoIterator<Item = S>,
4240 S: Into<String>,
4241 {
4242 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
4243 self
4244 }
4245
4246 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
4248 where
4249 I: IntoIterator<Item = S>,
4250 S: Into<String>,
4251 {
4252 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
4253 self
4254 }
4255
4256 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
4258 where
4259 I: IntoIterator<Item = S>,
4260 S: Into<String>,
4261 {
4262 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
4263 self
4264 }
4265
4266 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
4268 self.mcp_servers = Some(servers);
4269 self
4270 }
4271
4272 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
4275 self.mcp_oauth_token_storage = Some(mode.into());
4276 self
4277 }
4278
4279 pub fn with_embedding_cache_storage(
4281 mut self,
4282 embedding_cache_storage: impl Into<String>,
4283 ) -> Self {
4284 self.embedding_cache_storage = Some(embedding_cache_storage.into());
4285 self
4286 }
4287
4288 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
4291 self.enable_config_discovery = Some(enable);
4292 self
4293 }
4294
4295 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
4297 self.skip_embedding_retrieval = Some(value);
4298 self
4299 }
4300
4301 pub fn with_organization_custom_instructions(
4303 mut self,
4304 instructions: impl Into<String>,
4305 ) -> Self {
4306 self.organization_custom_instructions = Some(instructions.into());
4307 self
4308 }
4309
4310 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
4312 self.enable_on_demand_instruction_discovery = Some(value);
4313 self
4314 }
4315
4316 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
4318 self.enable_file_hooks = Some(value);
4319 self
4320 }
4321
4322 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
4324 self.enable_host_git_operations = Some(value);
4325 self
4326 }
4327
4328 pub fn with_enable_session_store(mut self, value: bool) -> Self {
4330 self.enable_session_store = Some(value);
4331 self
4332 }
4333
4334 pub fn with_enable_skills(mut self, value: bool) -> Self {
4336 self.enable_skills = Some(value);
4337 self
4338 }
4339
4340 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
4346 self.enable_mcp_apps = Some(enable);
4347 self
4348 }
4349
4350 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
4352 self.github_mcp_tool_config = Some(config);
4353 self
4354 }
4355
4356 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
4358 where
4359 I: IntoIterator<Item = P>,
4360 P: Into<PathBuf>,
4361 {
4362 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
4363 self
4364 }
4365
4366 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
4368 where
4369 I: IntoIterator<Item = S>,
4370 S: Into<String>,
4371 {
4372 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
4373 self
4374 }
4375
4376 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
4380 where
4381 I: IntoIterator<Item = P>,
4382 P: Into<PathBuf>,
4383 {
4384 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
4385 self
4386 }
4387
4388 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
4390 where
4391 I: IntoIterator<Item = P>,
4392 P: Into<PathBuf>,
4393 {
4394 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
4395 self
4396 }
4397
4398 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
4400 self.large_output = Some(config);
4401 self
4402 }
4403
4404 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
4407 self.tool_search = Some(config);
4408 self
4409 }
4410
4411 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
4413 where
4414 I: IntoIterator<Item = S>,
4415 S: Into<String>,
4416 {
4417 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
4418 self
4419 }
4420
4421 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
4423 where
4424 I: IntoIterator<Item = S>,
4425 S: Into<String>,
4426 {
4427 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
4428 self
4429 }
4430
4431 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
4433 mut self,
4434 agents: I,
4435 ) -> Self {
4436 self.custom_agents = Some(agents.into_iter().collect());
4437 self
4438 }
4439
4440 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
4442 self.default_agent = Some(agent);
4443 self
4444 }
4445
4446 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
4448 self.agent = Some(name.into());
4449 self
4450 }
4451
4452 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
4454 self.infinite_sessions = Some(config);
4455 self
4456 }
4457
4458 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
4460 self.provider = Some(provider);
4461 self
4462 }
4463
4464 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
4466 self.capi = Some(capi);
4467 self
4468 }
4469
4470 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
4476 self.providers = Some(providers);
4477 self
4478 }
4479
4480 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
4486 self.models = Some(models);
4487 self
4488 }
4489
4490 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
4494 self.enable_session_telemetry = Some(enable);
4495 self
4496 }
4497
4498 pub fn with_enable_citations(mut self, enable: bool) -> Self {
4500 self.enable_citations = Some(enable);
4501 self
4502 }
4503
4504 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
4507 self.enable_file_change_tracking = Some(enable);
4508 self
4509 }
4510
4511 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
4513 self.session_limits = Some(limits);
4514 self
4515 }
4516
4517 pub fn with_model_capabilities(
4519 mut self,
4520 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
4521 ) -> Self {
4522 self.model_capabilities = Some(capabilities);
4523 self
4524 }
4525
4526 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
4528 self.memory = Some(memory);
4529 self
4530 }
4531
4532 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4534 self.config_directory = Some(dir.into());
4535 self
4536 }
4537
4538 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4540 self.working_directory = Some(dir.into());
4541 self
4542 }
4543
4544 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
4546 where
4547 I: IntoIterator<Item = P>,
4548 P: Into<PathBuf>,
4549 {
4550 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
4551 self
4552 }
4553
4554 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
4558 self.github_token = Some(token.into());
4559 self
4560 }
4561
4562 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
4568 self.github_token_provider = Some(provider);
4569 self
4570 }
4571
4572 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4574 self.include_sub_agent_streaming_events = Some(include);
4575 self
4576 }
4577
4578 pub fn with_remote_session(
4580 mut self,
4581 mode: crate::generated::api_types::RemoteSessionMode,
4582 ) -> Self {
4583 self.remote_session = Some(mode);
4584 self
4585 }
4586
4587 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4590 self.suppress_resume_event = Some(suppress);
4591 self
4592 }
4593
4594 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4600 self.continue_pending_work = Some(continue_pending);
4601 self
4602 }
4603
4604 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4606 self.skip_custom_instructions = Some(value);
4607 self
4608 }
4609
4610 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4612 self.custom_agents_local_only = Some(value);
4613 self
4614 }
4615
4616 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
4618 self.enable_experimental_mode = Some(enable_experimental_mode);
4619 self
4620 }
4621
4622 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4624 self.coauthor_enabled = Some(value);
4625 self
4626 }
4627
4628 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4630 self.manage_schedule_enabled = Some(value);
4631 self
4632 }
4633
4634 pub fn with_feature_flags(mut self, feature_flags: HashMap<String, bool>) -> Self {
4636 self.feature_flags = Some(feature_flags);
4637 self
4638 }
4639
4640 pub fn with_event_buffer_capacity(mut self, capacity: usize) -> Self {
4648 self.event_buffer_capacity = Some(capacity);
4649 self
4650 }
4651
4652 #[doc(hidden)]
4656 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4657 self.exp_assignments = Some(assignments);
4658 self
4659 }
4660
4661 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4664 self.enable_managed_settings = Some(enabled);
4665 self
4666 }
4667
4668 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
4672 self.managed_settings = Some(managed_settings);
4673 self
4674 }
4675}
4676
4677#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4683#[serde(rename_all = "camelCase")]
4684#[non_exhaustive]
4685pub struct SystemMessageConfig {
4686 #[serde(skip_serializing_if = "Option::is_none")]
4688 pub mode: Option<String>,
4689 #[serde(skip_serializing_if = "Option::is_none")]
4691 pub content: Option<String>,
4692 #[serde(skip_serializing_if = "Option::is_none")]
4694 pub sections: Option<HashMap<String, SectionOverride>>,
4695}
4696
4697impl SystemMessageConfig {
4698 pub fn new() -> Self {
4701 Self::default()
4702 }
4703
4704 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4707 self.mode = Some(mode.into());
4708 self
4709 }
4710
4711 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4714 self.content = Some(content.into());
4715 self
4716 }
4717
4718 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4720 self.sections = Some(sections);
4721 self
4722 }
4723}
4724
4725#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4731#[serde(rename_all = "camelCase")]
4732pub struct SectionOverride {
4733 #[serde(skip_serializing_if = "Option::is_none")]
4736 pub action: Option<String>,
4737 #[serde(skip_serializing_if = "Option::is_none")]
4739 pub content: Option<String>,
4740}
4741
4742#[derive(Debug, Clone, Serialize, Deserialize)]
4744#[serde(rename_all = "camelCase")]
4745pub struct CreateSessionResult {
4746 pub session_id: SessionId,
4748 #[serde(skip_serializing_if = "Option::is_none")]
4750 pub workspace_path: Option<PathBuf>,
4751 #[serde(default, alias = "remote_url")]
4753 pub remote_url: Option<String>,
4754 #[serde(skip_serializing_if = "Option::is_none")]
4756 pub capabilities: Option<SessionCapabilities>,
4757}
4758
4759#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4761#[serde(rename_all = "camelCase")]
4762pub(crate) struct ResumeSessionResult {
4763 #[serde(default)]
4765 pub session_id: Option<SessionId>,
4766 #[serde(default, skip_serializing_if = "Option::is_none")]
4768 pub workspace_path: Option<PathBuf>,
4769 #[serde(default, alias = "remote_url")]
4771 pub remote_url: Option<String>,
4772 #[serde(default, skip_serializing_if = "Option::is_none")]
4774 pub capabilities: Option<SessionCapabilities>,
4775 #[serde(
4777 default,
4778 alias = "openCanvasInstances",
4779 skip_serializing_if = "Option::is_none"
4780 )]
4781 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4782}
4783
4784#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4786#[serde(rename_all = "lowercase")]
4787pub enum LogLevel {
4788 #[default]
4790 Info,
4791 Warning,
4793 Error,
4795}
4796
4797#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4802#[serde(rename_all = "camelCase")]
4803pub struct LogOptions {
4804 #[serde(skip_serializing_if = "Option::is_none")]
4806 pub level: Option<LogLevel>,
4807 #[serde(skip_serializing_if = "Option::is_none")]
4810 pub ephemeral: Option<bool>,
4811}
4812
4813impl LogOptions {
4814 pub fn with_level(mut self, level: LogLevel) -> Self {
4816 self.level = Some(level);
4817 self
4818 }
4819
4820 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4822 self.ephemeral = Some(ephemeral);
4823 self
4824 }
4825}
4826
4827#[derive(Debug, Clone, Default)]
4831pub struct SetModelOptions {
4832 pub reasoning_effort: Option<String>,
4835 pub reasoning_summary: Option<ReasoningSummary>,
4839 pub context_tier: Option<ContextTier>,
4842 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4846 pub auto_tier: Option<AutoTierPreference>,
4854}
4855
4856#[derive(Debug, Clone, PartialEq, Eq)]
4865pub enum AutoTierPreference {
4866 Tier(AutoTier),
4868 Reset,
4870}
4871
4872impl SetModelOptions {
4873 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4875 self.reasoning_effort = Some(effort.into());
4876 self
4877 }
4878
4879 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4881 self.reasoning_summary = Some(summary);
4882 self
4883 }
4884
4885 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4887 self.context_tier = Some(tier);
4888 self
4889 }
4890
4891 pub fn with_model_capabilities(
4893 mut self,
4894 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4895 ) -> Self {
4896 self.model_capabilities = Some(caps);
4897 self
4898 }
4899
4900 pub fn with_auto_tier(mut self, tier: AutoTier) -> Self {
4902 self.auto_tier = Some(AutoTierPreference::Tier(tier));
4903 self
4904 }
4905
4906 pub fn with_reset_auto_tier(mut self) -> Self {
4909 self.auto_tier = Some(AutoTierPreference::Reset);
4910 self
4911 }
4912}
4913
4914#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4921#[serde(rename_all = "camelCase")]
4922pub struct PingResponse {
4923 #[serde(default)]
4925 pub message: String,
4926 #[serde(default)]
4928 pub timestamp: String,
4929 #[serde(skip_serializing_if = "Option::is_none")]
4931 pub protocol_version: Option<u32>,
4932}
4933
4934#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4936#[serde(rename_all = "camelCase")]
4937pub struct AttachmentLineRange {
4938 pub start: u32,
4940 pub end: u32,
4942}
4943
4944#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4946#[serde(rename_all = "camelCase")]
4947pub struct AttachmentSelectionPosition {
4948 pub line: u32,
4950 pub character: u32,
4952}
4953
4954#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4956#[serde(rename_all = "camelCase")]
4957pub struct AttachmentSelectionRange {
4958 pub start: AttachmentSelectionPosition,
4960 pub end: AttachmentSelectionPosition,
4962}
4963
4964#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4966#[serde(rename_all = "snake_case")]
4967#[non_exhaustive]
4968pub enum GitHubReferenceType {
4969 Issue,
4971 Pr,
4973 Discussion,
4975}
4976
4977#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4983#[serde(rename_all = "camelCase")]
4984pub struct GitHubRepoPointer {
4985 #[serde(skip_serializing_if = "Option::is_none")]
4987 pub id: Option<i64>,
4988 pub name: String,
4990 pub owner: String,
4992}
4993
4994#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4996#[serde(rename_all = "camelCase")]
4997pub struct GitHubFileDiffSide {
4998 pub path: String,
5000 pub r#ref: String,
5002 pub repo: GitHubRepoPointer,
5004}
5005
5006#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5008#[serde(rename_all = "camelCase")]
5009pub struct GitHubTreeComparisonSide {
5010 pub repo: GitHubRepoPointer,
5012 pub revision: String,
5014}
5015
5016#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5018#[serde(rename_all = "camelCase")]
5019pub struct GitHubSnippetLineRange {
5020 pub start: i64,
5022 pub end: i64,
5024}
5025
5026#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5028#[serde(
5029 tag = "type",
5030 rename_all = "camelCase",
5031 rename_all_fields = "camelCase"
5032)]
5033#[non_exhaustive]
5034pub enum Attachment {
5035 File {
5037 path: PathBuf,
5039 #[serde(skip_serializing_if = "Option::is_none")]
5041 display_name: Option<String>,
5042 #[serde(skip_serializing_if = "Option::is_none")]
5044 line_range: Option<AttachmentLineRange>,
5045 },
5046 Directory {
5048 path: PathBuf,
5050 #[serde(skip_serializing_if = "Option::is_none")]
5052 display_name: Option<String>,
5053 },
5054 Selection {
5056 file_path: PathBuf,
5058 text: String,
5060 #[serde(skip_serializing_if = "Option::is_none")]
5062 display_name: Option<String>,
5063 selection: AttachmentSelectionRange,
5065 },
5066 Blob {
5068 data: String,
5070 mime_type: String,
5072 #[serde(skip_serializing_if = "Option::is_none")]
5074 display_name: Option<String>,
5075 },
5076 #[serde(rename = "github_reference")]
5078 GitHubReference {
5079 number: u64,
5081 title: String,
5083 reference_type: GitHubReferenceType,
5085 state: String,
5087 url: String,
5089 },
5090 #[serde(rename = "github_commit")]
5092 GitHubCommit {
5093 message: String,
5095 oid: String,
5097 repo: GitHubRepoPointer,
5099 url: String,
5101 },
5102 #[serde(rename = "github_release")]
5104 GitHubRelease {
5105 name: String,
5107 repo: GitHubRepoPointer,
5109 tag_name: String,
5111 url: String,
5113 },
5114 #[serde(rename = "github_actions_job")]
5116 GitHubActionsJob {
5117 #[serde(skip_serializing_if = "Option::is_none")]
5120 conclusion: Option<String>,
5121 job_id: i64,
5123 job_name: String,
5125 repo: GitHubRepoPointer,
5127 url: String,
5129 workflow_name: String,
5131 },
5132 #[serde(rename = "github_repository")]
5134 GitHubRepository {
5135 #[serde(skip_serializing_if = "Option::is_none")]
5137 description: Option<String>,
5138 #[serde(skip_serializing_if = "Option::is_none")]
5141 r#ref: Option<String>,
5142 repo: GitHubRepoPointer,
5144 url: String,
5146 },
5147 #[serde(rename = "github_file_diff")]
5149 GitHubFileDiff {
5150 #[serde(skip_serializing_if = "Option::is_none")]
5152 base: Option<GitHubFileDiffSide>,
5153 #[serde(skip_serializing_if = "Option::is_none")]
5155 head: Option<GitHubFileDiffSide>,
5156 url: String,
5158 },
5159 #[serde(rename = "github_tree_comparison")]
5161 GitHubTreeComparison {
5162 base: GitHubTreeComparisonSide,
5164 head: GitHubTreeComparisonSide,
5166 url: String,
5168 },
5169 #[serde(rename = "github_url")]
5171 GitHubUrl {
5172 url: String,
5174 },
5175 #[serde(rename = "github_file")]
5177 GitHubFile {
5178 path: String,
5180 r#ref: String,
5182 repo: GitHubRepoPointer,
5184 url: String,
5186 },
5187 #[serde(rename = "github_snippet")]
5189 GitHubSnippet {
5190 line_range: GitHubSnippetLineRange,
5192 path: String,
5194 r#ref: String,
5196 repo: GitHubRepoPointer,
5198 url: String,
5200 },
5201}
5202
5203impl Attachment {
5204 pub fn display_name(&self) -> Option<&str> {
5206 match self {
5207 Self::File { display_name, .. }
5208 | Self::Directory { display_name, .. }
5209 | Self::Selection { display_name, .. }
5210 | Self::Blob { display_name, .. } => display_name.as_deref(),
5211 Self::GitHubReference { .. }
5212 | Self::GitHubCommit { .. }
5213 | Self::GitHubRelease { .. }
5214 | Self::GitHubActionsJob { .. }
5215 | Self::GitHubRepository { .. }
5216 | Self::GitHubFileDiff { .. }
5217 | Self::GitHubTreeComparison { .. }
5218 | Self::GitHubUrl { .. }
5219 | Self::GitHubFile { .. }
5220 | Self::GitHubSnippet { .. } => None,
5221 }
5222 }
5223
5224 pub fn label(&self) -> Option<String> {
5226 if let Some(display_name) = self
5227 .display_name()
5228 .map(str::trim)
5229 .filter(|name| !name.is_empty())
5230 {
5231 return Some(display_name.to_string());
5232 }
5233
5234 match self {
5235 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
5236 format!("#{}", number)
5237 } else {
5238 title.trim().to_string()
5239 }),
5240 _ => self.derived_display_name(),
5241 }
5242 }
5243
5244 pub fn ensure_display_name(&mut self) {
5246 if self
5247 .display_name()
5248 .map(str::trim)
5249 .is_some_and(|name| !name.is_empty())
5250 {
5251 return;
5252 }
5253
5254 let Some(derived_display_name) = self.derived_display_name() else {
5255 return;
5256 };
5257
5258 match self {
5259 Self::File { display_name, .. }
5260 | Self::Directory { display_name, .. }
5261 | Self::Selection { display_name, .. }
5262 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
5263 Self::GitHubReference { .. }
5264 | Self::GitHubCommit { .. }
5265 | Self::GitHubRelease { .. }
5266 | Self::GitHubActionsJob { .. }
5267 | Self::GitHubRepository { .. }
5268 | Self::GitHubFileDiff { .. }
5269 | Self::GitHubTreeComparison { .. }
5270 | Self::GitHubUrl { .. }
5271 | Self::GitHubFile { .. }
5272 | Self::GitHubSnippet { .. } => {}
5273 }
5274 }
5275
5276 fn derived_display_name(&self) -> Option<String> {
5277 match self {
5278 Self::File { path, .. } | Self::Directory { path, .. } => {
5279 Some(attachment_name_from_path(path))
5280 }
5281 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
5282 Self::Blob { .. } => Some("attachment".to_string()),
5283 Self::GitHubReference { .. }
5284 | Self::GitHubCommit { .. }
5285 | Self::GitHubRelease { .. }
5286 | Self::GitHubActionsJob { .. }
5287 | Self::GitHubRepository { .. }
5288 | Self::GitHubFileDiff { .. }
5289 | Self::GitHubTreeComparison { .. }
5290 | Self::GitHubUrl { .. }
5291 | Self::GitHubFile { .. }
5292 | Self::GitHubSnippet { .. } => None,
5293 }
5294 }
5295}
5296
5297fn attachment_name_from_path(path: &Path) -> String {
5298 path.file_name()
5299 .map(|name| name.to_string_lossy().into_owned())
5300 .filter(|name| !name.is_empty())
5301 .unwrap_or_else(|| {
5302 let full = path.to_string_lossy();
5303 if full.is_empty() {
5304 "attachment".to_string()
5305 } else {
5306 full.into_owned()
5307 }
5308 })
5309}
5310
5311pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
5313 for attachment in attachments {
5314 attachment.ensure_display_name();
5315 }
5316}
5317
5318#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5323#[serde(rename_all = "lowercase")]
5324#[non_exhaustive]
5325pub enum DeliveryMode {
5326 Enqueue,
5328 Immediate,
5330}
5331
5332#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5337#[serde(rename_all = "lowercase")]
5338#[non_exhaustive]
5339pub enum AgentMode {
5340 Interactive,
5342 Plan,
5344 Autopilot,
5346 Shell,
5348}
5349
5350#[derive(Debug, Clone)]
5379#[non_exhaustive]
5380pub struct MessageOptions {
5381 pub prompt: String,
5383 pub mode: Option<DeliveryMode>,
5389 pub agent_mode: Option<AgentMode>,
5393 pub attachments: Option<Vec<Attachment>>,
5395 pub wait_timeout: Option<Duration>,
5398 pub request_headers: Option<HashMap<String, String>>,
5402 pub traceparent: Option<String>,
5409 pub tracestate: Option<String>,
5413 pub display_prompt: Option<String>,
5415}
5416
5417impl MessageOptions {
5418 pub fn new(prompt: impl Into<String>) -> Self {
5420 Self {
5421 prompt: prompt.into(),
5422 mode: None,
5423 agent_mode: None,
5424 attachments: None,
5425 wait_timeout: None,
5426 request_headers: None,
5427 traceparent: None,
5428 tracestate: None,
5429 display_prompt: None,
5430 }
5431 }
5432
5433 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
5439 self.mode = Some(mode);
5440 self
5441 }
5442
5443 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
5447 self.agent_mode = Some(agent_mode);
5448 self
5449 }
5450
5451 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
5453 self.attachments = Some(attachments);
5454 self
5455 }
5456
5457 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
5459 self.wait_timeout = Some(timeout);
5460 self
5461 }
5462
5463 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
5465 self.request_headers = Some(headers);
5466 self
5467 }
5468
5469 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
5474 self.traceparent = ctx.traceparent;
5475 self.tracestate = ctx.tracestate;
5476 self
5477 }
5478
5479 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5481 self.traceparent = Some(traceparent.into());
5482 self
5483 }
5484
5485 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5487 self.tracestate = Some(tracestate.into());
5488 self
5489 }
5490
5491 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5493 self.display_prompt = Some(display_prompt.into());
5494 self
5495 }
5496}
5497
5498impl From<&str> for MessageOptions {
5499 fn from(prompt: &str) -> Self {
5500 Self::new(prompt)
5501 }
5502}
5503
5504impl From<String> for MessageOptions {
5505 fn from(prompt: String) -> Self {
5506 Self::new(prompt)
5507 }
5508}
5509
5510impl From<&String> for MessageOptions {
5511 fn from(prompt: &String) -> Self {
5512 Self::new(prompt.clone())
5513 }
5514}
5515
5516#[derive(Debug, Clone, Serialize, Deserialize)]
5518#[serde(rename_all = "camelCase")]
5519#[non_exhaustive]
5520pub struct GetStatusResponse {
5521 pub version: String,
5523 pub protocol_version: u32,
5525}
5526
5527#[derive(Debug, Clone, Serialize, Deserialize)]
5529#[serde(rename_all = "camelCase")]
5530#[non_exhaustive]
5531pub struct GetAuthStatusResponse {
5532 pub is_authenticated: bool,
5534 #[serde(skip_serializing_if = "Option::is_none")]
5537 pub auth_type: Option<String>,
5538 #[serde(skip_serializing_if = "Option::is_none")]
5540 pub host: Option<String>,
5541 #[serde(skip_serializing_if = "Option::is_none")]
5543 pub login: Option<String>,
5544 #[serde(skip_serializing_if = "Option::is_none")]
5546 pub status_message: Option<String>,
5547}
5548
5549#[derive(Debug, Clone, Serialize, Deserialize)]
5553#[serde(rename_all = "camelCase")]
5554pub struct SessionEventNotification {
5555 pub session_id: SessionId,
5557 pub event: SessionEvent,
5559}
5560
5561#[derive(Debug, Clone, Serialize, Deserialize)]
5568#[serde(rename_all = "camelCase")]
5569pub struct SessionEvent {
5570 pub id: String,
5572 pub timestamp: String,
5574 pub parent_id: Option<String>,
5576 #[serde(skip_serializing_if = "Option::is_none")]
5578 pub ephemeral: Option<bool>,
5579 #[serde(skip_serializing_if = "Option::is_none")]
5582 pub agent_id: Option<String>,
5583 #[serde(skip_serializing_if = "Option::is_none")]
5585 pub debug_cli_received_at_ms: Option<i64>,
5586 #[serde(skip_serializing_if = "Option::is_none")]
5588 pub debug_ws_forwarded_at_ms: Option<i64>,
5589 #[serde(rename = "type")]
5591 pub event_type: String,
5592 pub data: Value,
5594}
5595
5596impl SessionEvent {
5597 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5602 use serde::de::IntoDeserializer;
5603 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5604 self.event_type.as_str().into_deserializer();
5605 crate::generated::SessionEventType::deserialize(deserializer)
5606 .unwrap_or(crate::generated::SessionEventType::Unknown)
5607 }
5608
5609 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5615 serde_json::from_value(self.data.clone()).ok()
5616 }
5617
5618 pub fn is_transient_error(&self) -> bool {
5622 self.event_type == "session.error"
5623 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5624 }
5625}
5626
5627#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5632#[serde(rename_all = "camelCase")]
5633#[non_exhaustive]
5634pub struct ToolInvocation {
5635 pub session_id: SessionId,
5637 pub tool_call_id: String,
5639 pub tool_name: String,
5641 pub arguments: Value,
5643 #[serde(skip)]
5651 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5652 #[serde(default, skip_serializing_if = "Option::is_none")]
5657 pub traceparent: Option<String>,
5658 #[serde(default, skip_serializing_if = "Option::is_none")]
5661 pub tracestate: Option<String>,
5662}
5663
5664impl ToolInvocation {
5665 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5686 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5687 }
5688
5689 pub fn trace_context(&self) -> TraceContext {
5692 TraceContext {
5693 traceparent: self.traceparent.clone(),
5694 tracestate: self.tracestate.clone(),
5695 }
5696 }
5697}
5698
5699#[derive(Debug, Clone, Serialize, Deserialize)]
5701#[serde(rename_all = "camelCase")]
5702pub struct ToolBinaryResult {
5703 pub data: String,
5705 pub mime_type: String,
5707 pub r#type: String,
5709 #[serde(default, skip_serializing_if = "Option::is_none")]
5711 pub description: Option<String>,
5712}
5713
5714#[derive(Debug, Clone, Serialize, Deserialize)]
5721#[serde(rename_all = "camelCase")]
5722#[non_exhaustive]
5723pub struct ToolResultExpanded {
5724 pub text_result_for_llm: String,
5726 pub result_type: String,
5728 #[serde(default, skip_serializing_if = "Option::is_none")]
5730 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5731 #[serde(skip_serializing_if = "Option::is_none")]
5733 pub session_log: Option<String>,
5734 #[serde(skip_serializing_if = "Option::is_none")]
5736 pub error: Option<String>,
5737 #[serde(default, skip_serializing_if = "Option::is_none")]
5739 pub tool_telemetry: Option<HashMap<String, Value>>,
5740 #[serde(default, skip_serializing_if = "Option::is_none")]
5742 pub tool_references: Option<Vec<String>>,
5743}
5744
5745impl ToolResultExpanded {
5746 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5750 Self {
5751 text_result_for_llm: text_result_for_llm.into(),
5752 result_type: result_type.into(),
5753 binary_results_for_llm: None,
5754 session_log: None,
5755 error: None,
5756 tool_telemetry: None,
5757 tool_references: None,
5758 }
5759 }
5760
5761 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5763 self.binary_results_for_llm = Some(results);
5764 self
5765 }
5766
5767 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5769 self.session_log = Some(session_log.into());
5770 self
5771 }
5772
5773 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5775 self.error = Some(error.into());
5776 self
5777 }
5778
5779 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5781 self.tool_telemetry = Some(telemetry);
5782 self
5783 }
5784
5785 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5787 where
5788 I: IntoIterator<Item = S>,
5789 S: Into<String>,
5790 {
5791 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5792 self
5793 }
5794}
5795
5796#[derive(Debug, Clone, Serialize, Deserialize)]
5798#[serde(untagged)]
5799#[non_exhaustive]
5800pub enum ToolResult {
5801 Text(String),
5803 Expanded(ToolResultExpanded),
5805}
5806
5807#[derive(Debug, Clone, Serialize, Deserialize)]
5809#[serde(rename_all = "camelCase")]
5810pub struct ToolResultResponse {
5811 pub result: ToolResult,
5813}
5814
5815#[derive(Debug, Clone, Serialize, Deserialize)]
5817#[serde(rename_all = "camelCase")]
5818pub struct SessionMetadata {
5819 pub session_id: SessionId,
5821 pub start_time: String,
5823 pub modified_time: String,
5825 #[serde(skip_serializing_if = "Option::is_none")]
5827 pub summary: Option<String>,
5828 pub is_remote: bool,
5830}
5831
5832#[derive(Debug, Clone, Serialize, Deserialize)]
5834#[serde(rename_all = "camelCase")]
5835pub struct ListSessionsResponse {
5836 pub sessions: Vec<SessionMetadata>,
5838}
5839
5840#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5844#[serde(rename_all = "camelCase")]
5845pub struct SessionListFilter {
5846 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5848 pub working_directory: Option<String>,
5849 #[serde(default, skip_serializing_if = "Option::is_none")]
5851 pub git_root: Option<String>,
5852 #[serde(default, skip_serializing_if = "Option::is_none")]
5854 pub repository: Option<String>,
5855 #[serde(default, skip_serializing_if = "Option::is_none")]
5857 pub branch: Option<String>,
5858}
5859
5860#[derive(Debug, Clone, Serialize, Deserialize)]
5862#[serde(rename_all = "camelCase")]
5863pub struct GetSessionMetadataResponse {
5864 #[serde(skip_serializing_if = "Option::is_none")]
5866 pub session: Option<SessionMetadata>,
5867}
5868
5869#[derive(Debug, Clone, Serialize, Deserialize)]
5871#[serde(rename_all = "camelCase")]
5872pub struct GetLastSessionIdResponse {
5873 #[serde(skip_serializing_if = "Option::is_none")]
5875 pub session_id: Option<SessionId>,
5876}
5877
5878#[derive(Debug, Clone, Serialize, Deserialize)]
5880#[serde(rename_all = "camelCase")]
5881pub struct GetForegroundSessionResponse {
5882 #[serde(skip_serializing_if = "Option::is_none")]
5884 pub session_id: Option<SessionId>,
5885}
5886
5887#[derive(Debug, Clone, Serialize, Deserialize)]
5889#[serde(rename_all = "camelCase")]
5890pub struct GetMessagesResponse {
5891 pub events: Vec<SessionEvent>,
5893}
5894
5895#[derive(Debug, Clone, Serialize, Deserialize)]
5897#[serde(rename_all = "camelCase")]
5898pub struct ElicitationResult {
5899 pub action: String,
5901 #[serde(skip_serializing_if = "Option::is_none")]
5903 pub content: Option<Value>,
5904}
5905
5906#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5912#[serde(rename_all = "camelCase")]
5913#[non_exhaustive]
5914pub enum ElicitationMode {
5915 Form,
5917 Url,
5919 #[serde(other)]
5921 Unknown,
5922}
5923
5924#[derive(Debug, Clone, Serialize, Deserialize)]
5931#[serde(rename_all = "camelCase")]
5932pub struct ElicitationRequest {
5933 pub message: String,
5935 #[serde(skip_serializing_if = "Option::is_none")]
5937 pub requested_schema: Option<Value>,
5938 #[serde(skip_serializing_if = "Option::is_none")]
5940 pub mode: Option<ElicitationMode>,
5941 #[serde(skip_serializing_if = "Option::is_none")]
5943 pub elicitation_source: Option<String>,
5944 #[serde(skip_serializing_if = "Option::is_none")]
5946 pub url: Option<String>,
5947}
5948
5949#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5954#[serde(rename_all = "camelCase")]
5955pub struct SessionCapabilities {
5956 #[serde(skip_serializing_if = "Option::is_none")]
5958 pub ui: Option<UiCapabilities>,
5959}
5960
5961#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5963#[serde(rename_all = "camelCase")]
5964pub struct UiCapabilities {
5965 #[serde(skip_serializing_if = "Option::is_none")]
5967 pub elicitation: Option<bool>,
5968 #[serde(skip_serializing_if = "Option::is_none")]
5979 pub mcp_apps: Option<bool>,
5980 #[serde(skip_serializing_if = "Option::is_none")]
5982 pub canvases: Option<bool>,
5983}
5984
5985#[derive(Debug, Clone, Default)]
5987pub struct UiInputOptions<'a> {
5988 pub title: Option<&'a str>,
5990 pub description: Option<&'a str>,
5992 pub min_length: Option<u64>,
5994 pub max_length: Option<u64>,
5996 pub format: Option<InputFormat>,
5998 pub default: Option<&'a str>,
6000}
6001
6002#[derive(Debug, Clone, Copy)]
6004#[non_exhaustive]
6005pub enum InputFormat {
6006 Email,
6008 Uri,
6010 Date,
6012 DateTime,
6014}
6015
6016impl InputFormat {
6017 pub fn as_str(&self) -> &'static str {
6019 match self {
6020 Self::Email => "email",
6021 Self::Uri => "uri",
6022 Self::Date => "date",
6023 Self::DateTime => "date-time",
6024 }
6025 }
6026}
6027
6028pub use crate::generated::api_types::{
6033 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
6034 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
6035 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
6036 PermissionDecisionApproveOnce, PermissionDecisionContext, PermissionDecisionOutcome,
6037 PermissionDecisionReject, PermissionDecisionSource, PermissionDecisionSurface,
6038 PermissionDecisionUserNotAvailable, PermissionResponseCapability,
6039};
6040
6041#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6047#[serde(rename_all = "kebab-case")]
6048#[non_exhaustive]
6049pub enum PermissionRequestKind {
6050 Shell,
6052 Write,
6054 Read,
6056 Url,
6058 Mcp,
6060 CustomTool,
6062 Memory,
6064 Hook,
6066 #[serde(other)]
6069 Unknown,
6070}
6071
6072#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6078#[serde(rename_all = "camelCase")]
6079pub struct PermissionRequestData {
6080 #[serde(default, skip_serializing_if = "Option::is_none")]
6084 pub kind: Option<PermissionRequestKind>,
6085 #[serde(default, skip_serializing_if = "Option::is_none")]
6088 pub tool_call_id: Option<String>,
6089 #[serde(default, skip_serializing_if = "Option::is_none")]
6091 pub managed_approval_required: Option<bool>,
6092 #[serde(default, skip_serializing_if = "is_false")]
6094 pub managed_settings_enabled: bool,
6095 #[serde(flatten)]
6099 pub extra: Value,
6100}
6101
6102#[derive(Debug, Clone, Serialize, Deserialize)]
6104#[serde(rename_all = "camelCase")]
6105pub struct ExitPlanModeData {
6106 #[serde(default)]
6108 pub summary: String,
6109 #[serde(default, skip_serializing_if = "Option::is_none")]
6111 pub plan_content: Option<String>,
6112 #[serde(default)]
6114 pub actions: Vec<String>,
6115 #[serde(default = "default_recommended_action")]
6117 pub recommended_action: String,
6118}
6119
6120fn default_recommended_action() -> String {
6121 "autopilot".to_string()
6122}
6123
6124impl Default for ExitPlanModeData {
6125 fn default() -> Self {
6126 Self {
6127 summary: String::new(),
6128 plan_content: None,
6129 actions: Vec::new(),
6130 recommended_action: default_recommended_action(),
6131 }
6132 }
6133}
6134
6135#[cfg(test)]
6136mod tests {
6137 use std::collections::HashMap;
6138 use std::path::PathBuf;
6139
6140 use serde_json::json;
6141
6142 use super::{
6143 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
6144 AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions,
6145 ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode,
6146 ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
6147 InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
6148 MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig,
6149 ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
6150 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
6151 ToolResultResponse, ensure_attachment_display_names,
6152 };
6153 use crate::generated::session_events::TypedSessionEvent;
6154
6155 #[test]
6156 fn permission_response_capability_is_publicly_exported() {
6157 assert_eq!(
6158 serde_json::to_value(PermissionResponseCapability::Interactive).unwrap(),
6159 json!("interactive")
6160 );
6161 }
6162
6163 #[test]
6164 fn tool_builder_composes() {
6165 let tool = Tool::new("greet")
6166 .with_description("Say hello")
6167 .with_namespaced_name("hello/greet")
6168 .with_instructions("Pass the user's name")
6169 .with_parameters(json!({
6170 "type": "object",
6171 "properties": { "name": { "type": "string" } },
6172 "required": ["name"]
6173 }))
6174 .with_overrides_built_in_tool(true)
6175 .with_skip_permission(true);
6176 assert_eq!(tool.name, "greet");
6177 assert_eq!(tool.description, "Say hello");
6178 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
6179 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
6180 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
6181 assert!(tool.overrides_built_in_tool);
6182 assert!(tool.skip_permission);
6183 }
6184
6185 #[test]
6186 fn tool_defer_serialization() {
6187 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
6188 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
6189 let value = serde_json::to_value(&tool).unwrap();
6190 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
6191
6192 let plain = Tool::new("plain");
6193 let value = serde_json::to_value(&plain).unwrap();
6194 assert!(value.get("defer").is_none());
6195 }
6196
6197 #[test]
6198 fn tool_metadata_serialization() {
6199 use indexmap::IndexMap;
6200
6201 let mut metadata = IndexMap::new();
6202 metadata.insert(
6203 "github.com/copilot:safeForTelemetry".to_string(),
6204 json!({ "name": true, "inputsNames": false }),
6205 );
6206 let tool = Tool::new("lookup").with_metadata(metadata);
6207 let value = serde_json::to_value(&tool).unwrap();
6208 assert_eq!(
6209 value
6210 .get("metadata")
6211 .unwrap()
6212 .get("github.com/copilot:safeForTelemetry")
6213 .unwrap(),
6214 &json!({ "name": true, "inputsNames": false })
6215 );
6216
6217 let plain = Tool::new("plain");
6219 let value = serde_json::to_value(&plain).unwrap();
6220 assert!(value.get("metadata").is_none());
6221 }
6222
6223 #[test]
6224 fn custom_agent_config_builder_with_model() {
6225 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
6226 .with_model("claude-haiku-4.5")
6227 .with_display_name("My Agent");
6228 assert_eq!(agent.name, "my-agent");
6229 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
6230 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
6231 }
6232
6233 #[test]
6234 fn custom_agent_config_serializes_model() {
6235 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
6236 let wire = serde_json::to_value(&agent).unwrap();
6237 assert_eq!(wire["model"], "claude-haiku-4.5");
6238 assert_eq!(wire["name"], "model-agent");
6239 }
6240
6241 #[test]
6242 fn custom_agent_config_omits_model_when_none() {
6243 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
6244 let wire = serde_json::to_value(&agent).unwrap();
6245 assert!(wire.get("model").is_none());
6246 }
6247
6248 #[test]
6249 fn custom_agent_config_builder_with_reasoning_effort() {
6250 let agent =
6251 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6252 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
6253 }
6254
6255 #[test]
6256 fn custom_agent_config_serializes_reasoning_effort() {
6257 let agent =
6258 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6259 let wire = serde_json::to_value(&agent).unwrap();
6260 assert_eq!(wire["reasoningEffort"], "high");
6261 }
6262
6263 #[test]
6264 fn custom_agent_config_omits_reasoning_effort_when_none() {
6265 let agent = CustomAgentConfig::new("default-agent", "prompt");
6266 let wire = serde_json::to_value(&agent).unwrap();
6267 assert!(wire.get("reasoningEffort").is_none());
6268 }
6269
6270 #[test]
6271 #[should_panic(expected = "tool parameter schema must be a JSON object")]
6272 fn tool_with_parameters_panics_on_non_object_value() {
6273 let _ = Tool::new("noop").with_parameters(json!(null));
6274 }
6275
6276 #[test]
6277 fn tool_result_expanded_serializes_binary_results_for_llm() {
6278 let response = ToolResultResponse {
6279 result: ToolResult::Expanded(ToolResultExpanded {
6280 text_result_for_llm: "rendered chart".to_string(),
6281 result_type: "success".to_string(),
6282 binary_results_for_llm: Some(vec![ToolBinaryResult {
6283 data: "aW1n".to_string(),
6284 mime_type: "image/png".to_string(),
6285 r#type: "image".to_string(),
6286 description: Some("chart preview".to_string()),
6287 }]),
6288 session_log: None,
6289 error: None,
6290 tool_telemetry: None,
6291 tool_references: None,
6292 }),
6293 };
6294
6295 let wire = serde_json::to_value(&response).unwrap();
6296
6297 assert_eq!(
6298 wire,
6299 json!({
6300 "result": {
6301 "textResultForLlm": "rendered chart",
6302 "resultType": "success",
6303 "binaryResultsForLlm": [
6304 {
6305 "data": "aW1n",
6306 "mimeType": "image/png",
6307 "type": "image",
6308 "description": "chart preview"
6309 }
6310 ]
6311 }
6312 })
6313 );
6314 }
6315
6316 #[test]
6317 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
6318 let response = ToolResultResponse {
6319 result: ToolResult::Expanded(ToolResultExpanded {
6320 text_result_for_llm: "ok".to_string(),
6321 result_type: "success".to_string(),
6322 binary_results_for_llm: None,
6323 session_log: None,
6324 error: None,
6325 tool_telemetry: None,
6326 tool_references: None,
6327 }),
6328 };
6329
6330 let wire = serde_json::to_value(&response).unwrap();
6331
6332 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6333 assert!(wire["result"].get("binaryResultsForLlm").is_none());
6334 }
6335
6336 #[test]
6337 fn tool_result_expanded_serializes_tool_references() {
6338 let response = ToolResultResponse {
6339 result: ToolResult::Expanded(
6340 ToolResultExpanded::new("found 2 tools", "success")
6341 .with_tool_references(["get_weather", "check_status"]),
6342 ),
6343 };
6344
6345 let wire = serde_json::to_value(&response).unwrap();
6346
6347 assert_eq!(
6348 wire,
6349 json!({
6350 "result": {
6351 "textResultForLlm": "found 2 tools",
6352 "resultType": "success",
6353 "toolReferences": ["get_weather", "check_status"]
6354 }
6355 })
6356 );
6357 }
6358
6359 #[test]
6360 fn tool_result_expanded_omits_tool_references_when_none() {
6361 let response = ToolResultResponse {
6362 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
6363 };
6364
6365 let wire = serde_json::to_value(&response).unwrap();
6366
6367 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6368 assert!(wire["result"].get("toolReferences").is_none());
6369 }
6370
6371 #[test]
6372 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
6373 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
6376 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
6377
6378 assert_eq!(
6379 expanded.tool_references.as_deref(),
6380 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6381 );
6382 }
6383
6384 #[test]
6385 fn tool_result_expanded_deserializes_tool_references() {
6386 let wire = json!({
6387 "textResultForLlm": "found tools",
6388 "resultType": "success",
6389 "toolReferences": ["alpha", "beta"]
6390 });
6391
6392 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
6393
6394 assert_eq!(
6395 expanded.tool_references.as_deref(),
6396 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6397 );
6398 }
6399
6400 #[test]
6401 fn session_config_default_wire_flags_off_without_handlers() {
6402 let cfg = SessionConfig::default();
6403 assert_eq!(cfg.mcp_oauth_token_storage, None);
6404 let (wire, _runtime) = cfg
6408 .into_wire(Some(SessionId::from("default-flags")))
6409 .expect("default config has no duplicate handlers");
6410 assert!(!wire.request_user_input);
6411 assert!(!wire.request_permission);
6412 assert!(!wire.request_elicitation);
6413 assert!(!wire.request_exit_plan_mode);
6414 assert!(!wire.request_auto_mode_switch);
6415 assert!(!wire.hooks);
6416 assert!(!wire.request_mcp_apps);
6417 let json = serde_json::to_value(&wire).unwrap();
6418 assert!(json.get("askUserVariant").is_none());
6419 }
6420
6421 #[test]
6422 fn resume_session_config_new_wire_flags_off_without_handlers() {
6423 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
6424 assert_eq!(cfg.mcp_oauth_token_storage, None);
6425 let (wire, _runtime) = cfg
6426 .into_wire()
6427 .expect("default resume config has no duplicate handlers");
6428 assert!(!wire.request_user_input);
6429 assert!(!wire.request_permission);
6430 assert!(!wire.request_elicitation);
6431 assert!(!wire.request_exit_plan_mode);
6432 assert!(!wire.request_auto_mode_switch);
6433 assert!(!wire.hooks);
6434 assert!(!wire.request_mcp_apps);
6435 let json = serde_json::to_value(&wire).unwrap();
6436 assert!(json.get("askUserVariant").is_none());
6437 }
6438
6439 #[test]
6440 fn custom_agents_local_only_serializes_on_create_and_resume() {
6441 let (create_wire, _) = SessionConfig::default()
6442 .with_custom_agents_local_only(false)
6443 .into_wire(Some(SessionId::from("create-locality")))
6444 .expect("create config has no duplicate handlers");
6445 let create_json = serde_json::to_value(&create_wire).unwrap();
6446 assert_eq!(create_json["customAgentsLocalOnly"], false);
6447
6448 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
6449 .with_custom_agents_local_only(false)
6450 .into_wire()
6451 .expect("resume config has no duplicate handlers");
6452 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6453 assert_eq!(resume_json["customAgentsLocalOnly"], false);
6454
6455 let (unset_create_wire, _) = SessionConfig::default()
6456 .into_wire(Some(SessionId::from("create-unset")))
6457 .expect("create config has no duplicate handlers");
6458 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6459 assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
6460
6461 let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
6462 .into_wire()
6463 .expect("resume config has no duplicate handlers");
6464 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6465 assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
6466 }
6467
6468 #[test]
6469 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6470 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
6471 assert_eq!(cfg.enable_mcp_apps, Some(true));
6472
6473 let (wire, _runtime) = cfg
6474 .into_wire(Some(SessionId::from("enable-mcp-apps")))
6475 .expect("enable_mcp_apps config has no duplicate handlers");
6476 assert!(wire.request_mcp_apps);
6477
6478 let json = serde_json::to_value(&wire).unwrap();
6479 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6480 }
6481
6482 #[test]
6483 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6484 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
6485 .with_enable_mcp_apps(true);
6486 assert_eq!(cfg.enable_mcp_apps, Some(true));
6487
6488 let (wire, _runtime) = cfg
6489 .into_wire()
6490 .expect("resume enable_mcp_apps config has no duplicate handlers");
6491 assert!(wire.request_mcp_apps);
6492
6493 let json = serde_json::to_value(&wire).unwrap();
6494 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6495 }
6496
6497 #[test]
6498 fn github_mcp_tool_config_serializes_for_create_and_resume() {
6499 let github_config = GitHubMcpToolConfig::new()
6500 .with_enable_all_tools(true)
6501 .with_additional_toolsets(["repos"])
6502 .with_additional_tools(["get_issue"])
6503 .with_enable_insiders_mode(true)
6504 .with_disable_form_deferral(true);
6505
6506 let (create_wire, _) = SessionConfig::default()
6507 .with_github_mcp_tool_config(github_config.clone())
6508 .into_wire(Some(SessionId::from("github-mcp")))
6509 .expect("create config has no duplicate handlers");
6510 assert_eq!(
6511 serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6512 serde_json::json!({
6513 "enableAllTools": true,
6514 "additionalToolsets": ["repos"],
6515 "additionalTools": ["get_issue"],
6516 "enableInsidersMode": true,
6517 "disableFormDeferral": true,
6518 })
6519 );
6520
6521 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6522 .with_github_mcp_tool_config(github_config)
6523 .into_wire()
6524 .expect("resume config has no duplicate handlers");
6525 assert!(resume_wire.github_mcp_tool_config.is_some());
6526
6527 let (unset_wire, _) = SessionConfig::default()
6528 .into_wire(Some(SessionId::from("github-mcp-unset")))
6529 .expect("default config has no duplicate handlers");
6530 assert!(
6531 serde_json::to_value(&unset_wire)
6532 .unwrap()
6533 .get("githubMcpToolConfig")
6534 .is_none()
6535 );
6536 }
6537
6538 #[test]
6539 fn memory_configuration_constructors_and_serde() {
6540 assert!(MemoryConfiguration::enabled().enabled);
6541 assert!(!MemoryConfiguration::disabled().enabled);
6542 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6543
6544 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6545 assert_eq!(json, serde_json::json!({ "enabled": true }));
6546 }
6547
6548 #[test]
6549 fn session_config_with_memory_serializes() {
6550 let (wire, _runtime) = SessionConfig::default()
6551 .with_memory(MemoryConfiguration::enabled())
6552 .into_wire(Some(SessionId::from("memory-on")))
6553 .expect("no duplicate handlers");
6554 let json = serde_json::to_value(&wire).unwrap();
6555 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6556
6557 let (wire_off, _) = SessionConfig::default()
6558 .with_memory(MemoryConfiguration::disabled())
6559 .into_wire(Some(SessionId::from("memory-off")))
6560 .expect("no duplicate handlers");
6561 let json_off = serde_json::to_value(&wire_off).unwrap();
6562 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6563
6564 let (empty_wire, _) = SessionConfig::default()
6566 .into_wire(Some(SessionId::from("memory-unset")))
6567 .expect("no duplicate handlers");
6568 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6569 assert!(empty_json.get("memory").is_none());
6570 }
6571
6572 #[test]
6573 fn resume_session_config_with_memory_serializes() {
6574 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6575 .with_memory(MemoryConfiguration::enabled())
6576 .into_wire()
6577 .expect("no duplicate handlers");
6578 let json = serde_json::to_value(&wire).unwrap();
6579 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6580
6581 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6583 .into_wire()
6584 .expect("no duplicate handlers");
6585 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6586 assert!(empty_json.get("memory").is_none());
6587 }
6588
6589 #[test]
6590 fn feature_flags_serialize_on_create_and_resume() {
6591 let feature_flags = HashMap::from([
6592 ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true),
6593 ("DISABLED_TEST_FLAG".to_string(), false),
6594 ]);
6595 let expected = serde_json::json!({
6596 "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true,
6597 "DISABLED_TEST_FLAG": false,
6598 });
6599
6600 let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone());
6601 assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags));
6602 let (create_wire, _) = create_config
6603 .into_wire(Some(SessionId::from("feature-flags-create")))
6604 .expect("no duplicate handlers");
6605 let create_json = serde_json::to_value(&create_wire).unwrap();
6606 assert_eq!(create_json["featureFlags"], expected);
6607
6608 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume"))
6609 .with_feature_flags(feature_flags)
6610 .into_wire()
6611 .expect("no duplicate handlers");
6612 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6613 assert_eq!(resume_json["featureFlags"], expected);
6614
6615 let (unset_create_wire, _) = SessionConfig::default()
6616 .into_wire(Some(SessionId::from("feature-flags-create-unset")))
6617 .expect("no duplicate handlers");
6618 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6619 assert!(unset_create_json.get("featureFlags").is_none());
6620
6621 let (unset_resume_wire, _) =
6622 ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset"))
6623 .into_wire()
6624 .expect("no duplicate handlers");
6625 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6626 assert!(unset_resume_json.get("featureFlags").is_none());
6627 }
6628
6629 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6630 CopilotExpAssignmentResponse {
6631 features: vec!["copilot_exp_flag".to_string()],
6632 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6633 configs: vec![ExpConfigEntry {
6634 id: "cfg-1".to_string(),
6635 parameters: HashMap::from([
6636 ("threshold".to_string(), ExpFlagValue::Integer(5)),
6637 ("enabled".to_string(), ExpFlagValue::Bool(true)),
6638 ]),
6639 }],
6640 assignment_context: context.to_string(),
6641 ..Default::default()
6642 }
6643 }
6644
6645 #[test]
6646 fn exp_flag_value_round_trips_all_variants() {
6647 let values = serde_json::json!({
6648 "s": "text",
6649 "i": 7,
6650 "f": 1.5,
6651 "b": true,
6652 "n": null,
6653 });
6654 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6655 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6656 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6657 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6658 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6659 assert_eq!(parsed["n"], ExpFlagValue::Null);
6660 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6661 }
6662
6663 #[test]
6664 fn session_config_with_exp_assignments_serializes() {
6665 let assignments = sample_exp_assignments("ctx-123");
6666 let expected = serde_json::to_value(&assignments).unwrap();
6667 let (wire, _runtime) = SessionConfig::default()
6668 .with_exp_assignments(assignments)
6669 .into_wire(Some(SessionId::from("exp-on")))
6670 .expect("no duplicate handlers");
6671 let json = serde_json::to_value(&wire).unwrap();
6672 assert_eq!(json["expAssignments"], expected);
6673 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6674 assert_eq!(
6675 json["expAssignments"]["Flights"]["copilot_exp_flag"],
6676 "treatment"
6677 );
6678
6679 let (empty_wire, _) = SessionConfig::default()
6681 .into_wire(Some(SessionId::from("exp-unset")))
6682 .expect("no duplicate handlers");
6683 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6684 assert!(empty_json.get("expAssignments").is_none());
6685 }
6686
6687 #[test]
6688 fn resume_session_config_with_exp_assignments_serializes() {
6689 let assignments = sample_exp_assignments("ctx-456");
6690 let expected = serde_json::to_value(&assignments).unwrap();
6691 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6692 .with_exp_assignments(assignments)
6693 .into_wire()
6694 .expect("no duplicate handlers");
6695 let json = serde_json::to_value(&wire).unwrap();
6696 assert_eq!(json["expAssignments"], expected);
6697
6698 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6700 .into_wire()
6701 .expect("no duplicate handlers");
6702 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6703 assert!(empty_json.get("expAssignments").is_none());
6704 }
6705
6706 #[test]
6707 fn session_config_clone_preserves_exp_assignments() {
6708 let assignments = sample_exp_assignments("ctx-clone");
6709 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6710 let cloned = config.clone();
6711
6712 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6713
6714 let (wire, _runtime) = cloned
6715 .into_wire(Some(SessionId::from("exp-clone")))
6716 .expect("no duplicate handlers");
6717 let json = serde_json::to_value(&wire).unwrap();
6718 assert_eq!(
6719 json["expAssignments"],
6720 serde_json::to_value(&assignments).unwrap()
6721 );
6722 }
6723
6724 #[test]
6725 fn resume_session_config_clone_preserves_exp_assignments() {
6726 let assignments = sample_exp_assignments("ctx-clone-resume");
6727 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6728 .with_exp_assignments(assignments.clone());
6729 let cloned = config.clone();
6730
6731 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6732
6733 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6734 let json = serde_json::to_value(&wire).unwrap();
6735 assert_eq!(
6736 json["expAssignments"],
6737 serde_json::to_value(&assignments).unwrap()
6738 );
6739 }
6740
6741 #[test]
6742 #[allow(clippy::field_reassign_with_default)]
6743 fn session_config_into_wire_serializes_bucket_b_fields() {
6744 use std::path::PathBuf;
6745
6746 use super::{CloudSessionOptions, CloudSessionRepository};
6747
6748 let mut cfg = SessionConfig::default();
6749 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6750 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6751 cfg.github_token = Some("ghs_secret".to_string());
6752 cfg.include_sub_agent_streaming_events = Some(false);
6753 cfg.enable_session_telemetry = Some(false);
6754 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
6755 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
6756 cfg.enable_on_demand_instruction_discovery = Some(false);
6757 cfg.cloud = Some(CloudSessionOptions::with_repository(
6758 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
6759 ));
6760
6761 let (wire, _runtime) = cfg
6762 .into_wire(Some(SessionId::from("custom-id")))
6763 .expect("no duplicate handlers");
6764 let wire_json = serde_json::to_value(&wire).unwrap();
6765 assert_eq!(wire_json["sessionId"], "custom-id");
6766 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6767 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6768 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6769 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6770 assert_eq!(wire_json["enableSessionTelemetry"], false);
6771 assert_eq!(wire_json["reasoningSummary"], "concise");
6772 assert_eq!(wire_json["remoteSession"], "export");
6773 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6774 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6775 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6776 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6777
6778 let (empty_wire, _) = SessionConfig::default()
6780 .into_wire(Some(SessionId::from("empty")))
6781 .expect("default has no duplicate handlers");
6782 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6783 assert!(empty_json.get("gitHubToken").is_none());
6784 assert!(empty_json.get("enableSessionTelemetry").is_none());
6785 assert!(empty_json.get("reasoningSummary").is_none());
6786 assert!(empty_json.get("remoteSession").is_none());
6787 assert!(
6788 empty_json
6789 .get("enableOnDemandInstructionDiscovery")
6790 .is_none()
6791 );
6792 assert!(empty_json.get("cloud").is_none());
6793 }
6794
6795 #[test]
6796 fn session_config_into_wire_serializes_named_providers_and_models() {
6797 let cfg = SessionConfig::default()
6798 .with_providers(vec![
6799 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6800 .with_provider_type("openai")
6801 .with_wire_api("responses")
6802 .with_api_key("sk-test"),
6803 ])
6804 .with_models(vec![
6805 ProviderModelConfig::new("gpt-x", "my-openai")
6806 .with_wire_model("gpt-x-2025")
6807 .with_max_output_tokens(2048),
6808 ]);
6809
6810 let (wire, _) = cfg
6811 .into_wire(Some(SessionId::from("sess-providers")))
6812 .expect("no duplicate handlers");
6813 let wire_json = serde_json::to_value(&wire).unwrap();
6814 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6815 assert_eq!(
6816 wire_json["providers"][0]["baseUrl"],
6817 "https://api.example.com/v1"
6818 );
6819 assert_eq!(wire_json["providers"][0]["type"], "openai");
6820 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6821 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6822 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
6823 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
6824 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
6825 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
6826
6827 let (empty_wire, _) = SessionConfig::default()
6828 .into_wire(Some(SessionId::from("empty")))
6829 .expect("default has no duplicate handlers");
6830 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6831 assert!(empty_json.get("providers").is_none());
6832 assert!(empty_json.get("models").is_none());
6833 }
6834
6835 #[test]
6836 fn resume_config_into_wire_serializes_named_providers_and_models() {
6837 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
6838 .with_providers(vec![
6839 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
6840 .with_provider_type("azure")
6841 .with_azure(AzureProviderOptions {
6842 api_version: Some("2024-10-21".to_string()),
6843 }),
6844 ])
6845 .with_models(vec![
6846 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
6847 ]);
6848
6849 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6850 let wire_json = serde_json::to_value(&wire).unwrap();
6851 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
6852 assert_eq!(wire_json["providers"][0]["type"], "azure");
6853 assert_eq!(
6854 wire_json["providers"][0]["azure"]["apiVersion"],
6855 "2024-10-21"
6856 );
6857 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
6858 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
6859 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
6860
6861 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
6862 .into_wire()
6863 .expect("default has no duplicate handlers");
6864 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6865 assert!(empty_json.get("providers").is_none());
6866 assert!(empty_json.get("models").is_none());
6867 }
6868
6869 #[test]
6870 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
6871 use std::path::PathBuf;
6872
6873 let cfg = SessionConfig {
6874 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
6875 disabled_mcp_servers: Some(vec![
6876 "local-files".to_string(),
6877 "remote-github".to_string(),
6878 ]),
6879 large_output: Some(
6880 LargeToolOutputConfig::new()
6881 .with_enabled(true)
6882 .with_max_size_bytes(1024)
6883 .with_output_directory(PathBuf::from("/tmp/large-output")),
6884 ),
6885 ..Default::default()
6886 };
6887
6888 let (wire, _) = cfg
6889 .into_wire(Some(SessionId::from("sess-1")))
6890 .expect("no duplicate handlers");
6891 let wire_json = serde_json::to_value(&wire).unwrap();
6892 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
6893 assert_eq!(
6894 wire_json["disabledMcpServers"],
6895 serde_json::json!(["local-files", "remote-github"])
6896 );
6897 assert_eq!(wire_json["largeOutput"]["enabled"], true);
6898 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
6899 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
6900
6901 let (empty_wire, _) = SessionConfig::default()
6902 .into_wire(Some(SessionId::from("empty")))
6903 .expect("default has no duplicate handlers");
6904 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6905 assert!(empty_json.get("pluginDirectories").is_none());
6906 assert!(empty_json.get("disabledMcpServers").is_none());
6907 assert!(empty_json.get("largeOutput").is_none());
6908 }
6909
6910 #[test]
6911 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
6912 use std::path::PathBuf;
6913
6914 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6915 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6916 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6917 cfg.github_token = Some("ghs_secret".to_string());
6918 cfg.include_sub_agent_streaming_events = Some(true);
6919 cfg.enable_session_telemetry = Some(false);
6920 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
6921 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
6922 cfg.enable_on_demand_instruction_discovery = Some(false);
6923
6924 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6925 let wire_json = serde_json::to_value(&wire).unwrap();
6926 assert_eq!(wire_json["sessionId"], "sess-1");
6927 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6928 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6929 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6930 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
6931 assert_eq!(wire_json["enableSessionTelemetry"], false);
6932 assert_eq!(wire_json["reasoningSummary"], "detailed");
6933 assert_eq!(wire_json["remoteSession"], "on");
6934 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6935
6936 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6938 .into_wire()
6939 .expect("default resume has no duplicate handlers");
6940 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6941 assert!(empty_json.get("reasoningSummary").is_none());
6942 assert!(empty_json.get("remoteSession").is_none());
6943 assert!(
6944 empty_json
6945 .get("enableOnDemandInstructionDiscovery")
6946 .is_none()
6947 );
6948 }
6949
6950 #[test]
6951 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
6952 use std::path::PathBuf;
6953
6954 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6955 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6956 cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]);
6957 cfg.large_output = Some(
6958 LargeToolOutputConfig::new()
6959 .with_enabled(false)
6960 .with_max_size_bytes(2048)
6961 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6962 );
6963
6964 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6965 let wire_json = serde_json::to_value(&wire).unwrap();
6966 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6967 assert_eq!(
6968 wire_json["disabledMcpServers"],
6969 serde_json::json!(["local-files-r"])
6970 );
6971 assert_eq!(wire_json["largeOutput"]["enabled"], false);
6972 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6973 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6974
6975 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6976 .into_wire()
6977 .expect("default resume has no duplicate handlers");
6978 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6979 assert!(empty_json.get("pluginDirectories").is_none());
6980 assert!(empty_json.get("disabledMcpServers").is_none());
6981 assert!(empty_json.get("largeOutput").is_none());
6982 }
6983
6984 #[test]
6985 fn session_config_clones_disabled_mcp_servers() {
6986 let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]);
6987 let mut create_clone = create.clone();
6988 create_clone
6989 .disabled_mcp_servers
6990 .as_mut()
6991 .expect("configured disabled MCP servers")
6992 .push("remote-github".to_string());
6993 assert_eq!(
6994 create.disabled_mcp_servers.as_deref(),
6995 Some(&["local-files".to_string()][..])
6996 );
6997
6998 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6999 .with_disabled_mcp_servers(["local-files"]);
7000 let mut resume_clone = resume.clone();
7001 resume_clone
7002 .disabled_mcp_servers
7003 .as_mut()
7004 .expect("configured disabled MCP servers")
7005 .push("remote-github".to_string());
7006 assert_eq!(
7007 resume.disabled_mcp_servers.as_deref(),
7008 Some(&["local-files".to_string()][..])
7009 );
7010 }
7011
7012 #[test]
7013 fn session_config_builder_composes() {
7014 use indexmap::IndexMap;
7015
7016 let cfg = SessionConfig::default()
7017 .with_session_id(SessionId::from("sess-1"))
7018 .with_model("claude-sonnet-4")
7019 .with_client_name("test-app")
7020 .with_reasoning_effort("medium")
7021 .with_reasoning_summary(ReasoningSummary::Concise)
7022 .with_context_tier("long_context")
7023 .with_streaming(true)
7024 .with_tools([Tool::new("greet")])
7025 .with_available_tools(["bash", "view"])
7026 .with_excluded_tools(["dangerous"])
7027 .with_mcp_servers(IndexMap::new())
7028 .with_mcp_oauth_token_storage("persistent")
7029 .with_enable_config_discovery(true)
7030 .with_enable_on_demand_instruction_discovery(true)
7031 .with_skill_directories([PathBuf::from("/tmp/skills")])
7032 .with_disabled_skills(["broken-skill"])
7033 .with_disabled_mcp_servers(["local-files"])
7034 .with_agent("researcher")
7035 .with_config_directory(PathBuf::from("/tmp/config"))
7036 .with_working_directory(PathBuf::from("/tmp/work"))
7037 .with_additional_directories([PathBuf::from("/tmp/shared")])
7038 .with_github_token("ghp_test")
7039 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7040 .with_enable_session_telemetry(false)
7041 .with_include_sub_agent_streaming_events(false)
7042 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
7043
7044 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
7045 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
7046 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
7047 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
7048 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
7049 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
7050 assert_eq!(cfg.streaming, Some(true));
7051 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
7052 assert_eq!(
7053 cfg.available_tools.as_deref(),
7054 Some(&["bash".to_string(), "view".to_string()][..])
7055 );
7056 assert_eq!(
7057 cfg.excluded_tools.as_deref(),
7058 Some(&["dangerous".to_string()][..])
7059 );
7060 assert!(cfg.mcp_servers.is_some());
7061 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
7062 assert_eq!(cfg.enable_config_discovery, Some(true));
7063 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
7064 assert_eq!(
7065 cfg.skill_directories.as_deref(),
7066 Some(&[PathBuf::from("/tmp/skills")][..])
7067 );
7068 assert_eq!(
7069 cfg.disabled_skills.as_deref(),
7070 Some(&["broken-skill".to_string()][..])
7071 );
7072 assert_eq!(
7073 cfg.disabled_mcp_servers.as_deref(),
7074 Some(&["local-files".to_string()][..])
7075 );
7076 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
7077 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
7078 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
7079 assert_eq!(
7080 cfg.additional_directories.as_deref(),
7081 Some(&[PathBuf::from("/tmp/shared")][..])
7082 );
7083 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
7084 assert_eq!(
7085 cfg.capi,
7086 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7087 );
7088 assert_eq!(cfg.enable_session_telemetry, Some(false));
7089 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
7090 assert_eq!(
7091 cfg.extension_info,
7092 Some(ExtensionInfo::new("github-app", "counter"))
7093 );
7094 }
7095
7096 #[test]
7097 fn resume_session_config_builder_composes() {
7098 use indexmap::IndexMap;
7099
7100 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
7101 .with_client_name("test-app")
7102 .with_reasoning_summary(ReasoningSummary::None)
7103 .with_context_tier("default")
7104 .with_streaming(true)
7105 .with_tools([Tool::new("greet")])
7106 .with_available_tools(["bash", "view"])
7107 .with_excluded_tools(["dangerous"])
7108 .with_mcp_servers(IndexMap::new())
7109 .with_mcp_oauth_token_storage("persistent")
7110 .with_enable_config_discovery(true)
7111 .with_enable_on_demand_instruction_discovery(false)
7112 .with_skill_directories([PathBuf::from("/tmp/skills")])
7113 .with_disabled_skills(["broken-skill"])
7114 .with_disabled_mcp_servers(["local-files"])
7115 .with_agent("researcher")
7116 .with_config_directory(PathBuf::from("/tmp/config"))
7117 .with_working_directory(PathBuf::from("/tmp/work"))
7118 .with_additional_directories([PathBuf::from("/tmp/shared")])
7119 .with_github_token("ghp_test")
7120 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7121 .with_enable_session_telemetry(false)
7122 .with_include_sub_agent_streaming_events(true)
7123 .with_suppress_resume_event(true)
7124 .with_continue_pending_work(true)
7125 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
7126
7127 assert_eq!(cfg.session_id.as_str(), "sess-2");
7128 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
7129 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
7130 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
7131 assert_eq!(cfg.streaming, Some(true));
7132 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
7133 assert_eq!(
7134 cfg.available_tools.as_deref(),
7135 Some(&["bash".to_string(), "view".to_string()][..])
7136 );
7137 assert_eq!(
7138 cfg.excluded_tools.as_deref(),
7139 Some(&["dangerous".to_string()][..])
7140 );
7141 assert!(cfg.mcp_servers.is_some());
7142 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
7143 assert_eq!(cfg.enable_config_discovery, Some(true));
7144 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
7145 assert_eq!(
7146 cfg.skill_directories.as_deref(),
7147 Some(&[PathBuf::from("/tmp/skills")][..])
7148 );
7149 assert_eq!(
7150 cfg.disabled_skills.as_deref(),
7151 Some(&["broken-skill".to_string()][..])
7152 );
7153 assert_eq!(
7154 cfg.disabled_mcp_servers.as_deref(),
7155 Some(&["local-files".to_string()][..])
7156 );
7157 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
7158 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
7159 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
7160 assert_eq!(
7161 cfg.additional_directories.as_deref(),
7162 Some(&[PathBuf::from("/tmp/shared")][..])
7163 );
7164 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
7165 assert_eq!(
7166 cfg.capi,
7167 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7168 );
7169 assert_eq!(cfg.enable_session_telemetry, Some(false));
7170 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
7171 assert_eq!(cfg.suppress_resume_event, Some(true));
7172 assert_eq!(cfg.continue_pending_work, Some(true));
7173 assert_eq!(
7174 cfg.extension_info,
7175 Some(ExtensionInfo::new("github-app", "counter"))
7176 );
7177 }
7178
7179 #[test]
7183 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
7184 let cfg =
7185 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
7186 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7187 let json = serde_json::to_value(&wire).unwrap();
7188 assert_eq!(json["continuePendingWork"], true);
7189
7190 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7192 .into_wire()
7193 .expect("no duplicate handlers");
7194 let json = serde_json::to_value(&wire).unwrap();
7195 assert!(json.get("continuePendingWork").is_none());
7196 }
7197
7198 #[test]
7199 fn session_configs_serialize_additional_directories() {
7200 let create = SessionConfig::default().with_additional_directories([
7201 PathBuf::from("/tmp/shared"),
7202 PathBuf::from("/tmp/generated"),
7203 ]);
7204 let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
7205 let create_json = serde_json::to_value(&create_wire).unwrap();
7206 assert_eq!(
7207 create_json["additionalDirectories"],
7208 serde_json::json!(["/tmp/shared", "/tmp/generated"])
7209 );
7210
7211 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
7212 .with_additional_directories([PathBuf::from("/tmp/resumed")]);
7213 let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
7214 let resume_json = serde_json::to_value(&resume_wire).unwrap();
7215 assert_eq!(
7216 resume_json["additionalDirectories"],
7217 serde_json::json!(["/tmp/resumed"])
7218 );
7219 }
7220
7221 #[test]
7225 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
7226 let cfg =
7227 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
7228 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7229 let json = serde_json::to_value(&wire).unwrap();
7230 assert_eq!(json["disableResume"], true);
7231 assert!(json.get("suppressResumeEvent").is_none());
7232 }
7233
7234 #[test]
7237 fn session_config_serializes_instruction_directories_to_camel_case() {
7238 let cfg =
7239 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
7240 let (wire, _) = cfg
7241 .into_wire(Some(SessionId::from("instr-on")))
7242 .expect("no duplicate handlers");
7243 let json = serde_json::to_value(&wire).unwrap();
7244 assert_eq!(
7245 json["instructionDirectories"],
7246 serde_json::json!(["/tmp/instr"])
7247 );
7248
7249 let (wire, _) = SessionConfig::default()
7251 .into_wire(Some(SessionId::from("instr-off")))
7252 .expect("no duplicate handlers");
7253 let json = serde_json::to_value(&wire).unwrap();
7254 assert!(json.get("instructionDirectories").is_none());
7255 }
7256
7257 #[test]
7260 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
7261 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
7262 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
7263 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7264 let json = serde_json::to_value(&wire).unwrap();
7265 assert_eq!(
7266 json["instructionDirectories"],
7267 serde_json::json!(["/tmp/instr"])
7268 );
7269
7270 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7271 .into_wire()
7272 .expect("no duplicate handlers");
7273 let json = serde_json::to_value(&wire).unwrap();
7274 assert!(json.get("instructionDirectories").is_none());
7275 }
7276
7277 #[test]
7278 fn custom_agent_config_builder_composes() {
7279 use indexmap::IndexMap;
7280
7281 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
7282 .with_display_name("Research Assistant")
7283 .with_description("Investigates technical questions.")
7284 .with_tools(["bash", "view"])
7285 .with_mcp_servers(IndexMap::new())
7286 .with_infer(true)
7287 .with_skills(["rust-coding-skill"]);
7288
7289 assert_eq!(cfg.name, "researcher");
7290 assert_eq!(cfg.prompt, "You are a research assistant.");
7291 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
7292 assert_eq!(
7293 cfg.description.as_deref(),
7294 Some("Investigates technical questions.")
7295 );
7296 assert_eq!(
7297 cfg.tools.as_deref(),
7298 Some(&["bash".to_string(), "view".to_string()][..])
7299 );
7300 assert!(cfg.mcp_servers.is_some());
7301 assert_eq!(cfg.infer, Some(true));
7302 assert_eq!(
7303 cfg.skills.as_deref(),
7304 Some(&["rust-coding-skill".to_string()][..])
7305 );
7306 }
7307
7308 #[test]
7309 fn mcp_servers_serialize_in_insertion_order() {
7310 use indexmap::IndexMap;
7311
7312 let order = [
7318 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
7319 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
7320 ];
7321 let mut servers = IndexMap::new();
7322 for name in order {
7323 servers.insert(
7324 name.to_string(),
7325 McpServerConfig::Stdio(McpStdioServerConfig {
7326 command: "run".to_string(),
7327 ..Default::default()
7328 }),
7329 );
7330 }
7331
7332 let (wire, _runtime) = SessionConfig::default()
7333 .with_mcp_servers(servers)
7334 .into_wire(None)
7335 .expect("into_wire should succeed");
7336 let json = serde_json::to_string(&wire).expect("serialize wire");
7337
7338 let positions: Vec<usize> = order
7339 .iter()
7340 .map(|name| {
7341 json.find(&format!("\"{name}\""))
7342 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
7343 })
7344 .collect();
7345 let mut ascending = positions.clone();
7346 ascending.sort_unstable();
7347 assert_eq!(
7348 positions, ascending,
7349 "mcp server keys must serialize in insertion order: {json}"
7350 );
7351 }
7352
7353 #[test]
7354 fn infinite_session_config_builder_composes() {
7355 let cfg = InfiniteSessionConfig::new()
7356 .with_enabled(true)
7357 .with_background_compaction_threshold(0.75)
7358 .with_buffer_exhaustion_threshold(0.92);
7359
7360 assert_eq!(cfg.enabled, Some(true));
7361 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
7362 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
7363 }
7364
7365 #[test]
7366 fn provider_config_builder_composes() {
7367 use std::collections::HashMap;
7368
7369 let mut headers = HashMap::new();
7370 headers.insert("X-Custom".to_string(), "value".to_string());
7371
7372 let cfg = ProviderConfig::new("https://api.example.com")
7373 .with_provider_type("openai")
7374 .with_wire_api("completions")
7375 .with_transport("websockets")
7376 .with_api_key("sk-test")
7377 .with_bearer_token("bearer-test")
7378 .with_headers(headers)
7379 .with_model_id("gpt-4")
7380 .with_wire_model("azure-gpt-4-deployment")
7381 .with_max_prompt_tokens(8192)
7382 .with_max_output_tokens(2048);
7383
7384 assert_eq!(cfg.base_url, "https://api.example.com");
7385 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
7386 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
7387 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
7388 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
7389 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
7390 assert_eq!(
7391 cfg.headers
7392 .as_ref()
7393 .and_then(|h| h.get("X-Custom"))
7394 .map(String::as_str),
7395 Some("value"),
7396 );
7397 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
7398 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
7399 assert_eq!(cfg.max_prompt_tokens, Some(8192));
7400 assert_eq!(cfg.max_output_tokens, Some(2048));
7401
7402 let wire = serde_json::to_value(&cfg).unwrap();
7404 assert_eq!(wire["modelId"], "gpt-4");
7405 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
7406 assert_eq!(wire["maxPromptTokens"], 8192);
7407 assert_eq!(wire["maxOutputTokens"], 2048);
7408
7409 let unset = ProviderConfig::new("https://api.example.com");
7410 let wire_unset = serde_json::to_value(&unset).unwrap();
7411 assert!(wire_unset.get("modelId").is_none());
7412 assert!(wire_unset.get("wireModel").is_none());
7413 assert!(wire_unset.get("maxPromptTokens").is_none());
7414 assert!(wire_unset.get("maxOutputTokens").is_none());
7415 }
7416
7417 #[test]
7418 fn capi_session_options_builder_composes_and_serializes() {
7419 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
7420
7421 assert_eq!(cfg.enable_web_socket_responses, Some(false));
7422
7423 let wire = serde_json::to_value(&cfg).unwrap();
7424 assert_eq!(
7425 wire,
7426 serde_json::json!({ "enableWebSocketResponses": false })
7427 );
7428
7429 let unset = CapiSessionOptions::new();
7430 let wire_unset = serde_json::to_value(&unset).unwrap();
7431 assert!(wire_unset.get("enableWebSocketResponses").is_none());
7432 assert!(wire_unset.get("autoTier").is_none());
7433 assert_eq!(wire_unset, json!({}));
7434 }
7435
7436 #[test]
7437 fn capi_auto_tier_canonical_values_round_trip_and_forward() {
7438 for (tier, value) in [
7439 (AutoTier::Efficiency, "efficiency"),
7440 (AutoTier::Balance, "balance"),
7441 (AutoTier::Intelligence, "intelligence"),
7442 ] {
7443 let exported: crate::AutoTier = tier.clone();
7444 let capi = CapiSessionOptions::new().with_auto_tier(exported);
7445 assert_eq!(capi.auto_tier, Some(tier));
7446 assert_eq!(
7447 serde_json::to_value(&capi).unwrap(),
7448 json!({"autoTier": value})
7449 );
7450 assert_eq!(
7451 serde_json::from_value::<CapiSessionOptions>(json!({"autoTier": value})).unwrap(),
7452 capi
7453 );
7454
7455 let capi = capi.with_enable_web_socket_responses(false);
7456 let expected = json!({"autoTier": value, "enableWebSocketResponses": false});
7457 let (create, _) = SessionConfig::default()
7458 .with_model("auto")
7459 .with_capi(capi.clone())
7460 .into_wire(Some(SessionId::from("capi-create")))
7461 .unwrap();
7462 assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected);
7463
7464 let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7465 .with_capi(capi)
7466 .into_wire()
7467 .unwrap();
7468 assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected);
7469 }
7470 }
7471
7472 #[test]
7473 fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() {
7474 for value in ["balanced", "Balance", "unknown"] {
7475 assert_eq!(
7476 serde_json::from_value::<AutoTier>(json!(value)).unwrap(),
7477 AutoTier::Unknown
7478 );
7479 }
7480 let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap();
7481 assert_eq!(capi.auto_tier, None);
7482 }
7483
7484 #[test]
7485 fn session_config_with_capi_serializes() {
7486 let (wire, _) = SessionConfig::default()
7487 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7488 .into_wire(Some(SessionId::from("capi-create")))
7489 .expect("no duplicate handlers");
7490 let json = serde_json::to_value(&wire).unwrap();
7491 assert_eq!(
7492 json["capi"],
7493 serde_json::json!({ "enableWebSocketResponses": false })
7494 );
7495
7496 let (empty_wire, _) = SessionConfig::default()
7497 .into_wire(Some(SessionId::from("capi-create-unset")))
7498 .expect("no duplicate handlers");
7499 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7500 assert!(empty_json.get("capi").is_none());
7501 }
7502
7503 #[test]
7504 fn resume_session_config_with_capi_serializes() {
7505 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7506 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7507 .into_wire()
7508 .expect("no duplicate handlers");
7509 let json = serde_json::to_value(&wire).unwrap();
7510 assert_eq!(
7511 json["capi"],
7512 serde_json::json!({ "enableWebSocketResponses": false })
7513 );
7514
7515 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
7516 .into_wire()
7517 .expect("no duplicate handlers");
7518 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7519 assert!(empty_json.get("capi").is_none());
7520 }
7521
7522 #[test]
7523 fn system_message_config_builder_composes() {
7524 use std::collections::HashMap;
7525
7526 let cfg = SystemMessageConfig::new()
7527 .with_mode("replace")
7528 .with_content("Custom system message.")
7529 .with_sections(HashMap::new());
7530
7531 assert_eq!(cfg.mode.as_deref(), Some("replace"));
7532 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
7533 assert!(cfg.sections.is_some());
7534 }
7535
7536 #[test]
7537 fn delivery_mode_serializes_to_kebab_case_strings() {
7538 assert_eq!(
7539 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
7540 "\"enqueue\""
7541 );
7542 assert_eq!(
7543 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
7544 "\"immediate\""
7545 );
7546 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
7547 assert_eq!(parsed, DeliveryMode::Immediate);
7548 }
7549
7550 #[test]
7551 fn agent_mode_serializes_to_kebab_case_strings() {
7552 assert_eq!(
7553 serde_json::to_string(&AgentMode::Interactive).unwrap(),
7554 "\"interactive\""
7555 );
7556 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
7557 assert_eq!(
7558 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
7559 "\"autopilot\""
7560 );
7561 assert_eq!(
7562 serde_json::to_string(&AgentMode::Shell).unwrap(),
7563 "\"shell\""
7564 );
7565 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
7566 assert_eq!(parsed, AgentMode::Plan);
7567 }
7568
7569 #[test]
7570 fn connection_state_distinguishes_variants() {
7571 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
7574 }
7575
7576 #[test]
7582 fn session_event_round_trips_agent_id_on_envelope() {
7583 let wire = json!({
7584 "id": "evt-1",
7585 "timestamp": "2026-04-30T12:00:00Z",
7586 "parentId": null,
7587 "agentId": "sub-agent-42",
7588 "type": "assistant.message",
7589 "data": { "message": "hi" }
7590 });
7591
7592 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
7593 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7594
7595 let roundtripped = serde_json::to_value(&event).unwrap();
7597 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7598
7599 let main_agent_event: SessionEvent = serde_json::from_value(json!({
7601 "id": "evt-2",
7602 "timestamp": "2026-04-30T12:00:01Z",
7603 "parentId": null,
7604 "type": "session.idle",
7605 "data": {}
7606 }))
7607 .unwrap();
7608 assert!(main_agent_event.agent_id.is_none());
7609 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
7610 assert!(roundtripped.get("agentId").is_none());
7611 }
7612
7613 #[test]
7615 fn typed_session_event_round_trips_agent_id_on_envelope() {
7616 let wire = json!({
7617 "id": "evt-1",
7618 "timestamp": "2026-04-30T12:00:00Z",
7619 "parentId": null,
7620 "agentId": "sub-agent-42",
7621 "type": "session.idle",
7622 "data": {}
7623 });
7624
7625 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
7626 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7627
7628 let roundtripped = serde_json::to_value(&event).unwrap();
7629 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7630 }
7631
7632 #[test]
7633 fn connection_state_variants_compile() {
7634 let _ = ConnectionState::Disconnected;
7638 let _ = ConnectionState::Connecting;
7639 let _ = ConnectionState::Connected;
7640 let _ = ConnectionState::Error;
7641 }
7642
7643 #[test]
7644 fn deserializes_runtime_attachment_variants() {
7645 let attachments: Vec<Attachment> = serde_json::from_value(json!([
7646 {
7647 "type": "file",
7648 "path": "/tmp/file.rs",
7649 "displayName": "file.rs",
7650 "lineRange": { "start": 7, "end": 12 }
7651 },
7652 {
7653 "type": "directory",
7654 "path": "/tmp/project",
7655 "displayName": "project"
7656 },
7657 {
7658 "type": "selection",
7659 "filePath": "/tmp/lib.rs",
7660 "displayName": "lib.rs",
7661 "text": "fn main() {}",
7662 "selection": {
7663 "start": { "line": 1, "character": 2 },
7664 "end": { "line": 3, "character": 4 }
7665 }
7666 },
7667 {
7668 "type": "blob",
7669 "data": "Zm9v",
7670 "mimeType": "image/png",
7671 "displayName": "image.png"
7672 },
7673 {
7674 "type": "github_reference",
7675 "number": 42,
7676 "title": "Fix rendering",
7677 "referenceType": "issue",
7678 "state": "open",
7679 "url": "https://github.com/example/repo/issues/42"
7680 }
7681 ]))
7682 .expect("attachments should deserialize");
7683
7684 assert_eq!(attachments.len(), 5);
7685 assert!(matches!(
7686 &attachments[0],
7687 Attachment::File {
7688 path,
7689 display_name,
7690 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7691 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7692 ));
7693 assert!(matches!(
7694 &attachments[1],
7695 Attachment::Directory { path, display_name }
7696 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7697 ));
7698 assert!(matches!(
7699 &attachments[2],
7700 Attachment::Selection {
7701 file_path,
7702 display_name,
7703 selection:
7704 AttachmentSelectionRange {
7705 start: AttachmentSelectionPosition { line: 1, character: 2 },
7706 end: AttachmentSelectionPosition { line: 3, character: 4 },
7707 },
7708 ..
7709 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
7710 ));
7711 assert!(matches!(
7712 &attachments[3],
7713 Attachment::Blob {
7714 data,
7715 mime_type,
7716 display_name,
7717 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
7718 ));
7719 assert!(matches!(
7720 &attachments[4],
7721 Attachment::GitHubReference {
7722 number: 42,
7723 title,
7724 reference_type: GitHubReferenceType::Issue,
7725 state,
7726 url,
7727 } if title == "Fix rendering"
7728 && state == "open"
7729 && url == "https://github.com/example/repo/issues/42"
7730 ));
7731 }
7732
7733 #[test]
7734 fn ensures_display_names_for_variants_that_support_them() {
7735 let mut attachments = vec![
7736 Attachment::File {
7737 path: PathBuf::from("/tmp/file.rs"),
7738 display_name: None,
7739 line_range: None,
7740 },
7741 Attachment::Selection {
7742 file_path: PathBuf::from("/tmp/src/lib.rs"),
7743 display_name: None,
7744 text: "fn main() {}".to_string(),
7745 selection: AttachmentSelectionRange {
7746 start: AttachmentSelectionPosition {
7747 line: 0,
7748 character: 0,
7749 },
7750 end: AttachmentSelectionPosition {
7751 line: 0,
7752 character: 10,
7753 },
7754 },
7755 },
7756 Attachment::Blob {
7757 data: "Zm9v".to_string(),
7758 mime_type: "image/png".to_string(),
7759 display_name: None,
7760 },
7761 Attachment::GitHubReference {
7762 number: 7,
7763 title: "Track regressions".to_string(),
7764 reference_type: GitHubReferenceType::Issue,
7765 state: "open".to_string(),
7766 url: "https://example.com/issues/7".to_string(),
7767 },
7768 ];
7769
7770 ensure_attachment_display_names(&mut attachments);
7771
7772 assert_eq!(attachments[0].display_name(), Some("file.rs"));
7773 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
7774 assert_eq!(attachments[2].display_name(), Some("attachment"));
7775 assert_eq!(attachments[3].display_name(), None);
7776 assert_eq!(
7777 attachments[3].label(),
7778 Some("Track regressions".to_string())
7779 );
7780 }
7781
7782 #[test]
7783 fn github_anchored_attachment_variants_round_trip() {
7784 let cases = vec![
7785 (
7786 "github_commit",
7787 json!({
7788 "type": "github_commit",
7789 "message": "Fix the thing",
7790 "oid": "abc123",
7791 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
7792 "url": "https://github.com/octocat/repo/commit/abc123"
7793 }),
7794 ),
7795 (
7796 "github_release",
7797 json!({
7798 "type": "github_release",
7799 "name": "v1.2.3",
7800 "repo": { "name": "repo", "owner": "octocat" },
7801 "tagName": "v1.2.3",
7802 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
7803 }),
7804 ),
7805 (
7806 "github_actions_job",
7807 json!({
7808 "type": "github_actions_job",
7809 "conclusion": "failure",
7810 "jobId": 99,
7811 "jobName": "build",
7812 "repo": { "name": "repo", "owner": "octocat" },
7813 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
7814 "workflowName": "CI"
7815 }),
7816 ),
7817 (
7818 "github_repository",
7819 json!({
7820 "type": "github_repository",
7821 "description": "An example repository",
7822 "ref": "main",
7823 "repo": { "name": "repo", "owner": "octocat" },
7824 "url": "https://github.com/octocat/repo"
7825 }),
7826 ),
7827 (
7828 "github_file_diff",
7829 json!({
7830 "type": "github_file_diff",
7831 "base": {
7832 "path": "src/lib.rs",
7833 "ref": "main",
7834 "repo": { "name": "repo", "owner": "octocat" }
7835 },
7836 "head": {
7837 "path": "src/lib.rs",
7838 "ref": "feature",
7839 "repo": { "name": "repo", "owner": "octocat" }
7840 },
7841 "url": "https://github.com/octocat/repo/compare/main...feature"
7842 }),
7843 ),
7844 (
7845 "github_tree_comparison",
7846 json!({
7847 "type": "github_tree_comparison",
7848 "base": {
7849 "repo": { "name": "repo", "owner": "octocat" },
7850 "revision": "main"
7851 },
7852 "head": {
7853 "repo": { "name": "repo", "owner": "octocat" },
7854 "revision": "feature"
7855 },
7856 "url": "https://github.com/octocat/repo/compare/main...feature"
7857 }),
7858 ),
7859 (
7860 "github_url",
7861 json!({
7862 "type": "github_url",
7863 "url": "https://github.com/octocat/repo/wiki"
7864 }),
7865 ),
7866 (
7867 "github_file",
7868 json!({
7869 "type": "github_file",
7870 "path": "src/main.rs",
7871 "ref": "main",
7872 "repo": { "name": "repo", "owner": "octocat" },
7873 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
7874 }),
7875 ),
7876 (
7877 "github_snippet",
7878 json!({
7879 "type": "github_snippet",
7880 "lineRange": { "start": 10, "end": 20 },
7881 "path": "src/main.rs",
7882 "ref": "main",
7883 "repo": { "name": "repo", "owner": "octocat" },
7884 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
7885 }),
7886 ),
7887 ];
7888
7889 for (expected_type, input) in cases {
7890 let attachment: Attachment = serde_json::from_value(input.clone())
7891 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
7892
7893 let serialized_string = serde_json::to_string(&attachment)
7898 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
7899
7900 assert_eq!(
7902 serialized_string.matches("\"type\":").count(),
7903 1,
7904 "{expected_type} must serialize a single `type` key"
7905 );
7906
7907 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
7908 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
7909 assert_eq!(
7910 serialized.get("type").and_then(|value| value.as_str()),
7911 Some(expected_type),
7912 "{expected_type} must serialize the correct discriminator"
7913 );
7914
7915 assert_eq!(
7917 serialized, input,
7918 "{expected_type} should round-trip without data loss"
7919 );
7920 let reparsed: Attachment = serde_json::from_value(serialized)
7921 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
7922 assert_eq!(
7923 reparsed, attachment,
7924 "{expected_type} should re-deserialize to the same value"
7925 );
7926 }
7927 }
7928}
7929
7930#[cfg(test)]
7931mod permission_builder_tests {
7932 use std::sync::Arc;
7933
7934 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
7935 use crate::permission;
7936 use crate::types::{
7937 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
7938 SessionId,
7939 };
7940
7941 fn data() -> PermissionRequestData {
7942 PermissionRequestData {
7943 extra: serde_json::json!({"tool": "shell"}),
7944 ..Default::default()
7945 }
7946 }
7947
7948 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7951 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7952 }
7953
7954 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7955 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7956 }
7957
7958 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
7959 handler
7960 .handle(SessionId::from("s1"), RequestId::new("1"), data())
7961 .await
7962 }
7963
7964 #[tokio::test]
7965 async fn approve_all_with_handler_present_approves() {
7966 let cfg = SessionConfig::default()
7967 .with_permission_handler(Arc::new(ApproveAllHandler))
7968 .approve_all_permissions();
7969 let h = resolve_create(cfg).expect("policy + handler yields handler");
7970 assert!(matches!(
7971 dispatch(&h).await,
7972 PermissionResult::Decision {
7973 decision: PermissionDecision::ApproveOnce(_),
7974 ..
7975 }
7976 ));
7977 }
7978
7979 #[tokio::test]
7980 async fn approve_all_standalone_produces_handler() {
7981 let cfg = SessionConfig::default().approve_all_permissions();
7982 let h = resolve_create(cfg).expect("policy alone yields handler");
7983 assert!(matches!(
7984 dispatch(&h).await,
7985 PermissionResult::Decision {
7986 decision: PermissionDecision::ApproveOnce(_),
7987 ..
7988 }
7989 ));
7990 }
7991
7992 #[tokio::test]
7995 async fn approve_all_is_order_independent() {
7996 let a = SessionConfig::default()
7997 .with_permission_handler(Arc::new(ApproveAllHandler))
7998 .approve_all_permissions();
7999 let b = SessionConfig::default()
8000 .approve_all_permissions()
8001 .with_permission_handler(Arc::new(ApproveAllHandler));
8002 let ha = resolve_create(a).unwrap();
8003 let hb = resolve_create(b).unwrap();
8004 assert!(matches!(
8005 dispatch(&ha).await,
8006 PermissionResult::Decision {
8007 decision: PermissionDecision::ApproveOnce(_),
8008 ..
8009 }
8010 ));
8011 assert!(matches!(
8012 dispatch(&hb).await,
8013 PermissionResult::Decision {
8014 decision: PermissionDecision::ApproveOnce(_),
8015 ..
8016 }
8017 ));
8018 }
8019
8020 #[tokio::test]
8021 async fn deny_all_is_order_independent() {
8022 let a = SessionConfig::default()
8023 .with_permission_handler(Arc::new(ApproveAllHandler))
8024 .deny_all_permissions();
8025 let b = SessionConfig::default()
8026 .deny_all_permissions()
8027 .with_permission_handler(Arc::new(ApproveAllHandler));
8028 let ha = resolve_create(a).unwrap();
8029 let hb = resolve_create(b).unwrap();
8030 assert!(matches!(
8031 dispatch(&ha).await,
8032 PermissionResult::Decision {
8033 decision: PermissionDecision::Reject(_),
8034 ..
8035 }
8036 ));
8037 assert!(matches!(
8038 dispatch(&hb).await,
8039 PermissionResult::Decision {
8040 decision: PermissionDecision::Reject(_),
8041 ..
8042 }
8043 ));
8044 }
8045
8046 #[tokio::test]
8047 async fn approve_permissions_if_consults_predicate() {
8048 let cfg = SessionConfig::default().approve_permissions_if(|d| {
8049 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
8050 });
8051 let h = resolve_create(cfg).unwrap();
8052 assert!(matches!(
8053 dispatch(&h).await,
8054 PermissionResult::Decision {
8055 decision: PermissionDecision::Reject(_),
8056 ..
8057 }
8058 ));
8059 }
8060
8061 #[tokio::test]
8062 async fn approve_permissions_if_is_order_independent() {
8063 let predicate = |d: &PermissionRequestData| {
8064 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
8065 };
8066 let a = SessionConfig::default()
8067 .with_permission_handler(Arc::new(ApproveAllHandler))
8068 .approve_permissions_if(predicate);
8069 let b = SessionConfig::default()
8070 .approve_permissions_if(predicate)
8071 .with_permission_handler(Arc::new(ApproveAllHandler));
8072 let ha = resolve_create(a).unwrap();
8073 let hb = resolve_create(b).unwrap();
8074 assert!(matches!(
8075 dispatch(&ha).await,
8076 PermissionResult::Decision {
8077 decision: PermissionDecision::Reject(_),
8078 ..
8079 }
8080 ));
8081 assert!(matches!(
8082 dispatch(&hb).await,
8083 PermissionResult::Decision {
8084 decision: PermissionDecision::Reject(_),
8085 ..
8086 }
8087 ));
8088 }
8089
8090 #[tokio::test]
8091 async fn resume_session_config_approve_all_works() {
8092 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
8093 .with_permission_handler(Arc::new(ApproveAllHandler))
8094 .approve_all_permissions();
8095 let h = resolve_resume(cfg).unwrap();
8096 assert!(matches!(
8097 dispatch(&h).await,
8098 PermissionResult::Decision {
8099 decision: PermissionDecision::ApproveOnce(_),
8100 ..
8101 }
8102 ));
8103 }
8104
8105 #[tokio::test]
8106 async fn resume_session_config_approve_all_is_order_independent() {
8107 let a = ResumeSessionConfig::new(SessionId::from("s1"))
8108 .with_permission_handler(Arc::new(ApproveAllHandler))
8109 .approve_all_permissions();
8110 let b = ResumeSessionConfig::new(SessionId::from("s1"))
8111 .approve_all_permissions()
8112 .with_permission_handler(Arc::new(ApproveAllHandler));
8113 let ha = resolve_resume(a).unwrap();
8114 let hb = resolve_resume(b).unwrap();
8115 assert!(matches!(
8116 dispatch(&ha).await,
8117 PermissionResult::Decision {
8118 decision: PermissionDecision::ApproveOnce(_),
8119 ..
8120 }
8121 ));
8122 assert!(matches!(
8123 dispatch(&hb).await,
8124 PermissionResult::Decision {
8125 decision: PermissionDecision::ApproveOnce(_),
8126 ..
8127 }
8128 ));
8129 }
8130
8131 #[test]
8132 fn session_config_enable_experimental_mode_serializes_when_set() {
8133 let cfg = SessionConfig::default().with_enable_experimental_mode(false);
8134 assert_eq!(cfg.enable_experimental_mode, Some(false));
8135
8136 let (wire, _runtime) = cfg
8137 .into_wire(Some(SessionId::from("experimental-mode")))
8138 .expect("enable_experimental_mode config has no duplicate handlers");
8139 assert_eq!(wire.is_experimental_mode, Some(false));
8140
8141 let json = serde_json::to_value(&wire).unwrap();
8142 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
8143 }
8144
8145 #[test]
8146 fn session_config_enable_experimental_mode_omitted_when_none() {
8147 let cfg = SessionConfig::default();
8148 assert_eq!(cfg.enable_experimental_mode, None);
8149
8150 let (wire, _runtime) = cfg
8151 .into_wire(Some(SessionId::from("no-experimental-mode")))
8152 .expect("default config has no duplicate handlers");
8153 assert_eq!(wire.is_experimental_mode, None);
8154
8155 let json = serde_json::to_value(&wire).unwrap();
8156 assert!(json.get("isExperimentalMode").is_none());
8157 }
8158
8159 #[test]
8160 fn resume_session_config_enable_experimental_mode_serializes_when_set() {
8161 let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
8162 .with_enable_experimental_mode(false);
8163 assert_eq!(cfg.enable_experimental_mode, Some(false));
8164
8165 let (wire, _runtime) = cfg
8166 .into_wire()
8167 .expect("resume enable_experimental_mode config has no duplicate handlers");
8168 assert_eq!(wire.is_experimental_mode, Some(false));
8169
8170 let json = serde_json::to_value(&wire).unwrap();
8171 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
8172 }
8173
8174 #[test]
8175 fn resume_session_config_enable_experimental_mode_omitted_when_none() {
8176 let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
8177 assert_eq!(cfg.enable_experimental_mode, None);
8178
8179 let (wire, _runtime) = cfg
8180 .into_wire()
8181 .expect("default resume config has no duplicate handlers");
8182 assert_eq!(wire.is_experimental_mode, None);
8183
8184 let json = serde_json::to_value(&wire).unwrap();
8185 assert!(json.get("isExperimentalMode").is_none());
8186 }
8187}
8188
8189#[cfg(test)]
8190mod is_terminal_tests {
8191 use super::Tool;
8192
8193 #[test]
8194 fn is_terminal_serializes_as_camel_case_when_set() {
8195 let tool = Tool {
8196 name: "clear_context".to_owned(),
8197 is_terminal: true,
8198 ..Default::default()
8199 };
8200 let value = serde_json::to_value(&tool).expect("tool serializes");
8201 assert_eq!(
8202 value.get("isTerminal"),
8203 Some(&serde_json::Value::Bool(true))
8204 );
8205 }
8206
8207 #[test]
8208 fn is_terminal_is_omitted_when_false() {
8209 let tool = Tool {
8210 name: "plain".to_owned(),
8211 ..Default::default()
8212 };
8213 let value = serde_json::to_value(&tool).expect("tool serializes");
8214 assert!(value.get("isTerminal").is_none());
8215 }
8216
8217 #[test]
8220 fn is_terminal_appears_in_debug_output() {
8221 let terminal = Tool {
8222 name: "clear_context".to_owned(),
8223 is_terminal: true,
8224 ..Default::default()
8225 };
8226 assert!(format!("{terminal:?}").contains("is_terminal: true"));
8227
8228 let plain = Tool {
8229 name: "plain".to_owned(),
8230 ..Default::default()
8231 };
8232 assert!(format!("{plain:?}").contains("is_terminal: false"));
8233 }
8234}