1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11
12use indexmap::IndexMap;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16use crate::canvas::{CanvasDeclaration, CanvasHandler};
17pub use crate::copilot_request_handler::{
18 CopilotHttpRequest, CopilotHttpResponse, CopilotHttpResponseBody, CopilotRequestContext,
19 CopilotRequestError, CopilotRequestHandler, CopilotRequestTransport, CopilotWebSocketForwarder,
20 CopilotWebSocketForwarderBuilder, CopilotWebSocketHandler, CopilotWebSocketMessage,
21 CopilotWebSocketResponse, WebSocketTransform, forward_http,
22};
23use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance};
24use crate::generated::session_events::ReasoningSummary;
25pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig};
27use crate::handler::{
28 AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler,
29 PermissionHandler, UserInputHandler,
30};
31use crate::hooks::SessionHooks;
32use crate::provider_token::BearerTokenProvider;
33pub use crate::session_fs::{
34 DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig,
35 SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult,
36 SessionFsSqliteQueryType,
37};
38pub use crate::trace_context::{TraceContext, TraceContextProvider};
39use crate::transforms::SystemMessageTransform;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44#[allow(dead_code)]
45#[non_exhaustive]
46pub(crate) enum ConnectionState {
47 Disconnected,
49 Connecting,
51 Connected,
53 Error,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
62#[non_exhaustive]
63pub enum SessionLifecycleEventType {
64 #[serde(rename = "session.created")]
66 Created,
67 #[serde(rename = "session.deleted")]
69 Deleted,
70 #[serde(rename = "session.updated")]
72 Updated,
73 #[serde(rename = "session.foreground")]
75 Foreground,
76 #[serde(rename = "session.background")]
78 Background,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct SessionLifecycleEventMetadata {
84 #[serde(rename = "startTime")]
86 pub start_time: String,
87 #[serde(rename = "modifiedTime")]
89 pub modified_time: String,
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub summary: Option<String>,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98pub struct SessionLifecycleEvent {
99 #[serde(rename = "type")]
101 pub event_type: SessionLifecycleEventType,
102 #[serde(rename = "sessionId")]
104 pub session_id: SessionId,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub metadata: Option<SessionLifecycleEventMetadata>,
108}
109
110#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
116#[serde(transparent)]
117pub struct SessionId(String);
118
119impl SessionId {
120 pub fn new(id: impl Into<String>) -> Self {
122 Self(id.into())
123 }
124
125 pub fn as_str(&self) -> &str {
127 &self.0
128 }
129
130 pub fn into_inner(self) -> String {
132 self.0
133 }
134}
135
136impl std::ops::Deref for SessionId {
137 type Target = str;
138
139 fn deref(&self) -> &str {
140 &self.0
141 }
142}
143
144impl std::fmt::Display for SessionId {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 f.write_str(&self.0)
147 }
148}
149
150impl From<String> for SessionId {
151 fn from(s: String) -> Self {
152 Self(s)
153 }
154}
155
156impl From<&str> for SessionId {
157 fn from(s: &str) -> Self {
158 Self(s.to_owned())
159 }
160}
161
162impl AsRef<str> for SessionId {
163 fn as_ref(&self) -> &str {
164 &self.0
165 }
166}
167
168impl std::borrow::Borrow<str> for SessionId {
169 fn borrow(&self) -> &str {
170 &self.0
171 }
172}
173
174impl From<SessionId> for String {
175 fn from(id: SessionId) -> String {
176 id.0
177 }
178}
179
180impl PartialEq<str> for SessionId {
181 fn eq(&self, other: &str) -> bool {
182 self.0 == other
183 }
184}
185
186impl PartialEq<String> for SessionId {
187 fn eq(&self, other: &String) -> bool {
188 &self.0 == other
189 }
190}
191
192impl PartialEq<SessionId> for String {
193 fn eq(&self, other: &SessionId) -> bool {
194 self == &other.0
195 }
196}
197
198impl PartialEq<&str> for SessionId {
199 fn eq(&self, other: &&str) -> bool {
200 self.0 == *other
201 }
202}
203
204impl PartialEq<&SessionId> for SessionId {
205 fn eq(&self, other: &&SessionId) -> bool {
206 self.0 == other.0
207 }
208}
209
210impl PartialEq<SessionId> for &SessionId {
211 fn eq(&self, other: &SessionId) -> bool {
212 self.0 == other.0
213 }
214}
215
216#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
222#[serde(transparent)]
223pub struct RequestId(String);
224
225impl RequestId {
226 pub fn new(id: impl Into<String>) -> Self {
228 Self(id.into())
229 }
230
231 pub fn into_inner(self) -> String {
233 self.0
234 }
235}
236
237impl std::ops::Deref for RequestId {
238 type Target = str;
239
240 fn deref(&self) -> &str {
241 &self.0
242 }
243}
244
245impl std::fmt::Display for RequestId {
246 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 f.write_str(&self.0)
248 }
249}
250
251impl From<String> for RequestId {
252 fn from(s: String) -> Self {
253 Self(s)
254 }
255}
256
257impl From<&str> for RequestId {
258 fn from(s: &str) -> Self {
259 Self(s.to_owned())
260 }
261}
262
263impl AsRef<str> for RequestId {
264 fn as_ref(&self) -> &str {
265 &self.0
266 }
267}
268
269impl std::borrow::Borrow<str> for RequestId {
270 fn borrow(&self) -> &str {
271 &self.0
272 }
273}
274
275impl From<RequestId> for String {
276 fn from(id: RequestId) -> String {
277 id.0
278 }
279}
280
281impl PartialEq<str> for RequestId {
282 fn eq(&self, other: &str) -> bool {
283 self.0 == other
284 }
285}
286
287impl PartialEq<String> for RequestId {
288 fn eq(&self, other: &String) -> bool {
289 &self.0 == other
290 }
291}
292
293impl PartialEq<RequestId> for String {
294 fn eq(&self, other: &RequestId) -> bool {
295 self == &other.0
296 }
297}
298
299impl PartialEq<&str> for RequestId {
300 fn eq(&self, other: &&str) -> bool {
301 self.0 == *other
302 }
303}
304
305#[derive(Clone, Default, Serialize, Deserialize)]
320#[serde(rename_all = "camelCase")]
321#[non_exhaustive]
322pub struct Tool {
323 pub name: String,
325 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub namespaced_name: Option<String>,
329 #[serde(default)]
331 pub description: String,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub instructions: Option<String>,
335 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
337 pub parameters: IndexMap<String, Value>,
338 #[serde(default, skip_serializing_if = "is_false")]
342 pub overrides_built_in_tool: bool,
343 #[serde(default, skip_serializing_if = "is_false")]
347 pub skip_permission: bool,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub defer: Option<DeferMode>,
355 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
360 pub metadata: IndexMap<String, Value>,
361 #[serde(skip)]
373 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
374}
375
376#[inline]
377fn is_false(b: &bool) -> bool {
378 !*b
379}
380
381#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
384#[serde(rename_all = "lowercase")]
385pub enum DeferMode {
386 Auto,
388 Never,
390}
391
392impl Tool {
393 pub fn new(name: impl Into<String>) -> Self {
413 Self {
414 name: name.into(),
415 ..Default::default()
416 }
417 }
418
419 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
422 self.namespaced_name = Some(namespaced_name.into());
423 self
424 }
425
426 pub fn with_description(mut self, description: impl Into<String>) -> Self {
428 self.description = description.into();
429 self
430 }
431
432 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
434 self.instructions = Some(instructions.into());
435 self
436 }
437
438 pub fn with_parameters(mut self, parameters: Value) -> Self {
452 self.parameters = crate::tool::tool_parameters(parameters);
453 self
454 }
455
456 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
460 self.overrides_built_in_tool = overrides;
461 self
462 }
463
464 pub fn with_skip_permission(mut self, skip: bool) -> Self {
468 self.skip_permission = skip;
469 self
470 }
471
472 pub fn with_defer(mut self, defer: DeferMode) -> Self {
476 self.defer = Some(defer);
477 self
478 }
479
480 pub fn with_metadata(mut self, metadata: IndexMap<String, Value>) -> Self {
483 self.metadata = metadata;
484 self
485 }
486
487 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
491 self.handler = Some(handler);
492 self
493 }
494
495 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
500 self.handler.as_ref()
501 }
502}
503
504impl std::fmt::Debug for Tool {
505 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
506 f.debug_struct("Tool")
507 .field("name", &self.name)
508 .field("namespaced_name", &self.namespaced_name)
509 .field("description", &self.description)
510 .field("instructions", &self.instructions)
511 .field("parameters", &self.parameters)
512 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
513 .field("skip_permission", &self.skip_permission)
514 .field("defer", &self.defer)
515 .field("metadata", &self.metadata)
516 .field(
517 "handler",
518 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
519 )
520 .finish()
521 }
522}
523
524#[non_exhaustive]
527#[derive(Debug, Clone)]
528pub struct CommandContext {
529 pub session_id: SessionId,
531 pub command: String,
533 pub command_name: String,
535 pub args: String,
537}
538
539#[async_trait::async_trait]
545pub trait CommandHandler: Send + Sync {
546 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
548}
549
550#[non_exhaustive]
556#[derive(Clone)]
557pub struct CommandDefinition {
558 pub name: String,
560 pub description: Option<String>,
562 pub handler: Arc<dyn CommandHandler>,
564}
565
566impl CommandDefinition {
567 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
570 Self {
571 name: name.into(),
572 description: None,
573 handler,
574 }
575 }
576
577 pub fn with_description(mut self, description: impl Into<String>) -> Self {
579 self.description = Some(description.into());
580 self
581 }
582}
583
584impl std::fmt::Debug for CommandDefinition {
585 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
586 f.debug_struct("CommandDefinition")
587 .field("name", &self.name)
588 .field("description", &self.description)
589 .field("handler", &"<set>")
590 .finish()
591 }
592}
593
594impl Serialize for CommandDefinition {
595 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
596 use serde::ser::SerializeStruct;
597 let len = if self.description.is_some() { 2 } else { 1 };
598 let mut state = serializer.serialize_struct("CommandDefinition", len)?;
599 state.serialize_field("name", &self.name)?;
600 if let Some(description) = &self.description {
601 state.serialize_field("description", description)?;
602 }
603 state.end()
604 }
605}
606
607#[derive(Debug, Clone, Default, Serialize, Deserialize)]
614#[serde(rename_all = "camelCase")]
615#[non_exhaustive]
616pub struct CustomAgentConfig {
617 pub name: String,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
621 pub display_name: Option<String>,
622 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub description: Option<String>,
625 #[serde(default, skip_serializing_if = "Option::is_none")]
627 pub tools: Option<Vec<String>>,
628 pub prompt: String,
630 #[serde(default, skip_serializing_if = "Option::is_none")]
632 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
633 #[serde(default, skip_serializing_if = "Option::is_none")]
635 pub infer: Option<bool>,
636 #[serde(default, skip_serializing_if = "Option::is_none")]
638 pub skills: Option<Vec<String>>,
639 #[serde(default, skip_serializing_if = "Option::is_none")]
644 pub model: Option<String>,
645 #[serde(default, skip_serializing_if = "Option::is_none")]
650 pub reasoning_effort: Option<String>,
651}
652
653impl CustomAgentConfig {
654 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
661 Self {
662 name: name.into(),
663 prompt: prompt.into(),
664 ..Self::default()
665 }
666 }
667
668 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
670 self.display_name = Some(display_name.into());
671 self
672 }
673
674 pub fn with_description(mut self, description: impl Into<String>) -> Self {
676 self.description = Some(description.into());
677 self
678 }
679
680 pub fn with_tools<I, S>(mut self, tools: I) -> Self
683 where
684 I: IntoIterator<Item = S>,
685 S: Into<String>,
686 {
687 self.tools = Some(tools.into_iter().map(Into::into).collect());
688 self
689 }
690
691 pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
693 self.mcp_servers = Some(mcp_servers);
694 self
695 }
696
697 pub fn with_infer(mut self, infer: bool) -> Self {
699 self.infer = Some(infer);
700 self
701 }
702
703 pub fn with_skills<I, S>(mut self, skills: I) -> Self
705 where
706 I: IntoIterator<Item = S>,
707 S: Into<String>,
708 {
709 self.skills = Some(skills.into_iter().map(Into::into).collect());
710 self
711 }
712
713 pub fn with_model(mut self, model: impl Into<String>) -> Self {
715 self.model = Some(model.into());
716 self
717 }
718
719 pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into<String>) -> Self {
721 self.reasoning_effort = Some(reasoning_effort.into());
722 self
723 }
724}
725
726#[derive(Debug, Clone, Default, Serialize, Deserialize)]
733#[serde(rename_all = "camelCase")]
734pub struct DefaultAgentConfig {
735 #[serde(default, skip_serializing_if = "Option::is_none")]
737 pub excluded_tools: Option<Vec<String>>,
738}
739
740#[derive(Debug, Clone, Default, Serialize, Deserialize)]
746#[serde(rename_all = "camelCase")]
747#[non_exhaustive]
748pub struct LargeToolOutputConfig {
749 #[serde(default, skip_serializing_if = "Option::is_none")]
751 pub enabled: Option<bool>,
752 #[serde(default, skip_serializing_if = "Option::is_none")]
755 pub max_size_bytes: Option<u64>,
756 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
759 pub output_directory: Option<PathBuf>,
760}
761
762impl LargeToolOutputConfig {
763 pub fn new() -> Self {
766 Self::default()
767 }
768
769 pub fn with_enabled(mut self, enabled: bool) -> Self {
771 self.enabled = Some(enabled);
772 self
773 }
774
775 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
777 self.max_size_bytes = Some(max_size_bytes);
778 self
779 }
780
781 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
783 self.output_directory = Some(output_directory.into());
784 self
785 }
786}
787
788#[derive(Debug, Clone, Default, Serialize, Deserialize)]
794#[serde(rename_all = "camelCase")]
795#[non_exhaustive]
796pub struct ToolSearchConfig {
797 #[serde(default, skip_serializing_if = "Option::is_none")]
799 pub enabled: Option<bool>,
800 #[serde(default, skip_serializing_if = "Option::is_none")]
803 pub defer_threshold: Option<u32>,
804}
805
806impl ToolSearchConfig {
807 pub fn new() -> Self {
810 Self::default()
811 }
812
813 pub fn with_enabled(mut self, enabled: bool) -> Self {
815 self.enabled = Some(enabled);
816 self
817 }
818
819 pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
822 self.defer_threshold = Some(defer_threshold);
823 self
824 }
825}
826
827#[derive(Debug, Clone, Default, Serialize, Deserialize)]
834#[serde(rename_all = "camelCase")]
835#[non_exhaustive]
836pub struct InfiniteSessionConfig {
837 #[serde(default, skip_serializing_if = "Option::is_none")]
839 pub enabled: Option<bool>,
840 #[serde(default, skip_serializing_if = "Option::is_none")]
843 pub background_compaction_threshold: Option<f64>,
844 #[serde(default, skip_serializing_if = "Option::is_none")]
847 pub buffer_exhaustion_threshold: Option<f64>,
848}
849
850impl InfiniteSessionConfig {
851 pub fn new() -> Self {
854 Self::default()
855 }
856
857 pub fn with_enabled(mut self, enabled: bool) -> Self {
860 self.enabled = Some(enabled);
861 self
862 }
863
864 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
867 self.background_compaction_threshold = Some(threshold);
868 self
869 }
870
871 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
874 self.buffer_exhaustion_threshold = Some(threshold);
875 self
876 }
877}
878
879#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
890#[serde(rename_all = "camelCase")]
891#[non_exhaustive]
892pub struct MemoryConfiguration {
893 pub enabled: bool,
895}
896
897impl MemoryConfiguration {
898 pub fn enabled() -> Self {
900 Self { enabled: true }
901 }
902
903 pub fn disabled() -> Self {
905 Self { enabled: false }
906 }
907
908 pub fn with_enabled(mut self, enabled: bool) -> Self {
910 self.enabled = enabled;
911 self
912 }
913}
914
915#[derive(Debug, Clone, Serialize, Deserialize)]
917#[serde(rename_all = "camelCase")]
918#[non_exhaustive]
919pub struct CloudSessionRepository {
920 pub owner: String,
922 pub name: String,
924 #[serde(skip_serializing_if = "Option::is_none")]
926 pub branch: Option<String>,
927}
928
929impl CloudSessionRepository {
930 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
932 Self {
933 owner: owner.into(),
934 name: name.into(),
935 branch: None,
936 }
937 }
938
939 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
941 self.branch = Some(branch.into());
942 self
943 }
944}
945
946#[derive(Debug, Clone, Default, Serialize, Deserialize)]
948#[serde(rename_all = "camelCase")]
949#[non_exhaustive]
950pub struct CloudSessionOptions {
951 #[serde(skip_serializing_if = "Option::is_none")]
953 pub repository: Option<CloudSessionRepository>,
954}
955
956impl CloudSessionOptions {
957 pub fn with_repository(repository: CloudSessionRepository) -> Self {
959 Self {
960 repository: Some(repository),
961 }
962 }
963}
964
965#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
967#[serde(rename_all = "camelCase")]
968pub struct ExtensionInfo {
969 pub source: String,
971 pub name: String,
973}
974
975impl ExtensionInfo {
976 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
978 Self {
979 source: source.into(),
980 name: name.into(),
981 }
982 }
983}
984
985#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
996#[serde(rename_all = "camelCase")]
997pub struct CanvasProviderIdentity {
998 pub id: String,
1000 #[serde(skip_serializing_if = "Option::is_none")]
1002 pub name: Option<String>,
1003}
1004
1005impl CanvasProviderIdentity {
1006 pub fn new(id: impl Into<String>) -> Self {
1008 Self {
1009 id: id.into(),
1010 name: None,
1011 }
1012 }
1013
1014 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1016 self.name = Some(name.into());
1017 self
1018 }
1019}
1020
1021#[derive(Debug, Clone, Serialize, Deserialize)]
1055#[serde(tag = "type", rename_all = "lowercase")]
1056#[non_exhaustive]
1057pub enum McpServerConfig {
1058 #[serde(alias = "local")]
1062 Stdio(McpStdioServerConfig),
1063 Http(McpHttpServerConfig),
1065 Sse(McpHttpServerConfig),
1067}
1068
1069#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1073#[serde(rename_all = "camelCase")]
1074pub struct McpStdioServerConfig {
1075 #[serde(default, skip_serializing_if = "Option::is_none")]
1081 pub tools: Option<Vec<String>>,
1082 #[serde(default, skip_serializing_if = "Option::is_none")]
1084 pub timeout: Option<i64>,
1085 pub command: String,
1087 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1089 pub args: Vec<String>,
1090 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1093 pub env: HashMap<String, String>,
1094 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1096 pub working_directory: Option<String>,
1097}
1098
1099#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1103#[serde(rename_all = "camelCase")]
1104pub struct McpHttpServerConfig {
1105 #[serde(default, skip_serializing_if = "Option::is_none")]
1111 pub tools: Option<Vec<String>>,
1112 #[serde(default, skip_serializing_if = "Option::is_none")]
1114 pub timeout: Option<i64>,
1115 pub url: String,
1117 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1119 pub headers: HashMap<String, String>,
1120}
1121
1122#[derive(Clone, Default, Serialize, Deserialize)]
1128#[serde(rename_all = "camelCase")]
1129#[non_exhaustive]
1130pub struct ProviderConfig {
1131 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1134 pub provider_type: Option<String>,
1135 #[serde(default, skip_serializing_if = "Option::is_none")]
1138 pub wire_api: Option<String>,
1139 #[serde(default, skip_serializing_if = "Option::is_none")]
1144 pub transport: Option<String>,
1145 pub base_url: String,
1147 #[serde(default, skip_serializing_if = "Option::is_none")]
1149 pub api_key: Option<String>,
1150 #[serde(default, skip_serializing_if = "Option::is_none")]
1154 pub bearer_token: Option<String>,
1155 #[serde(skip)]
1158 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1159 #[serde(default, skip_serializing_if = "Option::is_none")]
1160 pub(crate) has_bearer_token_provider: Option<bool>,
1161 #[serde(default, skip_serializing_if = "Option::is_none")]
1163 pub azure: Option<AzureProviderOptions>,
1164 #[serde(default, skip_serializing_if = "Option::is_none")]
1166 pub headers: Option<HashMap<String, String>>,
1167 #[serde(default, skip_serializing_if = "Option::is_none")]
1171 pub model_id: Option<String>,
1172 #[serde(default, skip_serializing_if = "Option::is_none")]
1179 pub wire_model: Option<String>,
1180 #[serde(default, skip_serializing_if = "Option::is_none")]
1185 pub max_prompt_tokens: Option<i64>,
1186 #[serde(default, skip_serializing_if = "Option::is_none")]
1189 pub max_output_tokens: Option<i64>,
1190}
1191
1192impl std::fmt::Debug for ProviderConfig {
1193 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1194 f.debug_struct("ProviderConfig")
1195 .field("provider_type", &self.provider_type)
1196 .field("wire_api", &self.wire_api)
1197 .field("transport", &self.transport)
1198 .field("base_url", &self.base_url)
1199 .field("api_key", &self.api_key)
1200 .field("bearer_token", &self.bearer_token)
1201 .field(
1202 "bearer_token_provider",
1203 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1204 )
1205 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1206 .field("azure", &self.azure)
1207 .field("headers", &self.headers)
1208 .field("model_id", &self.model_id)
1209 .field("wire_model", &self.wire_model)
1210 .field("max_prompt_tokens", &self.max_prompt_tokens)
1211 .field("max_output_tokens", &self.max_output_tokens)
1212 .finish()
1213 }
1214}
1215
1216impl ProviderConfig {
1217 pub fn new(base_url: impl Into<String>) -> Self {
1220 Self {
1221 base_url: base_url.into(),
1222 ..Self::default()
1223 }
1224 }
1225
1226 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1228 self.provider_type = Some(provider_type.into());
1229 self
1230 }
1231
1232 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1234 self.wire_api = Some(wire_api.into());
1235 self
1236 }
1237
1238 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1241 self.transport = Some(transport.into());
1242 self
1243 }
1244
1245 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1247 self.api_key = Some(api_key.into());
1248 self
1249 }
1250
1251 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1254 self.bearer_token = Some(bearer_token.into());
1255 self
1256 }
1257
1258 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1264 self.bearer_token_provider = Some(provider);
1265 self
1266 }
1267
1268 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1270 self.azure = Some(azure);
1271 self
1272 }
1273
1274 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1276 self.headers = Some(headers);
1277 self
1278 }
1279
1280 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1283 self.model_id = Some(model_id.into());
1284 self
1285 }
1286
1287 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1292 self.wire_model = Some(wire_model.into());
1293 self
1294 }
1295
1296 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1300 self.max_prompt_tokens = Some(max);
1301 self
1302 }
1303
1304 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1307 self.max_output_tokens = Some(max);
1308 self
1309 }
1310}
1311
1312#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1325#[serde(rename_all = "camelCase")]
1326#[non_exhaustive]
1327pub struct CapiSessionOptions {
1328 #[serde(default, skip_serializing_if = "Option::is_none")]
1334 pub enable_web_socket_responses: Option<bool>,
1335}
1336
1337impl CapiSessionOptions {
1338 pub fn new() -> Self {
1340 Self::default()
1341 }
1342
1343 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1345 self.enable_web_socket_responses = Some(enable);
1346 self
1347 }
1348}
1349
1350#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1352#[serde(rename_all = "camelCase")]
1353pub struct AzureProviderOptions {
1354 #[serde(default, skip_serializing_if = "Option::is_none")]
1356 pub api_version: Option<String>,
1357}
1358
1359#[derive(Clone, Default, Serialize, Deserialize)]
1370#[serde(rename_all = "camelCase")]
1371#[non_exhaustive]
1372pub struct NamedProviderConfig {
1373 pub name: String,
1376 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1379 pub provider_type: Option<String>,
1380 #[serde(default, skip_serializing_if = "Option::is_none")]
1383 pub wire_api: Option<String>,
1384 pub base_url: String,
1386 #[serde(default, skip_serializing_if = "Option::is_none")]
1388 pub api_key: Option<String>,
1389 #[serde(default, skip_serializing_if = "Option::is_none")]
1392 pub bearer_token: Option<String>,
1393 #[serde(skip)]
1396 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1397 #[serde(default, skip_serializing_if = "Option::is_none")]
1398 pub(crate) has_bearer_token_provider: Option<bool>,
1399 #[serde(default, skip_serializing_if = "Option::is_none")]
1401 pub azure: Option<AzureProviderOptions>,
1402 #[serde(default, skip_serializing_if = "Option::is_none")]
1404 pub headers: Option<HashMap<String, String>>,
1405}
1406
1407impl std::fmt::Debug for NamedProviderConfig {
1408 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1409 f.debug_struct("NamedProviderConfig")
1410 .field("name", &self.name)
1411 .field("provider_type", &self.provider_type)
1412 .field("wire_api", &self.wire_api)
1413 .field("base_url", &self.base_url)
1414 .field("api_key", &self.api_key)
1415 .field("bearer_token", &self.bearer_token)
1416 .field(
1417 "bearer_token_provider",
1418 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1419 )
1420 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1421 .field("azure", &self.azure)
1422 .field("headers", &self.headers)
1423 .finish()
1424 }
1425}
1426
1427impl NamedProviderConfig {
1428 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1431 Self {
1432 name: name.into(),
1433 base_url: base_url.into(),
1434 ..Self::default()
1435 }
1436 }
1437
1438 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1440 self.provider_type = Some(provider_type.into());
1441 self
1442 }
1443
1444 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1446 self.wire_api = Some(wire_api.into());
1447 self
1448 }
1449
1450 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1452 self.api_key = Some(api_key.into());
1453 self
1454 }
1455
1456 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1459 self.bearer_token = Some(bearer_token.into());
1460 self
1461 }
1462
1463 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1469 self.bearer_token_provider = Some(provider);
1470 self
1471 }
1472
1473 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1475 self.azure = Some(azure);
1476 self
1477 }
1478
1479 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1481 self.headers = Some(headers);
1482 self
1483 }
1484}
1485
1486fn prepare_bearer_token_providers(
1487 provider: &mut Option<ProviderConfig>,
1488 providers: &mut Option<Vec<NamedProviderConfig>>,
1489) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1490 let mut bearer_token_providers = HashMap::new();
1491
1492 if let Some(provider) = provider.as_mut()
1493 && let Some(token_provider) = provider.bearer_token_provider.take()
1494 {
1495 provider.has_bearer_token_provider = Some(true);
1496 bearer_token_providers.insert("default".to_string(), token_provider);
1497 }
1498
1499 if let Some(providers) = providers.as_mut() {
1500 for provider in providers {
1501 if let Some(token_provider) = provider.bearer_token_provider.take() {
1502 provider.has_bearer_token_provider = Some(true);
1503 bearer_token_providers.insert(provider.name.clone(), token_provider);
1504 }
1505 }
1506 }
1507
1508 bearer_token_providers
1509}
1510
1511#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1519#[serde(rename_all = "camelCase")]
1520#[non_exhaustive]
1521pub struct ProviderModelConfig {
1522 pub id: String,
1525 pub provider: String,
1527 #[serde(default, skip_serializing_if = "Option::is_none")]
1530 pub wire_model: Option<String>,
1531 #[serde(default, skip_serializing_if = "Option::is_none")]
1534 pub model_id: Option<String>,
1535 #[serde(default, skip_serializing_if = "Option::is_none")]
1537 pub name: Option<String>,
1538 #[serde(default, skip_serializing_if = "Option::is_none")]
1540 pub max_prompt_tokens: Option<i64>,
1541 #[serde(default, skip_serializing_if = "Option::is_none")]
1543 pub max_context_window_tokens: Option<i64>,
1544 #[serde(default, skip_serializing_if = "Option::is_none")]
1546 pub max_output_tokens: Option<i64>,
1547 #[serde(default, skip_serializing_if = "Option::is_none")]
1550 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1551}
1552
1553impl ProviderModelConfig {
1554 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1557 Self {
1558 id: id.into(),
1559 provider: provider.into(),
1560 ..Self::default()
1561 }
1562 }
1563
1564 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1566 self.wire_model = Some(wire_model.into());
1567 self
1568 }
1569
1570 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1573 self.model_id = Some(model_id.into());
1574 self
1575 }
1576
1577 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1579 self.name = Some(name.into());
1580 self
1581 }
1582
1583 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1585 self.max_prompt_tokens = Some(max);
1586 self
1587 }
1588
1589 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1591 self.max_context_window_tokens = Some(max);
1592 self
1593 }
1594
1595 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1597 self.max_output_tokens = Some(max);
1598 self
1599 }
1600
1601 pub fn with_capabilities(
1603 mut self,
1604 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1605 ) -> Self {
1606 self.capabilities = Some(capabilities);
1607 self
1608 }
1609}
1610
1611#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1615#[serde(untagged)]
1616pub enum ExpFlagValue {
1617 Bool(bool),
1619 Integer(i64),
1621 Float(f64),
1623 String(String),
1625 Null,
1627}
1628
1629#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1633#[serde(rename_all = "PascalCase")]
1634pub struct ExpConfigEntry {
1635 pub id: String,
1637 pub parameters: HashMap<String, ExpFlagValue>,
1639}
1640
1641#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1647#[serde(rename_all = "PascalCase")]
1648pub struct CopilotExpAssignmentResponse {
1649 #[serde(default)]
1651 pub features: Vec<String>,
1652 #[serde(default)]
1654 pub flights: HashMap<String, String>,
1655 #[serde(default)]
1657 pub configs: Vec<ExpConfigEntry>,
1658 #[serde(default, skip_serializing_if = "Option::is_none")]
1660 pub parameter_groups: Option<Value>,
1661 #[serde(default, skip_serializing_if = "Option::is_none")]
1663 pub flighting_version: Option<i64>,
1664 #[serde(default, skip_serializing_if = "Option::is_none")]
1666 pub impression_id: Option<String>,
1667 #[serde(default)]
1669 pub assignment_context: String,
1670}
1671
1672#[derive(Clone)]
1724#[non_exhaustive]
1725pub struct SessionConfig {
1726 pub session_id: Option<SessionId>,
1728 pub model: Option<String>,
1730 pub client_name: Option<String>,
1732 pub reasoning_effort: Option<String>,
1734 pub reasoning_summary: Option<ReasoningSummary>,
1738 pub context_tier: Option<String>,
1741 pub streaming: Option<bool>,
1743 pub system_message: Option<SystemMessageConfig>,
1745 pub tools: Option<Vec<Tool>>,
1747 pub canvases: Option<Vec<CanvasDeclaration>>,
1749 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1754 pub request_canvas_renderer: Option<bool>,
1756 pub request_extensions: Option<bool>,
1758 pub extension_sdk_path: Option<String>,
1762 pub extension_info: Option<ExtensionInfo>,
1764 pub canvas_provider: Option<CanvasProviderIdentity>,
1767 pub available_tools: Option<Vec<String>>,
1769 pub excluded_tools: Option<Vec<String>>,
1771 pub excluded_builtin_agents: Option<Vec<String>>,
1777 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
1779 pub mcp_oauth_token_storage: Option<String>,
1788 pub enable_config_discovery: Option<bool>,
1790 pub skip_embedding_retrieval: Option<bool>,
1792 pub embedding_cache_storage: Option<String>,
1795 pub organization_custom_instructions: Option<String>,
1797 pub enable_on_demand_instruction_discovery: Option<bool>,
1799 pub enable_file_hooks: Option<bool>,
1801 pub enable_host_git_operations: Option<bool>,
1803 pub enable_session_store: Option<bool>,
1805 pub enable_skills: Option<bool>,
1807 pub enable_mcp_apps: Option<bool>,
1834 pub skill_directories: Option<Vec<PathBuf>>,
1836 pub instruction_directories: Option<Vec<PathBuf>>,
1839 pub plugin_directories: Option<Vec<PathBuf>>,
1841 pub large_output: Option<LargeToolOutputConfig>,
1843 pub tool_search: Option<ToolSearchConfig>,
1847 pub disabled_skills: Option<Vec<String>>,
1850 pub hooks: Option<bool>,
1854 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1856 pub default_agent: Option<DefaultAgentConfig>,
1860 pub agent: Option<String>,
1863 pub infinite_sessions: Option<InfiniteSessionConfig>,
1866 pub provider: Option<ProviderConfig>,
1870 pub capi: Option<CapiSessionOptions>,
1876 pub providers: Option<Vec<NamedProviderConfig>>,
1883 pub models: Option<Vec<ProviderModelConfig>>,
1889 pub enable_session_telemetry: Option<bool>,
1897 pub enable_citations: Option<bool>,
1899 pub session_limits: Option<SessionLimitsConfig>,
1901 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1904 pub memory: Option<MemoryConfiguration>,
1906 pub config_directory: Option<PathBuf>,
1909 pub working_directory: Option<PathBuf>,
1912 pub github_token: Option<String>,
1918 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
1924 pub cloud: Option<CloudSessionOptions>,
1927 pub include_sub_agent_streaming_events: Option<bool>,
1931 pub commands: Option<Vec<CommandDefinition>>,
1935 #[doc(hidden)]
1942 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
1943 pub enable_managed_settings: Option<bool>,
1950 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1955 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
1959 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
1962 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
1965 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
1969 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
1972 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
1975 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1979 pub(crate) permission_policy: Option<crate::permission::Policy>,
1983 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
1988 pub skip_custom_instructions: Option<bool>,
1992 pub custom_agents_local_only: Option<bool>,
1996 pub coauthor_enabled: Option<bool>,
2000 pub manage_schedule_enabled: Option<bool>,
2004}
2005
2006impl std::fmt::Debug for SessionConfig {
2007 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2008 f.debug_struct("SessionConfig")
2009 .field("session_id", &self.session_id)
2010 .field("model", &self.model)
2011 .field("client_name", &self.client_name)
2012 .field("reasoning_effort", &self.reasoning_effort)
2013 .field("reasoning_summary", &self.reasoning_summary)
2014 .field("context_tier", &self.context_tier)
2015 .field("streaming", &self.streaming)
2016 .field("system_message", &self.system_message)
2017 .field("tools", &self.tools)
2018 .field("canvases", &self.canvases)
2019 .field(
2020 "canvas_handler",
2021 &self.canvas_handler.as_ref().map(|_| "<set>"),
2022 )
2023 .field("request_canvas_renderer", &self.request_canvas_renderer)
2024 .field("request_extensions", &self.request_extensions)
2025 .field("extension_sdk_path", &self.extension_sdk_path)
2026 .field("extension_info", &self.extension_info)
2027 .field("canvas_provider", &self.canvas_provider)
2028 .field("available_tools", &self.available_tools)
2029 .field("excluded_tools", &self.excluded_tools)
2030 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2031 .field("mcp_servers", &self.mcp_servers)
2032 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2033 .field("embedding_cache_storage", &self.embedding_cache_storage)
2034 .field("enable_config_discovery", &self.enable_config_discovery)
2035 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2036 .field(
2037 "organization_custom_instructions",
2038 &self
2039 .organization_custom_instructions
2040 .as_ref()
2041 .map(|_| "<redacted>"),
2042 )
2043 .field(
2044 "enable_on_demand_instruction_discovery",
2045 &self.enable_on_demand_instruction_discovery,
2046 )
2047 .field("enable_file_hooks", &self.enable_file_hooks)
2048 .field(
2049 "enable_host_git_operations",
2050 &self.enable_host_git_operations,
2051 )
2052 .field("enable_session_store", &self.enable_session_store)
2053 .field("enable_skills", &self.enable_skills)
2054 .field("enable_mcp_apps", &self.enable_mcp_apps)
2055 .field("skill_directories", &self.skill_directories)
2056 .field("instruction_directories", &self.instruction_directories)
2057 .field("plugin_directories", &self.plugin_directories)
2058 .field("large_output", &self.large_output)
2059 .field("tool_search", &self.tool_search)
2060 .field("disabled_skills", &self.disabled_skills)
2061 .field("hooks", &self.hooks)
2062 .field("custom_agents", &self.custom_agents)
2063 .field("default_agent", &self.default_agent)
2064 .field("agent", &self.agent)
2065 .field("infinite_sessions", &self.infinite_sessions)
2066 .field("provider", &self.provider)
2067 .field("capi", &self.capi)
2068 .field("enable_session_telemetry", &self.enable_session_telemetry)
2069 .field("enable_citations", &self.enable_citations)
2070 .field("session_limits", &self.session_limits)
2071 .field("model_capabilities", &self.model_capabilities)
2072 .field("memory", &self.memory)
2073 .field("config_directory", &self.config_directory)
2074 .field("working_directory", &self.working_directory)
2075 .field(
2076 "github_token",
2077 &self.github_token.as_ref().map(|_| "<redacted>"),
2078 )
2079 .field("remote_session", &self.remote_session)
2080 .field("cloud", &self.cloud)
2081 .field(
2082 "include_sub_agent_streaming_events",
2083 &self.include_sub_agent_streaming_events,
2084 )
2085 .field("commands", &self.commands)
2086 .field("exp_assignments", &self.exp_assignments)
2087 .field("enable_managed_settings", &self.enable_managed_settings)
2088 .field(
2089 "session_fs_provider",
2090 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2091 )
2092 .field(
2093 "permission_handler",
2094 &self.permission_handler.as_ref().map(|_| "<set>"),
2095 )
2096 .field(
2097 "elicitation_handler",
2098 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2099 )
2100 .field(
2101 "mcp_auth_handler",
2102 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2103 )
2104 .field(
2105 "user_input_handler",
2106 &self.user_input_handler.as_ref().map(|_| "<set>"),
2107 )
2108 .field(
2109 "exit_plan_mode_handler",
2110 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2111 )
2112 .field(
2113 "auto_mode_switch_handler",
2114 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2115 )
2116 .field(
2117 "hooks_handler",
2118 &self.hooks_handler.as_ref().map(|_| "<set>"),
2119 )
2120 .field(
2121 "system_message_transform",
2122 &self.system_message_transform.as_ref().map(|_| "<set>"),
2123 )
2124 .finish()
2125 }
2126}
2127
2128impl Default for SessionConfig {
2129 fn default() -> Self {
2135 Self {
2136 session_id: None,
2137 model: None,
2138 client_name: None,
2139 reasoning_effort: None,
2140 reasoning_summary: None,
2141 context_tier: None,
2142 streaming: None,
2143 system_message: None,
2144 tools: None,
2145 canvases: None,
2146 canvas_handler: None,
2147 request_canvas_renderer: None,
2148 request_extensions: None,
2149 extension_sdk_path: None,
2150 extension_info: None,
2151 canvas_provider: None,
2152 available_tools: None,
2153 excluded_tools: None,
2154 excluded_builtin_agents: None,
2155 mcp_servers: None,
2156 mcp_oauth_token_storage: None,
2157 enable_config_discovery: None,
2158 skip_embedding_retrieval: None,
2159 organization_custom_instructions: None,
2160 enable_on_demand_instruction_discovery: None,
2161 enable_file_hooks: None,
2162 enable_host_git_operations: None,
2163 enable_session_store: None,
2164 enable_skills: None,
2165 embedding_cache_storage: None,
2166 enable_mcp_apps: None,
2167 skill_directories: None,
2168 instruction_directories: None,
2169 plugin_directories: None,
2170 large_output: None,
2171 tool_search: None,
2172 disabled_skills: None,
2173 hooks: None,
2174 custom_agents: None,
2175 default_agent: None,
2176 agent: None,
2177 infinite_sessions: None,
2178 provider: None,
2179 capi: None,
2180 providers: None,
2181 models: None,
2182 enable_session_telemetry: None,
2183 enable_citations: None,
2184 session_limits: None,
2185 model_capabilities: None,
2186 memory: None,
2187 config_directory: None,
2188 working_directory: None,
2189 github_token: None,
2190 remote_session: None,
2191 cloud: None,
2192 include_sub_agent_streaming_events: None,
2193 commands: None,
2194 exp_assignments: None,
2195 enable_managed_settings: None,
2196 session_fs_provider: None,
2197 permission_handler: None,
2198 elicitation_handler: None,
2199 mcp_auth_handler: None,
2200 user_input_handler: None,
2201 exit_plan_mode_handler: None,
2202 auto_mode_switch_handler: None,
2203 hooks_handler: None,
2204 permission_policy: None,
2205 system_message_transform: None,
2206 skip_custom_instructions: None,
2207 custom_agents_local_only: None,
2208 coauthor_enabled: None,
2209 manage_schedule_enabled: None,
2210 }
2211 }
2212}
2213
2214pub(crate) struct SessionConfigRuntime {
2220 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2221 pub permission_policy: Option<crate::permission::Policy>,
2222 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2223 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2224 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2225 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2226 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2227 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2228 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2229 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2230 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2231 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2232 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2233 pub commands: Option<Vec<CommandDefinition>>,
2234}
2235
2236impl SessionConfig {
2237 pub(crate) fn into_wire(
2249 mut self,
2250 session_id: Option<SessionId>,
2251 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2252 let permission_active =
2253 self.permission_handler.is_some() || self.permission_policy.is_some();
2254 let request_user_input = self.user_input_handler.is_some();
2255 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2256 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2257 let request_elicitation = self.elicitation_handler.is_some();
2258 let hooks_flag = self.hooks_handler.is_some();
2259
2260 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2261 if let Some(tools) = self.tools.as_mut() {
2262 for tool in tools.iter_mut() {
2263 if let Some(handler) = tool.handler.take()
2264 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2265 {
2266 return Err(crate::Error::with_message(
2267 crate::ErrorKind::InvalidConfig,
2268 format!("duplicate tool handler registered for name {:?}", tool.name),
2269 ));
2270 }
2271 }
2272 }
2273
2274 let wire_commands = self.commands.as_ref().map(|cmds| {
2275 cmds.iter()
2276 .map(|c| crate::wire::CommandWireDefinition {
2277 name: c.name.clone(),
2278 description: c.description.clone(),
2279 })
2280 .collect()
2281 });
2282 let wire_canvases = self.canvases.clone();
2283 let canvas_handler = self.canvas_handler.clone();
2284 let bearer_token_providers =
2285 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2286
2287 let wire = crate::wire::SessionCreateWire {
2288 session_id,
2289 model: self.model,
2290 client_name: self.client_name,
2291 reasoning_effort: self.reasoning_effort,
2292 reasoning_summary: self.reasoning_summary,
2293 context_tier: self.context_tier,
2294 streaming: self.streaming,
2295 system_message: self.system_message,
2296 tools: self.tools,
2297 canvases: wire_canvases,
2298 request_canvas_renderer: self.request_canvas_renderer,
2299 request_extensions: self.request_extensions,
2300 extension_sdk_path: self.extension_sdk_path,
2301 extension_info: self.extension_info,
2302 canvas_provider: self.canvas_provider,
2303 available_tools: self.available_tools,
2304 excluded_tools: self.excluded_tools,
2305 excluded_builtin_agents: self.excluded_builtin_agents,
2306 tool_filter_precedence: "excluded",
2307 mcp_servers: self.mcp_servers,
2308 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2309 embedding_cache_storage: self.embedding_cache_storage,
2310 env_value_mode: "direct",
2311 enable_config_discovery: self.enable_config_discovery,
2312 skip_embedding_retrieval: self.skip_embedding_retrieval,
2313 organization_custom_instructions: self.organization_custom_instructions,
2314 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2315 enable_file_hooks: self.enable_file_hooks,
2316 enable_host_git_operations: self.enable_host_git_operations,
2317 enable_session_store: self.enable_session_store,
2318 enable_skills: self.enable_skills,
2319 request_user_input,
2320 request_permission: permission_active,
2321 request_exit_plan_mode,
2322 request_auto_mode_switch,
2323 request_elicitation,
2324 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2325 hooks: hooks_flag,
2326 skill_directories: self.skill_directories,
2327 instruction_directories: self.instruction_directories,
2328 plugin_directories: self.plugin_directories,
2329 large_output: self.large_output,
2330 tool_search: self.tool_search,
2331 disabled_skills: self.disabled_skills,
2332 custom_agents: self.custom_agents,
2333 default_agent: self.default_agent,
2334 agent: self.agent,
2335 infinite_sessions: self.infinite_sessions,
2336 provider: self.provider,
2337 capi: self.capi,
2338 providers: self.providers,
2339 models: self.models,
2340 enable_session_telemetry: self.enable_session_telemetry,
2341 enable_citations: self.enable_citations,
2342 session_limits: self.session_limits,
2343 model_capabilities: self.model_capabilities,
2344 memory: self.memory,
2345 config_dir: self.config_directory,
2346 working_directory: self.working_directory,
2347 github_token: self.github_token,
2348 remote_session: self.remote_session,
2349 cloud: self.cloud,
2350 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2351 enable_github_telemetry_forwarding: None,
2352 commands: wire_commands,
2353 exp_assignments: self.exp_assignments,
2354 enable_managed_settings: self.enable_managed_settings,
2355 };
2356
2357 let runtime = SessionConfigRuntime {
2358 permission_handler: self.permission_handler,
2359 permission_policy: self.permission_policy,
2360 elicitation_handler: self.elicitation_handler,
2361 mcp_auth_handler: self.mcp_auth_handler,
2362 user_input_handler: self.user_input_handler,
2363 exit_plan_mode_handler: self.exit_plan_mode_handler,
2364 auto_mode_switch_handler: self.auto_mode_switch_handler,
2365 hooks_handler: self.hooks_handler,
2366 system_message_transform: self.system_message_transform,
2367 tool_handlers,
2368 canvas_handler,
2369 session_fs_provider: self.session_fs_provider,
2370 bearer_token_providers,
2371 commands: self.commands,
2372 };
2373
2374 Ok((wire, runtime))
2375 }
2376
2377 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2381 self.permission_handler = Some(handler);
2382 self
2383 }
2384
2385 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2388 self.elicitation_handler = Some(handler);
2389 self
2390 }
2391
2392 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2394 self.mcp_auth_handler = Some(handler);
2395 self
2396 }
2397
2398 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2401 self.user_input_handler = Some(handler);
2402 self
2403 }
2404
2405 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2407 self.exit_plan_mode_handler = Some(handler);
2408 self
2409 }
2410
2411 pub fn with_auto_mode_switch_handler(
2413 mut self,
2414 handler: Arc<dyn AutoModeSwitchHandler>,
2415 ) -> Self {
2416 self.auto_mode_switch_handler = Some(handler);
2417 self
2418 }
2419
2420 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2425 self.commands = Some(commands);
2426 self
2427 }
2428
2429 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2433 self.session_fs_provider = Some(provider);
2434 self
2435 }
2436
2437 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2440 self.hooks_handler = Some(hooks);
2441 self
2442 }
2443
2444 pub fn with_system_message_transform(
2448 mut self,
2449 transform: Arc<dyn SystemMessageTransform>,
2450 ) -> Self {
2451 self.system_message_transform = Some(transform);
2452 self
2453 }
2454
2455 pub fn approve_all_permissions(mut self) -> Self {
2461 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2462 self
2463 }
2464
2465 pub fn deny_all_permissions(mut self) -> Self {
2468 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2469 self
2470 }
2471
2472 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2477 where
2478 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2479 {
2480 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2481 self
2482 }
2483
2484 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2486 self.session_id = Some(id.into());
2487 self
2488 }
2489
2490 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2492 self.model = Some(model.into());
2493 self
2494 }
2495
2496 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2498 self.client_name = Some(name.into());
2499 self
2500 }
2501
2502 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2504 self.reasoning_effort = Some(effort.into());
2505 self
2506 }
2507
2508 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2510 self.reasoning_summary = Some(summary);
2511 self
2512 }
2513
2514 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2516 self.context_tier = Some(tier.into());
2517 self
2518 }
2519
2520 pub fn with_streaming(mut self, streaming: bool) -> Self {
2522 self.streaming = Some(streaming);
2523 self
2524 }
2525
2526 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2528 self.system_message = Some(system_message);
2529 self
2530 }
2531
2532 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2534 self.tools = Some(tools.into_iter().collect());
2535 self
2536 }
2537
2538 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2543 self.canvases = Some(canvases.into_iter().collect());
2544 self
2545 }
2546
2547 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2549 self.canvas_handler = Some(handler);
2550 self
2551 }
2552
2553 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2555 self.request_canvas_renderer = Some(request);
2556 self
2557 }
2558
2559 pub fn with_request_extensions(mut self, request: bool) -> Self {
2561 self.request_extensions = Some(request);
2562 self
2563 }
2564
2565 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2569 self.extension_sdk_path = Some(path.into());
2570 self
2571 }
2572
2573 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2575 self.extension_info = Some(extension_info);
2576 self
2577 }
2578
2579 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2582 self.canvas_provider = Some(canvas_provider);
2583 self
2584 }
2585
2586 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2588 where
2589 I: IntoIterator<Item = S>,
2590 S: Into<String>,
2591 {
2592 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2593 self
2594 }
2595
2596 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2598 where
2599 I: IntoIterator<Item = S>,
2600 S: Into<String>,
2601 {
2602 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2603 self
2604 }
2605
2606 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2608 where
2609 I: IntoIterator<Item = S>,
2610 S: Into<String>,
2611 {
2612 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2613 self
2614 }
2615
2616 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2618 self.mcp_servers = Some(servers);
2619 self
2620 }
2621
2622 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2630 self.mcp_oauth_token_storage = Some(mode.into());
2631 self
2632 }
2633
2634 pub fn with_embedding_cache_storage(
2636 mut self,
2637 embedding_cache_storage: impl Into<String>,
2638 ) -> Self {
2639 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2640 self
2641 }
2642
2643 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2645 self.enable_config_discovery = Some(enable);
2646 self
2647 }
2648
2649 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2651 self.skip_embedding_retrieval = Some(value);
2652 self
2653 }
2654
2655 pub fn with_organization_custom_instructions(
2657 mut self,
2658 instructions: impl Into<String>,
2659 ) -> Self {
2660 self.organization_custom_instructions = Some(instructions.into());
2661 self
2662 }
2663
2664 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2666 self.enable_on_demand_instruction_discovery = Some(value);
2667 self
2668 }
2669
2670 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2672 self.enable_file_hooks = Some(value);
2673 self
2674 }
2675
2676 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2678 self.enable_host_git_operations = Some(value);
2679 self
2680 }
2681
2682 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2684 self.enable_session_store = Some(value);
2685 self
2686 }
2687
2688 pub fn with_enable_skills(mut self, value: bool) -> Self {
2690 self.enable_skills = Some(value);
2691 self
2692 }
2693
2694 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2700 self.enable_mcp_apps = Some(enable);
2701 self
2702 }
2703
2704 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2706 where
2707 I: IntoIterator<Item = P>,
2708 P: Into<PathBuf>,
2709 {
2710 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2711 self
2712 }
2713
2714 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2718 where
2719 I: IntoIterator<Item = P>,
2720 P: Into<PathBuf>,
2721 {
2722 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2723 self
2724 }
2725
2726 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2728 where
2729 I: IntoIterator<Item = P>,
2730 P: Into<PathBuf>,
2731 {
2732 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2733 self
2734 }
2735
2736 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2738 self.large_output = Some(config);
2739 self
2740 }
2741
2742 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
2745 self.tool_search = Some(config);
2746 self
2747 }
2748
2749 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2751 where
2752 I: IntoIterator<Item = S>,
2753 S: Into<String>,
2754 {
2755 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2756 self
2757 }
2758
2759 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2761 mut self,
2762 agents: I,
2763 ) -> Self {
2764 self.custom_agents = Some(agents.into_iter().collect());
2765 self
2766 }
2767
2768 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2770 self.default_agent = Some(agent);
2771 self
2772 }
2773
2774 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2777 self.agent = Some(name.into());
2778 self
2779 }
2780
2781 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2784 self.infinite_sessions = Some(config);
2785 self
2786 }
2787
2788 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2790 self.provider = Some(provider);
2791 self
2792 }
2793
2794 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
2796 self.capi = Some(capi);
2797 self
2798 }
2799
2800 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
2806 self.providers = Some(providers);
2807 self
2808 }
2809
2810 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
2816 self.models = Some(models);
2817 self
2818 }
2819
2820 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2824 self.enable_session_telemetry = Some(enable);
2825 self
2826 }
2827
2828 pub fn with_enable_citations(mut self, enable: bool) -> Self {
2830 self.enable_citations = Some(enable);
2831 self
2832 }
2833
2834 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
2836 self.session_limits = Some(limits);
2837 self
2838 }
2839
2840 pub fn with_model_capabilities(
2842 mut self,
2843 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2844 ) -> Self {
2845 self.model_capabilities = Some(capabilities);
2846 self
2847 }
2848
2849 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
2851 self.memory = Some(memory);
2852 self
2853 }
2854
2855 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2857 self.config_directory = Some(dir.into());
2858 self
2859 }
2860
2861 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2864 self.working_directory = Some(dir.into());
2865 self
2866 }
2867
2868 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2873 self.github_token = Some(token.into());
2874 self
2875 }
2876
2877 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2880 self.include_sub_agent_streaming_events = Some(include);
2881 self
2882 }
2883
2884 pub fn with_remote_session(
2886 mut self,
2887 mode: crate::generated::api_types::RemoteSessionMode,
2888 ) -> Self {
2889 self.remote_session = Some(mode);
2890 self
2891 }
2892
2893 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
2895 self.cloud = Some(cloud);
2896 self
2897 }
2898
2899 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
2901 self.skip_custom_instructions = Some(value);
2902 self
2903 }
2904
2905 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
2907 self.custom_agents_local_only = Some(value);
2908 self
2909 }
2910
2911 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
2913 self.coauthor_enabled = Some(value);
2914 self
2915 }
2916
2917 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
2919 self.manage_schedule_enabled = Some(value);
2920 self
2921 }
2922
2923 #[doc(hidden)]
2931 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
2932 self.exp_assignments = Some(assignments);
2933 self
2934 }
2935
2936 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
2942 self.enable_managed_settings = Some(enabled);
2943 self
2944 }
2945}
2946#[derive(Clone)]
2953#[non_exhaustive]
2954pub struct ResumeSessionConfig {
2955 pub session_id: SessionId,
2957 pub model: Option<String>,
2960 pub client_name: Option<String>,
2962 pub reasoning_effort: Option<String>,
2964 pub reasoning_summary: Option<ReasoningSummary>,
2968 pub context_tier: Option<String>,
2971 pub streaming: Option<bool>,
2973 pub system_message: Option<SystemMessageConfig>,
2976 pub tools: Option<Vec<Tool>>,
2978 pub canvases: Option<Vec<CanvasDeclaration>>,
2980 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2983 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
2985 pub request_canvas_renderer: Option<bool>,
2987 pub request_extensions: Option<bool>,
2989 pub extension_sdk_path: Option<String>,
2993 pub extension_info: Option<ExtensionInfo>,
2995 pub canvas_provider: Option<CanvasProviderIdentity>,
2998 pub available_tools: Option<Vec<String>>,
3000 pub excluded_tools: Option<Vec<String>>,
3002 pub excluded_builtin_agents: Option<Vec<String>>,
3008 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3010 pub mcp_oauth_token_storage: Option<String>,
3013 pub enable_config_discovery: Option<bool>,
3015 pub skip_embedding_retrieval: Option<bool>,
3017 pub embedding_cache_storage: Option<String>,
3019 pub organization_custom_instructions: Option<String>,
3021 pub enable_on_demand_instruction_discovery: Option<bool>,
3023 pub enable_file_hooks: Option<bool>,
3025 pub enable_host_git_operations: Option<bool>,
3027 pub enable_session_store: Option<bool>,
3029 pub enable_skills: Option<bool>,
3031 pub enable_mcp_apps: Option<bool>,
3037 pub skill_directories: Option<Vec<PathBuf>>,
3039 pub instruction_directories: Option<Vec<PathBuf>>,
3042 pub plugin_directories: Option<Vec<PathBuf>>,
3044 pub large_output: Option<LargeToolOutputConfig>,
3046 pub tool_search: Option<ToolSearchConfig>,
3049 pub disabled_skills: Option<Vec<String>>,
3051 pub hooks: Option<bool>,
3053 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3055 pub default_agent: Option<DefaultAgentConfig>,
3057 pub agent: Option<String>,
3059 pub infinite_sessions: Option<InfiniteSessionConfig>,
3061 pub provider: Option<ProviderConfig>,
3063 pub capi: Option<CapiSessionOptions>,
3069 pub providers: Option<Vec<NamedProviderConfig>>,
3075 pub models: Option<Vec<ProviderModelConfig>>,
3081 pub enable_session_telemetry: Option<bool>,
3089 pub enable_citations: Option<bool>,
3091 pub session_limits: Option<SessionLimitsConfig>,
3093 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3095 pub memory: Option<MemoryConfiguration>,
3097 pub config_directory: Option<PathBuf>,
3099 pub working_directory: Option<PathBuf>,
3101 pub github_token: Option<String>,
3104 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3107 pub include_sub_agent_streaming_events: Option<bool>,
3109 pub commands: Option<Vec<CommandDefinition>>,
3113 #[doc(hidden)]
3118 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3119 pub enable_managed_settings: Option<bool>,
3125 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3130 pub suppress_resume_event: Option<bool>,
3133 pub continue_pending_work: Option<bool>,
3141 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3144 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3147 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3149 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3152 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3155 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3158 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3160 pub(crate) permission_policy: Option<crate::permission::Policy>,
3162 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3164 pub skip_custom_instructions: Option<bool>,
3166 pub custom_agents_local_only: Option<bool>,
3168 pub coauthor_enabled: Option<bool>,
3170 pub manage_schedule_enabled: Option<bool>,
3172}
3173
3174impl std::fmt::Debug for ResumeSessionConfig {
3175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3176 f.debug_struct("ResumeSessionConfig")
3177 .field("session_id", &self.session_id)
3178 .field("model", &self.model)
3179 .field("client_name", &self.client_name)
3180 .field("reasoning_effort", &self.reasoning_effort)
3181 .field("reasoning_summary", &self.reasoning_summary)
3182 .field("context_tier", &self.context_tier)
3183 .field("streaming", &self.streaming)
3184 .field("system_message", &self.system_message)
3185 .field("tools", &self.tools)
3186 .field("canvases", &self.canvases)
3187 .field(
3188 "canvas_handler",
3189 &self.canvas_handler.as_ref().map(|_| "<set>"),
3190 )
3191 .field("open_canvases", &self.open_canvases)
3192 .field("request_canvas_renderer", &self.request_canvas_renderer)
3193 .field("request_extensions", &self.request_extensions)
3194 .field("extension_sdk_path", &self.extension_sdk_path)
3195 .field("extension_info", &self.extension_info)
3196 .field("canvas_provider", &self.canvas_provider)
3197 .field("available_tools", &self.available_tools)
3198 .field("excluded_tools", &self.excluded_tools)
3199 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3200 .field("mcp_servers", &self.mcp_servers)
3201 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3202 .field("embedding_cache_storage", &self.embedding_cache_storage)
3203 .field("enable_config_discovery", &self.enable_config_discovery)
3204 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3205 .field(
3206 "organization_custom_instructions",
3207 &self
3208 .organization_custom_instructions
3209 .as_ref()
3210 .map(|_| "<redacted>"),
3211 )
3212 .field(
3213 "enable_on_demand_instruction_discovery",
3214 &self.enable_on_demand_instruction_discovery,
3215 )
3216 .field("enable_file_hooks", &self.enable_file_hooks)
3217 .field(
3218 "enable_host_git_operations",
3219 &self.enable_host_git_operations,
3220 )
3221 .field("enable_session_store", &self.enable_session_store)
3222 .field("enable_skills", &self.enable_skills)
3223 .field("enable_mcp_apps", &self.enable_mcp_apps)
3224 .field("skill_directories", &self.skill_directories)
3225 .field("instruction_directories", &self.instruction_directories)
3226 .field("plugin_directories", &self.plugin_directories)
3227 .field("large_output", &self.large_output)
3228 .field("tool_search", &self.tool_search)
3229 .field("disabled_skills", &self.disabled_skills)
3230 .field("hooks", &self.hooks)
3231 .field("custom_agents", &self.custom_agents)
3232 .field("default_agent", &self.default_agent)
3233 .field("agent", &self.agent)
3234 .field("infinite_sessions", &self.infinite_sessions)
3235 .field("provider", &self.provider)
3236 .field("capi", &self.capi)
3237 .field("enable_session_telemetry", &self.enable_session_telemetry)
3238 .field("enable_citations", &self.enable_citations)
3239 .field("session_limits", &self.session_limits)
3240 .field("model_capabilities", &self.model_capabilities)
3241 .field("memory", &self.memory)
3242 .field("config_directory", &self.config_directory)
3243 .field("working_directory", &self.working_directory)
3244 .field(
3245 "github_token",
3246 &self.github_token.as_ref().map(|_| "<redacted>"),
3247 )
3248 .field("remote_session", &self.remote_session)
3249 .field(
3250 "include_sub_agent_streaming_events",
3251 &self.include_sub_agent_streaming_events,
3252 )
3253 .field("commands", &self.commands)
3254 .field("exp_assignments", &self.exp_assignments)
3255 .field("enable_managed_settings", &self.enable_managed_settings)
3256 .field(
3257 "session_fs_provider",
3258 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3259 )
3260 .field(
3261 "permission_handler",
3262 &self.permission_handler.as_ref().map(|_| "<set>"),
3263 )
3264 .field(
3265 "elicitation_handler",
3266 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3267 )
3268 .field(
3269 "user_input_handler",
3270 &self.user_input_handler.as_ref().map(|_| "<set>"),
3271 )
3272 .field(
3273 "exit_plan_mode_handler",
3274 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3275 )
3276 .field(
3277 "auto_mode_switch_handler",
3278 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3279 )
3280 .field(
3281 "hooks_handler",
3282 &self.hooks_handler.as_ref().map(|_| "<set>"),
3283 )
3284 .field(
3285 "system_message_transform",
3286 &self.system_message_transform.as_ref().map(|_| "<set>"),
3287 )
3288 .field("suppress_resume_event", &self.suppress_resume_event)
3289 .field("continue_pending_work", &self.continue_pending_work)
3290 .finish()
3291 }
3292}
3293
3294impl ResumeSessionConfig {
3295 pub(crate) fn into_wire(
3303 mut self,
3304 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3305 let permission_active =
3306 self.permission_handler.is_some() || self.permission_policy.is_some();
3307 let request_user_input = self.user_input_handler.is_some();
3308 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3309 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3310 let request_elicitation = self.elicitation_handler.is_some();
3311 let hooks_flag = self.hooks_handler.is_some();
3312
3313 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3314 if let Some(tools) = self.tools.as_mut() {
3315 for tool in tools.iter_mut() {
3316 if let Some(handler) = tool.handler.take()
3317 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3318 {
3319 return Err(crate::Error::with_message(
3320 crate::ErrorKind::InvalidConfig,
3321 format!("duplicate tool handler registered for name {:?}", tool.name),
3322 ));
3323 }
3324 }
3325 }
3326
3327 let wire_commands = self.commands.as_ref().map(|cmds| {
3328 cmds.iter()
3329 .map(|c| crate::wire::CommandWireDefinition {
3330 name: c.name.clone(),
3331 description: c.description.clone(),
3332 })
3333 .collect()
3334 });
3335 let wire_canvases = self.canvases.clone();
3336 let canvas_handler = self.canvas_handler.clone();
3337 let bearer_token_providers =
3338 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3339
3340 let wire = crate::wire::SessionResumeWire {
3341 session_id: self.session_id,
3342 model: self.model,
3343 client_name: self.client_name,
3344 reasoning_effort: self.reasoning_effort,
3345 reasoning_summary: self.reasoning_summary,
3346 context_tier: self.context_tier,
3347 streaming: self.streaming,
3348 system_message: self.system_message,
3349 tools: self.tools,
3350 canvases: wire_canvases,
3351 open_canvases: self.open_canvases,
3352 request_canvas_renderer: self.request_canvas_renderer,
3353 request_extensions: self.request_extensions,
3354 extension_sdk_path: self.extension_sdk_path,
3355 extension_info: self.extension_info,
3356 canvas_provider: self.canvas_provider,
3357 available_tools: self.available_tools,
3358 excluded_tools: self.excluded_tools,
3359 excluded_builtin_agents: self.excluded_builtin_agents,
3360 tool_filter_precedence: "excluded",
3361 mcp_servers: self.mcp_servers,
3362 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3363 embedding_cache_storage: self.embedding_cache_storage,
3364 env_value_mode: "direct",
3365 enable_config_discovery: self.enable_config_discovery,
3366 skip_embedding_retrieval: self.skip_embedding_retrieval,
3367 organization_custom_instructions: self.organization_custom_instructions,
3368 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3369 enable_file_hooks: self.enable_file_hooks,
3370 enable_host_git_operations: self.enable_host_git_operations,
3371 enable_session_store: self.enable_session_store,
3372 enable_skills: self.enable_skills,
3373 request_user_input,
3374 request_permission: permission_active,
3375 request_exit_plan_mode,
3376 request_auto_mode_switch,
3377 request_elicitation,
3378 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3379 hooks: hooks_flag,
3380 skill_directories: self.skill_directories,
3381 instruction_directories: self.instruction_directories,
3382 plugin_directories: self.plugin_directories,
3383 large_output: self.large_output,
3384 tool_search: self.tool_search,
3385 disabled_skills: self.disabled_skills,
3386 custom_agents: self.custom_agents,
3387 default_agent: self.default_agent,
3388 agent: self.agent,
3389 infinite_sessions: self.infinite_sessions,
3390 provider: self.provider,
3391 capi: self.capi,
3392 providers: self.providers,
3393 models: self.models,
3394 enable_session_telemetry: self.enable_session_telemetry,
3395 enable_citations: self.enable_citations,
3396 session_limits: self.session_limits,
3397 model_capabilities: self.model_capabilities,
3398 memory: self.memory,
3399 config_dir: self.config_directory,
3400 working_directory: self.working_directory,
3401 github_token: self.github_token,
3402 remote_session: self.remote_session,
3403 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3404 enable_github_telemetry_forwarding: None,
3405 commands: wire_commands,
3406 exp_assignments: self.exp_assignments,
3407 enable_managed_settings: self.enable_managed_settings,
3408 suppress_resume_event: self.suppress_resume_event,
3409 continue_pending_work: self.continue_pending_work,
3410 };
3411
3412 let runtime = SessionConfigRuntime {
3413 permission_handler: self.permission_handler,
3414 permission_policy: self.permission_policy,
3415 elicitation_handler: self.elicitation_handler,
3416 mcp_auth_handler: self.mcp_auth_handler,
3417 user_input_handler: self.user_input_handler,
3418 exit_plan_mode_handler: self.exit_plan_mode_handler,
3419 auto_mode_switch_handler: self.auto_mode_switch_handler,
3420 hooks_handler: self.hooks_handler,
3421 system_message_transform: self.system_message_transform,
3422 tool_handlers,
3423 canvas_handler,
3424 session_fs_provider: self.session_fs_provider,
3425 bearer_token_providers,
3426 commands: self.commands,
3427 };
3428
3429 Ok((wire, runtime))
3430 }
3431
3432 pub fn new(session_id: SessionId) -> Self {
3437 Self {
3438 session_id,
3439 model: None,
3440 client_name: None,
3441 reasoning_effort: None,
3442 reasoning_summary: None,
3443 context_tier: None,
3444 streaming: None,
3445 system_message: None,
3446 tools: None,
3447 canvases: None,
3448 canvas_handler: None,
3449 open_canvases: None,
3450 request_canvas_renderer: None,
3451 request_extensions: None,
3452 extension_sdk_path: None,
3453 extension_info: None,
3454 canvas_provider: None,
3455 available_tools: None,
3456 excluded_tools: None,
3457 excluded_builtin_agents: None,
3458 mcp_servers: None,
3459 mcp_oauth_token_storage: None,
3460 enable_config_discovery: None,
3461 skip_embedding_retrieval: None,
3462 organization_custom_instructions: None,
3463 enable_on_demand_instruction_discovery: None,
3464 enable_file_hooks: None,
3465 enable_host_git_operations: None,
3466 enable_session_store: None,
3467 enable_skills: None,
3468 embedding_cache_storage: None,
3469 enable_mcp_apps: None,
3470 skill_directories: None,
3471 instruction_directories: None,
3472 plugin_directories: None,
3473 large_output: None,
3474 tool_search: None,
3475 disabled_skills: None,
3476 hooks: None,
3477 custom_agents: None,
3478 default_agent: None,
3479 agent: None,
3480 infinite_sessions: None,
3481 provider: None,
3482 capi: None,
3483 providers: None,
3484 models: None,
3485 enable_session_telemetry: None,
3486 enable_citations: None,
3487 session_limits: None,
3488 model_capabilities: None,
3489 memory: None,
3490 config_directory: None,
3491 working_directory: None,
3492 github_token: None,
3493 remote_session: None,
3494 include_sub_agent_streaming_events: None,
3495 commands: None,
3496 exp_assignments: None,
3497 enable_managed_settings: None,
3498 session_fs_provider: None,
3499 suppress_resume_event: None,
3500 continue_pending_work: None,
3501 permission_handler: None,
3502 elicitation_handler: None,
3503 mcp_auth_handler: None,
3504 user_input_handler: None,
3505 exit_plan_mode_handler: None,
3506 auto_mode_switch_handler: None,
3507 hooks_handler: None,
3508 permission_policy: None,
3509 system_message_transform: None,
3510 skip_custom_instructions: None,
3511 custom_agents_local_only: None,
3512 coauthor_enabled: None,
3513 manage_schedule_enabled: None,
3514 }
3515 }
3516
3517 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3519 self.permission_handler = Some(handler);
3520 self
3521 }
3522
3523 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3525 self.elicitation_handler = Some(handler);
3526 self
3527 }
3528
3529 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3531 self.mcp_auth_handler = Some(handler);
3532 self
3533 }
3534
3535 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3537 self.user_input_handler = Some(handler);
3538 self
3539 }
3540
3541 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3543 self.exit_plan_mode_handler = Some(handler);
3544 self
3545 }
3546
3547 pub fn with_auto_mode_switch_handler(
3549 mut self,
3550 handler: Arc<dyn AutoModeSwitchHandler>,
3551 ) -> Self {
3552 self.auto_mode_switch_handler = Some(handler);
3553 self
3554 }
3555
3556 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3559 self.hooks_handler = Some(hooks);
3560 self
3561 }
3562
3563 pub fn with_system_message_transform(
3565 mut self,
3566 transform: Arc<dyn SystemMessageTransform>,
3567 ) -> Self {
3568 self.system_message_transform = Some(transform);
3569 self
3570 }
3571
3572 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3576 self.commands = Some(commands);
3577 self
3578 }
3579
3580 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3583 self.session_fs_provider = Some(provider);
3584 self
3585 }
3586
3587 pub fn approve_all_permissions(mut self) -> Self {
3590 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3591 self
3592 }
3593
3594 pub fn deny_all_permissions(mut self) -> Self {
3597 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3598 self
3599 }
3600
3601 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3604 where
3605 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3606 {
3607 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3608 self
3609 }
3610
3611 pub fn with_model(mut self, model: impl Into<String>) -> Self {
3613 self.model = Some(model.into());
3614 self
3615 }
3616
3617 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3619 self.client_name = Some(name.into());
3620 self
3621 }
3622
3623 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3625 self.reasoning_effort = Some(effort.into());
3626 self
3627 }
3628
3629 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3631 self.reasoning_summary = Some(summary);
3632 self
3633 }
3634
3635 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3638 self.context_tier = Some(tier.into());
3639 self
3640 }
3641
3642 pub fn with_streaming(mut self, streaming: bool) -> Self {
3644 self.streaming = Some(streaming);
3645 self
3646 }
3647
3648 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3651 self.system_message = Some(system_message);
3652 self
3653 }
3654
3655 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3657 self.tools = Some(tools.into_iter().collect());
3658 self
3659 }
3660
3661 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3663 self.canvases = Some(canvases.into_iter().collect());
3664 self
3665 }
3666
3667 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
3669 self.canvas_handler = Some(handler);
3670 self
3671 }
3672
3673 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
3675 mut self,
3676 open_canvases: I,
3677 ) -> Self {
3678 self.open_canvases = Some(open_canvases.into_iter().collect());
3679 self
3680 }
3681
3682 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
3684 self.request_canvas_renderer = Some(request);
3685 self
3686 }
3687
3688 pub fn with_request_extensions(mut self, request: bool) -> Self {
3690 self.request_extensions = Some(request);
3691 self
3692 }
3693
3694 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
3698 self.extension_sdk_path = Some(path.into());
3699 self
3700 }
3701
3702 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
3704 self.extension_info = Some(extension_info);
3705 self
3706 }
3707
3708 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
3711 self.canvas_provider = Some(canvas_provider);
3712 self
3713 }
3714
3715 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
3717 where
3718 I: IntoIterator<Item = S>,
3719 S: Into<String>,
3720 {
3721 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
3722 self
3723 }
3724
3725 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
3727 where
3728 I: IntoIterator<Item = S>,
3729 S: Into<String>,
3730 {
3731 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
3732 self
3733 }
3734
3735 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
3737 where
3738 I: IntoIterator<Item = S>,
3739 S: Into<String>,
3740 {
3741 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
3742 self
3743 }
3744
3745 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
3747 self.mcp_servers = Some(servers);
3748 self
3749 }
3750
3751 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3754 self.mcp_oauth_token_storage = Some(mode.into());
3755 self
3756 }
3757
3758 pub fn with_embedding_cache_storage(
3760 mut self,
3761 embedding_cache_storage: impl Into<String>,
3762 ) -> Self {
3763 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3764 self
3765 }
3766
3767 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3769 self.enable_config_discovery = Some(enable);
3770 self
3771 }
3772
3773 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3775 self.skip_embedding_retrieval = Some(value);
3776 self
3777 }
3778
3779 pub fn with_organization_custom_instructions(
3781 mut self,
3782 instructions: impl Into<String>,
3783 ) -> Self {
3784 self.organization_custom_instructions = Some(instructions.into());
3785 self
3786 }
3787
3788 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3790 self.enable_on_demand_instruction_discovery = Some(value);
3791 self
3792 }
3793
3794 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3796 self.enable_file_hooks = Some(value);
3797 self
3798 }
3799
3800 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3802 self.enable_host_git_operations = Some(value);
3803 self
3804 }
3805
3806 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3808 self.enable_session_store = Some(value);
3809 self
3810 }
3811
3812 pub fn with_enable_skills(mut self, value: bool) -> Self {
3814 self.enable_skills = Some(value);
3815 self
3816 }
3817
3818 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3824 self.enable_mcp_apps = Some(enable);
3825 self
3826 }
3827
3828 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3830 where
3831 I: IntoIterator<Item = P>,
3832 P: Into<PathBuf>,
3833 {
3834 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3835 self
3836 }
3837
3838 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3842 where
3843 I: IntoIterator<Item = P>,
3844 P: Into<PathBuf>,
3845 {
3846 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3847 self
3848 }
3849
3850 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3852 where
3853 I: IntoIterator<Item = P>,
3854 P: Into<PathBuf>,
3855 {
3856 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3857 self
3858 }
3859
3860 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3862 self.large_output = Some(config);
3863 self
3864 }
3865
3866 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
3869 self.tool_search = Some(config);
3870 self
3871 }
3872
3873 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3875 where
3876 I: IntoIterator<Item = S>,
3877 S: Into<String>,
3878 {
3879 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3880 self
3881 }
3882
3883 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3885 mut self,
3886 agents: I,
3887 ) -> Self {
3888 self.custom_agents = Some(agents.into_iter().collect());
3889 self
3890 }
3891
3892 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3894 self.default_agent = Some(agent);
3895 self
3896 }
3897
3898 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3900 self.agent = Some(name.into());
3901 self
3902 }
3903
3904 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3906 self.infinite_sessions = Some(config);
3907 self
3908 }
3909
3910 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3912 self.provider = Some(provider);
3913 self
3914 }
3915
3916 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3918 self.capi = Some(capi);
3919 self
3920 }
3921
3922 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3928 self.providers = Some(providers);
3929 self
3930 }
3931
3932 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3938 self.models = Some(models);
3939 self
3940 }
3941
3942 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3946 self.enable_session_telemetry = Some(enable);
3947 self
3948 }
3949
3950 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3952 self.enable_citations = Some(enable);
3953 self
3954 }
3955
3956 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3958 self.session_limits = Some(limits);
3959 self
3960 }
3961
3962 pub fn with_model_capabilities(
3964 mut self,
3965 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3966 ) -> Self {
3967 self.model_capabilities = Some(capabilities);
3968 self
3969 }
3970
3971 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3973 self.memory = Some(memory);
3974 self
3975 }
3976
3977 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3979 self.config_directory = Some(dir.into());
3980 self
3981 }
3982
3983 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3985 self.working_directory = Some(dir.into());
3986 self
3987 }
3988
3989 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3993 self.github_token = Some(token.into());
3994 self
3995 }
3996
3997 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3999 self.include_sub_agent_streaming_events = Some(include);
4000 self
4001 }
4002
4003 pub fn with_remote_session(
4005 mut self,
4006 mode: crate::generated::api_types::RemoteSessionMode,
4007 ) -> Self {
4008 self.remote_session = Some(mode);
4009 self
4010 }
4011
4012 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4015 self.suppress_resume_event = Some(suppress);
4016 self
4017 }
4018
4019 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4025 self.continue_pending_work = Some(continue_pending);
4026 self
4027 }
4028
4029 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4031 self.skip_custom_instructions = Some(value);
4032 self
4033 }
4034
4035 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4037 self.custom_agents_local_only = Some(value);
4038 self
4039 }
4040
4041 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4043 self.coauthor_enabled = Some(value);
4044 self
4045 }
4046
4047 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4049 self.manage_schedule_enabled = Some(value);
4050 self
4051 }
4052
4053 #[doc(hidden)]
4057 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4058 self.exp_assignments = Some(assignments);
4059 self
4060 }
4061
4062 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4065 self.enable_managed_settings = Some(enabled);
4066 self
4067 }
4068}
4069
4070#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4076#[serde(rename_all = "camelCase")]
4077#[non_exhaustive]
4078pub struct SystemMessageConfig {
4079 #[serde(skip_serializing_if = "Option::is_none")]
4081 pub mode: Option<String>,
4082 #[serde(skip_serializing_if = "Option::is_none")]
4084 pub content: Option<String>,
4085 #[serde(skip_serializing_if = "Option::is_none")]
4087 pub sections: Option<HashMap<String, SectionOverride>>,
4088}
4089
4090impl SystemMessageConfig {
4091 pub fn new() -> Self {
4094 Self::default()
4095 }
4096
4097 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4100 self.mode = Some(mode.into());
4101 self
4102 }
4103
4104 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4107 self.content = Some(content.into());
4108 self
4109 }
4110
4111 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4113 self.sections = Some(sections);
4114 self
4115 }
4116}
4117
4118#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4124#[serde(rename_all = "camelCase")]
4125pub struct SectionOverride {
4126 #[serde(skip_serializing_if = "Option::is_none")]
4129 pub action: Option<String>,
4130 #[serde(skip_serializing_if = "Option::is_none")]
4132 pub content: Option<String>,
4133}
4134
4135#[derive(Debug, Clone, Serialize, Deserialize)]
4137#[serde(rename_all = "camelCase")]
4138pub struct CreateSessionResult {
4139 pub session_id: SessionId,
4141 #[serde(skip_serializing_if = "Option::is_none")]
4143 pub workspace_path: Option<PathBuf>,
4144 #[serde(default, alias = "remote_url")]
4146 pub remote_url: Option<String>,
4147 #[serde(skip_serializing_if = "Option::is_none")]
4149 pub capabilities: Option<SessionCapabilities>,
4150}
4151
4152#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4154#[serde(rename_all = "camelCase")]
4155pub(crate) struct ResumeSessionResult {
4156 #[serde(default)]
4158 pub session_id: Option<SessionId>,
4159 #[serde(default, skip_serializing_if = "Option::is_none")]
4161 pub workspace_path: Option<PathBuf>,
4162 #[serde(default, alias = "remote_url")]
4164 pub remote_url: Option<String>,
4165 #[serde(default, skip_serializing_if = "Option::is_none")]
4167 pub capabilities: Option<SessionCapabilities>,
4168 #[serde(
4170 default,
4171 alias = "openCanvasInstances",
4172 skip_serializing_if = "Option::is_none"
4173 )]
4174 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4175}
4176
4177#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4179#[serde(rename_all = "lowercase")]
4180pub enum LogLevel {
4181 #[default]
4183 Info,
4184 Warning,
4186 Error,
4188}
4189
4190#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4195#[serde(rename_all = "camelCase")]
4196pub struct LogOptions {
4197 #[serde(skip_serializing_if = "Option::is_none")]
4199 pub level: Option<LogLevel>,
4200 #[serde(skip_serializing_if = "Option::is_none")]
4203 pub ephemeral: Option<bool>,
4204}
4205
4206impl LogOptions {
4207 pub fn with_level(mut self, level: LogLevel) -> Self {
4209 self.level = Some(level);
4210 self
4211 }
4212
4213 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4215 self.ephemeral = Some(ephemeral);
4216 self
4217 }
4218}
4219
4220#[derive(Debug, Clone, Default)]
4224pub struct SetModelOptions {
4225 pub reasoning_effort: Option<String>,
4228 pub reasoning_summary: Option<ReasoningSummary>,
4232 pub context_tier: Option<ContextTier>,
4235 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4239}
4240
4241impl SetModelOptions {
4242 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4244 self.reasoning_effort = Some(effort.into());
4245 self
4246 }
4247
4248 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4250 self.reasoning_summary = Some(summary);
4251 self
4252 }
4253
4254 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4256 self.context_tier = Some(tier);
4257 self
4258 }
4259
4260 pub fn with_model_capabilities(
4262 mut self,
4263 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4264 ) -> Self {
4265 self.model_capabilities = Some(caps);
4266 self
4267 }
4268}
4269
4270#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4277#[serde(rename_all = "camelCase")]
4278pub struct PingResponse {
4279 #[serde(default)]
4281 pub message: String,
4282 #[serde(default)]
4284 pub timestamp: String,
4285 #[serde(skip_serializing_if = "Option::is_none")]
4287 pub protocol_version: Option<u32>,
4288}
4289
4290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4292#[serde(rename_all = "camelCase")]
4293pub struct AttachmentLineRange {
4294 pub start: u32,
4296 pub end: u32,
4298}
4299
4300#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4302#[serde(rename_all = "camelCase")]
4303pub struct AttachmentSelectionPosition {
4304 pub line: u32,
4306 pub character: u32,
4308}
4309
4310#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4312#[serde(rename_all = "camelCase")]
4313pub struct AttachmentSelectionRange {
4314 pub start: AttachmentSelectionPosition,
4316 pub end: AttachmentSelectionPosition,
4318}
4319
4320#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4322#[serde(rename_all = "snake_case")]
4323#[non_exhaustive]
4324pub enum GitHubReferenceType {
4325 Issue,
4327 Pr,
4329 Discussion,
4331}
4332
4333#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4339#[serde(rename_all = "camelCase")]
4340pub struct GitHubRepoPointer {
4341 #[serde(skip_serializing_if = "Option::is_none")]
4343 pub id: Option<i64>,
4344 pub name: String,
4346 pub owner: String,
4348}
4349
4350#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4352#[serde(rename_all = "camelCase")]
4353pub struct GitHubFileDiffSide {
4354 pub path: String,
4356 pub r#ref: String,
4358 pub repo: GitHubRepoPointer,
4360}
4361
4362#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4364#[serde(rename_all = "camelCase")]
4365pub struct GitHubTreeComparisonSide {
4366 pub repo: GitHubRepoPointer,
4368 pub revision: String,
4370}
4371
4372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4374#[serde(rename_all = "camelCase")]
4375pub struct GitHubSnippetLineRange {
4376 pub start: i64,
4378 pub end: i64,
4380}
4381
4382#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4384#[serde(
4385 tag = "type",
4386 rename_all = "camelCase",
4387 rename_all_fields = "camelCase"
4388)]
4389#[non_exhaustive]
4390pub enum Attachment {
4391 File {
4393 path: PathBuf,
4395 #[serde(skip_serializing_if = "Option::is_none")]
4397 display_name: Option<String>,
4398 #[serde(skip_serializing_if = "Option::is_none")]
4400 line_range: Option<AttachmentLineRange>,
4401 },
4402 Directory {
4404 path: PathBuf,
4406 #[serde(skip_serializing_if = "Option::is_none")]
4408 display_name: Option<String>,
4409 },
4410 Selection {
4412 file_path: PathBuf,
4414 text: String,
4416 #[serde(skip_serializing_if = "Option::is_none")]
4418 display_name: Option<String>,
4419 selection: AttachmentSelectionRange,
4421 },
4422 Blob {
4424 data: String,
4426 mime_type: String,
4428 #[serde(skip_serializing_if = "Option::is_none")]
4430 display_name: Option<String>,
4431 },
4432 #[serde(rename = "github_reference")]
4434 GitHubReference {
4435 number: u64,
4437 title: String,
4439 reference_type: GitHubReferenceType,
4441 state: String,
4443 url: String,
4445 },
4446 #[serde(rename = "github_commit")]
4448 GitHubCommit {
4449 message: String,
4451 oid: String,
4453 repo: GitHubRepoPointer,
4455 url: String,
4457 },
4458 #[serde(rename = "github_release")]
4460 GitHubRelease {
4461 name: String,
4463 repo: GitHubRepoPointer,
4465 tag_name: String,
4467 url: String,
4469 },
4470 #[serde(rename = "github_actions_job")]
4472 GitHubActionsJob {
4473 #[serde(skip_serializing_if = "Option::is_none")]
4476 conclusion: Option<String>,
4477 job_id: i64,
4479 job_name: String,
4481 repo: GitHubRepoPointer,
4483 url: String,
4485 workflow_name: String,
4487 },
4488 #[serde(rename = "github_repository")]
4490 GitHubRepository {
4491 #[serde(skip_serializing_if = "Option::is_none")]
4493 description: Option<String>,
4494 #[serde(skip_serializing_if = "Option::is_none")]
4497 r#ref: Option<String>,
4498 repo: GitHubRepoPointer,
4500 url: String,
4502 },
4503 #[serde(rename = "github_file_diff")]
4505 GitHubFileDiff {
4506 #[serde(skip_serializing_if = "Option::is_none")]
4508 base: Option<GitHubFileDiffSide>,
4509 #[serde(skip_serializing_if = "Option::is_none")]
4511 head: Option<GitHubFileDiffSide>,
4512 url: String,
4514 },
4515 #[serde(rename = "github_tree_comparison")]
4517 GitHubTreeComparison {
4518 base: GitHubTreeComparisonSide,
4520 head: GitHubTreeComparisonSide,
4522 url: String,
4524 },
4525 #[serde(rename = "github_url")]
4527 GitHubUrl {
4528 url: String,
4530 },
4531 #[serde(rename = "github_file")]
4533 GitHubFile {
4534 path: String,
4536 r#ref: String,
4538 repo: GitHubRepoPointer,
4540 url: String,
4542 },
4543 #[serde(rename = "github_snippet")]
4545 GitHubSnippet {
4546 line_range: GitHubSnippetLineRange,
4548 path: String,
4550 r#ref: String,
4552 repo: GitHubRepoPointer,
4554 url: String,
4556 },
4557}
4558
4559impl Attachment {
4560 pub fn display_name(&self) -> Option<&str> {
4562 match self {
4563 Self::File { display_name, .. }
4564 | Self::Directory { display_name, .. }
4565 | Self::Selection { display_name, .. }
4566 | Self::Blob { display_name, .. } => display_name.as_deref(),
4567 Self::GitHubReference { .. }
4568 | Self::GitHubCommit { .. }
4569 | Self::GitHubRelease { .. }
4570 | Self::GitHubActionsJob { .. }
4571 | Self::GitHubRepository { .. }
4572 | Self::GitHubFileDiff { .. }
4573 | Self::GitHubTreeComparison { .. }
4574 | Self::GitHubUrl { .. }
4575 | Self::GitHubFile { .. }
4576 | Self::GitHubSnippet { .. } => None,
4577 }
4578 }
4579
4580 pub fn label(&self) -> Option<String> {
4582 if let Some(display_name) = self
4583 .display_name()
4584 .map(str::trim)
4585 .filter(|name| !name.is_empty())
4586 {
4587 return Some(display_name.to_string());
4588 }
4589
4590 match self {
4591 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4592 format!("#{}", number)
4593 } else {
4594 title.trim().to_string()
4595 }),
4596 _ => self.derived_display_name(),
4597 }
4598 }
4599
4600 pub fn ensure_display_name(&mut self) {
4602 if self
4603 .display_name()
4604 .map(str::trim)
4605 .is_some_and(|name| !name.is_empty())
4606 {
4607 return;
4608 }
4609
4610 let Some(derived_display_name) = self.derived_display_name() else {
4611 return;
4612 };
4613
4614 match self {
4615 Self::File { display_name, .. }
4616 | Self::Directory { display_name, .. }
4617 | Self::Selection { display_name, .. }
4618 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4619 Self::GitHubReference { .. }
4620 | Self::GitHubCommit { .. }
4621 | Self::GitHubRelease { .. }
4622 | Self::GitHubActionsJob { .. }
4623 | Self::GitHubRepository { .. }
4624 | Self::GitHubFileDiff { .. }
4625 | Self::GitHubTreeComparison { .. }
4626 | Self::GitHubUrl { .. }
4627 | Self::GitHubFile { .. }
4628 | Self::GitHubSnippet { .. } => {}
4629 }
4630 }
4631
4632 fn derived_display_name(&self) -> Option<String> {
4633 match self {
4634 Self::File { path, .. } | Self::Directory { path, .. } => {
4635 Some(attachment_name_from_path(path))
4636 }
4637 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4638 Self::Blob { .. } => Some("attachment".to_string()),
4639 Self::GitHubReference { .. }
4640 | Self::GitHubCommit { .. }
4641 | Self::GitHubRelease { .. }
4642 | Self::GitHubActionsJob { .. }
4643 | Self::GitHubRepository { .. }
4644 | Self::GitHubFileDiff { .. }
4645 | Self::GitHubTreeComparison { .. }
4646 | Self::GitHubUrl { .. }
4647 | Self::GitHubFile { .. }
4648 | Self::GitHubSnippet { .. } => None,
4649 }
4650 }
4651}
4652
4653fn attachment_name_from_path(path: &Path) -> String {
4654 path.file_name()
4655 .map(|name| name.to_string_lossy().into_owned())
4656 .filter(|name| !name.is_empty())
4657 .unwrap_or_else(|| {
4658 let full = path.to_string_lossy();
4659 if full.is_empty() {
4660 "attachment".to_string()
4661 } else {
4662 full.into_owned()
4663 }
4664 })
4665}
4666
4667pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
4669 for attachment in attachments {
4670 attachment.ensure_display_name();
4671 }
4672}
4673
4674#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4679#[serde(rename_all = "lowercase")]
4680#[non_exhaustive]
4681pub enum DeliveryMode {
4682 Enqueue,
4684 Immediate,
4686}
4687
4688#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4693#[serde(rename_all = "lowercase")]
4694#[non_exhaustive]
4695pub enum AgentMode {
4696 Interactive,
4698 Plan,
4700 Autopilot,
4702 Shell,
4704}
4705
4706#[derive(Debug, Clone)]
4735#[non_exhaustive]
4736pub struct MessageOptions {
4737 pub prompt: String,
4739 pub mode: Option<DeliveryMode>,
4745 pub agent_mode: Option<AgentMode>,
4749 pub attachments: Option<Vec<Attachment>>,
4751 pub wait_timeout: Option<Duration>,
4754 pub request_headers: Option<HashMap<String, String>>,
4758 pub traceparent: Option<String>,
4765 pub tracestate: Option<String>,
4769 pub display_prompt: Option<String>,
4771}
4772
4773impl MessageOptions {
4774 pub fn new(prompt: impl Into<String>) -> Self {
4776 Self {
4777 prompt: prompt.into(),
4778 mode: None,
4779 agent_mode: None,
4780 attachments: None,
4781 wait_timeout: None,
4782 request_headers: None,
4783 traceparent: None,
4784 tracestate: None,
4785 display_prompt: None,
4786 }
4787 }
4788
4789 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
4795 self.mode = Some(mode);
4796 self
4797 }
4798
4799 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
4803 self.agent_mode = Some(agent_mode);
4804 self
4805 }
4806
4807 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
4809 self.attachments = Some(attachments);
4810 self
4811 }
4812
4813 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
4815 self.wait_timeout = Some(timeout);
4816 self
4817 }
4818
4819 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
4821 self.request_headers = Some(headers);
4822 self
4823 }
4824
4825 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
4830 self.traceparent = ctx.traceparent;
4831 self.tracestate = ctx.tracestate;
4832 self
4833 }
4834
4835 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
4837 self.traceparent = Some(traceparent.into());
4838 self
4839 }
4840
4841 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
4843 self.tracestate = Some(tracestate.into());
4844 self
4845 }
4846
4847 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
4849 self.display_prompt = Some(display_prompt.into());
4850 self
4851 }
4852}
4853
4854impl From<&str> for MessageOptions {
4855 fn from(prompt: &str) -> Self {
4856 Self::new(prompt)
4857 }
4858}
4859
4860impl From<String> for MessageOptions {
4861 fn from(prompt: String) -> Self {
4862 Self::new(prompt)
4863 }
4864}
4865
4866impl From<&String> for MessageOptions {
4867 fn from(prompt: &String) -> Self {
4868 Self::new(prompt.clone())
4869 }
4870}
4871
4872#[derive(Debug, Clone, Serialize, Deserialize)]
4874#[serde(rename_all = "camelCase")]
4875#[non_exhaustive]
4876pub struct GetStatusResponse {
4877 pub version: String,
4879 pub protocol_version: u32,
4881}
4882
4883#[derive(Debug, Clone, Serialize, Deserialize)]
4885#[serde(rename_all = "camelCase")]
4886#[non_exhaustive]
4887pub struct GetAuthStatusResponse {
4888 pub is_authenticated: bool,
4890 #[serde(skip_serializing_if = "Option::is_none")]
4893 pub auth_type: Option<String>,
4894 #[serde(skip_serializing_if = "Option::is_none")]
4896 pub host: Option<String>,
4897 #[serde(skip_serializing_if = "Option::is_none")]
4899 pub login: Option<String>,
4900 #[serde(skip_serializing_if = "Option::is_none")]
4902 pub status_message: Option<String>,
4903}
4904
4905#[derive(Debug, Clone, Serialize, Deserialize)]
4909#[serde(rename_all = "camelCase")]
4910pub struct SessionEventNotification {
4911 pub session_id: SessionId,
4913 pub event: SessionEvent,
4915}
4916
4917#[derive(Debug, Clone, Serialize, Deserialize)]
4924#[serde(rename_all = "camelCase")]
4925pub struct SessionEvent {
4926 pub id: String,
4928 pub timestamp: String,
4930 pub parent_id: Option<String>,
4932 #[serde(skip_serializing_if = "Option::is_none")]
4934 pub ephemeral: Option<bool>,
4935 #[serde(skip_serializing_if = "Option::is_none")]
4938 pub agent_id: Option<String>,
4939 #[serde(skip_serializing_if = "Option::is_none")]
4941 pub debug_cli_received_at_ms: Option<i64>,
4942 #[serde(skip_serializing_if = "Option::is_none")]
4944 pub debug_ws_forwarded_at_ms: Option<i64>,
4945 #[serde(rename = "type")]
4947 pub event_type: String,
4948 pub data: Value,
4950}
4951
4952impl SessionEvent {
4953 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
4958 use serde::de::IntoDeserializer;
4959 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
4960 self.event_type.as_str().into_deserializer();
4961 crate::generated::SessionEventType::deserialize(deserializer)
4962 .unwrap_or(crate::generated::SessionEventType::Unknown)
4963 }
4964
4965 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
4971 serde_json::from_value(self.data.clone()).ok()
4972 }
4973
4974 pub fn is_transient_error(&self) -> bool {
4978 self.event_type == "session.error"
4979 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
4980 }
4981}
4982
4983#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4988#[serde(rename_all = "camelCase")]
4989#[non_exhaustive]
4990pub struct ToolInvocation {
4991 pub session_id: SessionId,
4993 pub tool_call_id: String,
4995 pub tool_name: String,
4997 pub arguments: Value,
4999 #[serde(skip)]
5007 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5008 #[serde(default, skip_serializing_if = "Option::is_none")]
5013 pub traceparent: Option<String>,
5014 #[serde(default, skip_serializing_if = "Option::is_none")]
5017 pub tracestate: Option<String>,
5018}
5019
5020impl ToolInvocation {
5021 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5042 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5043 }
5044
5045 pub fn trace_context(&self) -> TraceContext {
5048 TraceContext {
5049 traceparent: self.traceparent.clone(),
5050 tracestate: self.tracestate.clone(),
5051 }
5052 }
5053}
5054
5055#[derive(Debug, Clone, Serialize, Deserialize)]
5057#[serde(rename_all = "camelCase")]
5058pub struct ToolBinaryResult {
5059 pub data: String,
5061 pub mime_type: String,
5063 pub r#type: String,
5065 #[serde(default, skip_serializing_if = "Option::is_none")]
5067 pub description: Option<String>,
5068}
5069
5070#[derive(Debug, Clone, Serialize, Deserialize)]
5077#[serde(rename_all = "camelCase")]
5078#[non_exhaustive]
5079pub struct ToolResultExpanded {
5080 pub text_result_for_llm: String,
5082 pub result_type: String,
5084 #[serde(default, skip_serializing_if = "Option::is_none")]
5086 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5087 #[serde(skip_serializing_if = "Option::is_none")]
5089 pub session_log: Option<String>,
5090 #[serde(skip_serializing_if = "Option::is_none")]
5092 pub error: Option<String>,
5093 #[serde(default, skip_serializing_if = "Option::is_none")]
5095 pub tool_telemetry: Option<HashMap<String, Value>>,
5096 #[serde(default, skip_serializing_if = "Option::is_none")]
5098 pub tool_references: Option<Vec<String>>,
5099}
5100
5101impl ToolResultExpanded {
5102 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5106 Self {
5107 text_result_for_llm: text_result_for_llm.into(),
5108 result_type: result_type.into(),
5109 binary_results_for_llm: None,
5110 session_log: None,
5111 error: None,
5112 tool_telemetry: None,
5113 tool_references: None,
5114 }
5115 }
5116
5117 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5119 self.binary_results_for_llm = Some(results);
5120 self
5121 }
5122
5123 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5125 self.session_log = Some(session_log.into());
5126 self
5127 }
5128
5129 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5131 self.error = Some(error.into());
5132 self
5133 }
5134
5135 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5137 self.tool_telemetry = Some(telemetry);
5138 self
5139 }
5140
5141 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5143 where
5144 I: IntoIterator<Item = S>,
5145 S: Into<String>,
5146 {
5147 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5148 self
5149 }
5150}
5151
5152#[derive(Debug, Clone, Serialize, Deserialize)]
5154#[serde(untagged)]
5155#[non_exhaustive]
5156pub enum ToolResult {
5157 Text(String),
5159 Expanded(ToolResultExpanded),
5161}
5162
5163#[derive(Debug, Clone, Serialize, Deserialize)]
5165#[serde(rename_all = "camelCase")]
5166pub struct ToolResultResponse {
5167 pub result: ToolResult,
5169}
5170
5171#[derive(Debug, Clone, Serialize, Deserialize)]
5173#[serde(rename_all = "camelCase")]
5174pub struct SessionMetadata {
5175 pub session_id: SessionId,
5177 pub start_time: String,
5179 pub modified_time: String,
5181 #[serde(skip_serializing_if = "Option::is_none")]
5183 pub summary: Option<String>,
5184 pub is_remote: bool,
5186}
5187
5188#[derive(Debug, Clone, Serialize, Deserialize)]
5190#[serde(rename_all = "camelCase")]
5191pub struct ListSessionsResponse {
5192 pub sessions: Vec<SessionMetadata>,
5194}
5195
5196#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5200#[serde(rename_all = "camelCase")]
5201pub struct SessionListFilter {
5202 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5204 pub working_directory: Option<String>,
5205 #[serde(default, skip_serializing_if = "Option::is_none")]
5207 pub git_root: Option<String>,
5208 #[serde(default, skip_serializing_if = "Option::is_none")]
5210 pub repository: Option<String>,
5211 #[serde(default, skip_serializing_if = "Option::is_none")]
5213 pub branch: Option<String>,
5214}
5215
5216#[derive(Debug, Clone, Serialize, Deserialize)]
5218#[serde(rename_all = "camelCase")]
5219pub struct GetSessionMetadataResponse {
5220 #[serde(skip_serializing_if = "Option::is_none")]
5222 pub session: Option<SessionMetadata>,
5223}
5224
5225#[derive(Debug, Clone, Serialize, Deserialize)]
5227#[serde(rename_all = "camelCase")]
5228pub struct GetLastSessionIdResponse {
5229 #[serde(skip_serializing_if = "Option::is_none")]
5231 pub session_id: Option<SessionId>,
5232}
5233
5234#[derive(Debug, Clone, Serialize, Deserialize)]
5236#[serde(rename_all = "camelCase")]
5237pub struct GetForegroundSessionResponse {
5238 #[serde(skip_serializing_if = "Option::is_none")]
5240 pub session_id: Option<SessionId>,
5241}
5242
5243#[derive(Debug, Clone, Serialize, Deserialize)]
5245#[serde(rename_all = "camelCase")]
5246pub struct GetMessagesResponse {
5247 pub events: Vec<SessionEvent>,
5249}
5250
5251#[derive(Debug, Clone, Serialize, Deserialize)]
5253#[serde(rename_all = "camelCase")]
5254pub struct ElicitationResult {
5255 pub action: String,
5257 #[serde(skip_serializing_if = "Option::is_none")]
5259 pub content: Option<Value>,
5260}
5261
5262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5268#[serde(rename_all = "camelCase")]
5269#[non_exhaustive]
5270pub enum ElicitationMode {
5271 Form,
5273 Url,
5275 #[serde(other)]
5277 Unknown,
5278}
5279
5280#[derive(Debug, Clone, Serialize, Deserialize)]
5287#[serde(rename_all = "camelCase")]
5288pub struct ElicitationRequest {
5289 pub message: String,
5291 #[serde(skip_serializing_if = "Option::is_none")]
5293 pub requested_schema: Option<Value>,
5294 #[serde(skip_serializing_if = "Option::is_none")]
5296 pub mode: Option<ElicitationMode>,
5297 #[serde(skip_serializing_if = "Option::is_none")]
5299 pub elicitation_source: Option<String>,
5300 #[serde(skip_serializing_if = "Option::is_none")]
5302 pub url: Option<String>,
5303}
5304
5305#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5310#[serde(rename_all = "camelCase")]
5311pub struct SessionCapabilities {
5312 #[serde(skip_serializing_if = "Option::is_none")]
5314 pub ui: Option<UiCapabilities>,
5315}
5316
5317#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5319#[serde(rename_all = "camelCase")]
5320pub struct UiCapabilities {
5321 #[serde(skip_serializing_if = "Option::is_none")]
5323 pub elicitation: Option<bool>,
5324 #[serde(skip_serializing_if = "Option::is_none")]
5335 pub mcp_apps: Option<bool>,
5336 #[serde(skip_serializing_if = "Option::is_none")]
5338 pub canvases: Option<bool>,
5339}
5340
5341#[derive(Debug, Clone, Default)]
5343pub struct UiInputOptions<'a> {
5344 pub title: Option<&'a str>,
5346 pub description: Option<&'a str>,
5348 pub min_length: Option<u64>,
5350 pub max_length: Option<u64>,
5352 pub format: Option<InputFormat>,
5354 pub default: Option<&'a str>,
5356}
5357
5358#[derive(Debug, Clone, Copy)]
5360#[non_exhaustive]
5361pub enum InputFormat {
5362 Email,
5364 Uri,
5366 Date,
5368 DateTime,
5370}
5371
5372impl InputFormat {
5373 pub fn as_str(&self) -> &'static str {
5375 match self {
5376 Self::Email => "email",
5377 Self::Uri => "uri",
5378 Self::Date => "date",
5379 Self::DateTime => "date-time",
5380 }
5381 }
5382}
5383
5384pub use crate::generated::api_types::{
5389 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5390 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5391 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5392 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
5393};
5394
5395#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5401#[serde(rename_all = "kebab-case")]
5402#[non_exhaustive]
5403pub enum PermissionRequestKind {
5404 Shell,
5406 Write,
5408 Read,
5410 Url,
5412 Mcp,
5414 CustomTool,
5416 Memory,
5418 Hook,
5420 #[serde(other)]
5423 Unknown,
5424}
5425
5426#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5432#[serde(rename_all = "camelCase")]
5433pub struct PermissionRequestData {
5434 #[serde(default, skip_serializing_if = "Option::is_none")]
5438 pub kind: Option<PermissionRequestKind>,
5439 #[serde(default, skip_serializing_if = "Option::is_none")]
5442 pub tool_call_id: Option<String>,
5443 #[serde(flatten)]
5446 pub extra: Value,
5447}
5448
5449#[derive(Debug, Clone, Serialize, Deserialize)]
5451#[serde(rename_all = "camelCase")]
5452pub struct ExitPlanModeData {
5453 #[serde(default)]
5455 pub summary: String,
5456 #[serde(default, skip_serializing_if = "Option::is_none")]
5458 pub plan_content: Option<String>,
5459 #[serde(default)]
5461 pub actions: Vec<String>,
5462 #[serde(default = "default_recommended_action")]
5464 pub recommended_action: String,
5465}
5466
5467fn default_recommended_action() -> String {
5468 "autopilot".to_string()
5469}
5470
5471impl Default for ExitPlanModeData {
5472 fn default() -> Self {
5473 Self {
5474 summary: String::new(),
5475 plan_content: None,
5476 actions: Vec::new(),
5477 recommended_action: default_recommended_action(),
5478 }
5479 }
5480}
5481
5482#[cfg(test)]
5483mod tests {
5484 use std::collections::HashMap;
5485 use std::path::PathBuf;
5486
5487 use serde_json::json;
5488
5489 use super::{
5490 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5491 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5492 CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry,
5493 ExpFlagValue, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig,
5494 LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration,
5495 NamedProviderConfig, ProviderConfig, ProviderModelConfig, ReasoningSummary,
5496 ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, SystemMessageConfig, Tool,
5497 ToolBinaryResult, ToolResult, ToolResultExpanded, ToolResultResponse,
5498 ensure_attachment_display_names,
5499 };
5500 use crate::generated::session_events::TypedSessionEvent;
5501
5502 #[test]
5503 fn tool_builder_composes() {
5504 let tool = Tool::new("greet")
5505 .with_description("Say hello")
5506 .with_namespaced_name("hello/greet")
5507 .with_instructions("Pass the user's name")
5508 .with_parameters(json!({
5509 "type": "object",
5510 "properties": { "name": { "type": "string" } },
5511 "required": ["name"]
5512 }))
5513 .with_overrides_built_in_tool(true)
5514 .with_skip_permission(true);
5515 assert_eq!(tool.name, "greet");
5516 assert_eq!(tool.description, "Say hello");
5517 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5518 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5519 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5520 assert!(tool.overrides_built_in_tool);
5521 assert!(tool.skip_permission);
5522 }
5523
5524 #[test]
5525 fn tool_defer_serialization() {
5526 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5527 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5528 let value = serde_json::to_value(&tool).unwrap();
5529 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5530
5531 let plain = Tool::new("plain");
5532 let value = serde_json::to_value(&plain).unwrap();
5533 assert!(value.get("defer").is_none());
5534 }
5535
5536 #[test]
5537 fn tool_metadata_serialization() {
5538 use indexmap::IndexMap;
5539
5540 let mut metadata = IndexMap::new();
5541 metadata.insert(
5542 "github.com/copilot:safeForTelemetry".to_string(),
5543 json!({ "name": true, "inputsNames": false }),
5544 );
5545 let tool = Tool::new("lookup").with_metadata(metadata);
5546 let value = serde_json::to_value(&tool).unwrap();
5547 assert_eq!(
5548 value
5549 .get("metadata")
5550 .unwrap()
5551 .get("github.com/copilot:safeForTelemetry")
5552 .unwrap(),
5553 &json!({ "name": true, "inputsNames": false })
5554 );
5555
5556 let plain = Tool::new("plain");
5558 let value = serde_json::to_value(&plain).unwrap();
5559 assert!(value.get("metadata").is_none());
5560 }
5561
5562 #[test]
5563 fn custom_agent_config_builder_with_model() {
5564 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5565 .with_model("claude-haiku-4.5")
5566 .with_display_name("My Agent");
5567 assert_eq!(agent.name, "my-agent");
5568 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5569 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5570 }
5571
5572 #[test]
5573 fn custom_agent_config_serializes_model() {
5574 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5575 let wire = serde_json::to_value(&agent).unwrap();
5576 assert_eq!(wire["model"], "claude-haiku-4.5");
5577 assert_eq!(wire["name"], "model-agent");
5578 }
5579
5580 #[test]
5581 fn custom_agent_config_omits_model_when_none() {
5582 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5583 let wire = serde_json::to_value(&agent).unwrap();
5584 assert!(wire.get("model").is_none());
5585 }
5586
5587 #[test]
5588 fn custom_agent_config_builder_with_reasoning_effort() {
5589 let agent =
5590 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5591 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
5592 }
5593
5594 #[test]
5595 fn custom_agent_config_serializes_reasoning_effort() {
5596 let agent =
5597 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5598 let wire = serde_json::to_value(&agent).unwrap();
5599 assert_eq!(wire["reasoningEffort"], "high");
5600 }
5601
5602 #[test]
5603 fn custom_agent_config_omits_reasoning_effort_when_none() {
5604 let agent = CustomAgentConfig::new("default-agent", "prompt");
5605 let wire = serde_json::to_value(&agent).unwrap();
5606 assert!(wire.get("reasoningEffort").is_none());
5607 }
5608
5609 #[test]
5610 #[should_panic(expected = "tool parameter schema must be a JSON object")]
5611 fn tool_with_parameters_panics_on_non_object_value() {
5612 let _ = Tool::new("noop").with_parameters(json!(null));
5613 }
5614
5615 #[test]
5616 fn tool_result_expanded_serializes_binary_results_for_llm() {
5617 let response = ToolResultResponse {
5618 result: ToolResult::Expanded(ToolResultExpanded {
5619 text_result_for_llm: "rendered chart".to_string(),
5620 result_type: "success".to_string(),
5621 binary_results_for_llm: Some(vec![ToolBinaryResult {
5622 data: "aW1n".to_string(),
5623 mime_type: "image/png".to_string(),
5624 r#type: "image".to_string(),
5625 description: Some("chart preview".to_string()),
5626 }]),
5627 session_log: None,
5628 error: None,
5629 tool_telemetry: None,
5630 tool_references: None,
5631 }),
5632 };
5633
5634 let wire = serde_json::to_value(&response).unwrap();
5635
5636 assert_eq!(
5637 wire,
5638 json!({
5639 "result": {
5640 "textResultForLlm": "rendered chart",
5641 "resultType": "success",
5642 "binaryResultsForLlm": [
5643 {
5644 "data": "aW1n",
5645 "mimeType": "image/png",
5646 "type": "image",
5647 "description": "chart preview"
5648 }
5649 ]
5650 }
5651 })
5652 );
5653 }
5654
5655 #[test]
5656 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
5657 let response = ToolResultResponse {
5658 result: ToolResult::Expanded(ToolResultExpanded {
5659 text_result_for_llm: "ok".to_string(),
5660 result_type: "success".to_string(),
5661 binary_results_for_llm: None,
5662 session_log: None,
5663 error: None,
5664 tool_telemetry: None,
5665 tool_references: None,
5666 }),
5667 };
5668
5669 let wire = serde_json::to_value(&response).unwrap();
5670
5671 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5672 assert!(wire["result"].get("binaryResultsForLlm").is_none());
5673 }
5674
5675 #[test]
5676 fn tool_result_expanded_serializes_tool_references() {
5677 let response = ToolResultResponse {
5678 result: ToolResult::Expanded(
5679 ToolResultExpanded::new("found 2 tools", "success")
5680 .with_tool_references(["get_weather", "check_status"]),
5681 ),
5682 };
5683
5684 let wire = serde_json::to_value(&response).unwrap();
5685
5686 assert_eq!(
5687 wire,
5688 json!({
5689 "result": {
5690 "textResultForLlm": "found 2 tools",
5691 "resultType": "success",
5692 "toolReferences": ["get_weather", "check_status"]
5693 }
5694 })
5695 );
5696 }
5697
5698 #[test]
5699 fn tool_result_expanded_omits_tool_references_when_none() {
5700 let response = ToolResultResponse {
5701 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
5702 };
5703
5704 let wire = serde_json::to_value(&response).unwrap();
5705
5706 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5707 assert!(wire["result"].get("toolReferences").is_none());
5708 }
5709
5710 #[test]
5711 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
5712 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
5715 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
5716
5717 assert_eq!(
5718 expanded.tool_references.as_deref(),
5719 Some(["alpha".to_string(), "beta".to_string()].as_slice())
5720 );
5721 }
5722
5723 #[test]
5724 fn tool_result_expanded_deserializes_tool_references() {
5725 let wire = json!({
5726 "textResultForLlm": "found tools",
5727 "resultType": "success",
5728 "toolReferences": ["alpha", "beta"]
5729 });
5730
5731 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
5732
5733 assert_eq!(
5734 expanded.tool_references.as_deref(),
5735 Some(["alpha".to_string(), "beta".to_string()].as_slice())
5736 );
5737 }
5738
5739 #[test]
5740 fn session_config_default_wire_flags_off_without_handlers() {
5741 let cfg = SessionConfig::default();
5742 assert_eq!(cfg.mcp_oauth_token_storage, None);
5743 let (wire, _runtime) = cfg
5747 .into_wire(Some(SessionId::from("default-flags")))
5748 .expect("default config has no duplicate handlers");
5749 assert!(!wire.request_user_input);
5750 assert!(!wire.request_permission);
5751 assert!(!wire.request_elicitation);
5752 assert!(!wire.request_exit_plan_mode);
5753 assert!(!wire.request_auto_mode_switch);
5754 assert!(!wire.hooks);
5755 assert!(!wire.request_mcp_apps);
5756 }
5757
5758 #[test]
5759 fn resume_session_config_new_wire_flags_off_without_handlers() {
5760 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
5761 assert_eq!(cfg.mcp_oauth_token_storage, None);
5762 let (wire, _runtime) = cfg
5763 .into_wire()
5764 .expect("default resume config has no duplicate handlers");
5765 assert!(!wire.request_user_input);
5766 assert!(!wire.request_permission);
5767 assert!(!wire.request_elicitation);
5768 assert!(!wire.request_exit_plan_mode);
5769 assert!(!wire.request_auto_mode_switch);
5770 assert!(!wire.hooks);
5771 assert!(!wire.request_mcp_apps);
5772 }
5773
5774 #[test]
5775 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5776 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
5777 assert_eq!(cfg.enable_mcp_apps, Some(true));
5778
5779 let (wire, _runtime) = cfg
5780 .into_wire(Some(SessionId::from("enable-mcp-apps")))
5781 .expect("enable_mcp_apps config has no duplicate handlers");
5782 assert!(wire.request_mcp_apps);
5783
5784 let json = serde_json::to_value(&wire).unwrap();
5785 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5786 }
5787
5788 #[test]
5789 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5790 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
5791 .with_enable_mcp_apps(true);
5792 assert_eq!(cfg.enable_mcp_apps, Some(true));
5793
5794 let (wire, _runtime) = cfg
5795 .into_wire()
5796 .expect("resume enable_mcp_apps config has no duplicate handlers");
5797 assert!(wire.request_mcp_apps);
5798
5799 let json = serde_json::to_value(&wire).unwrap();
5800 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5801 }
5802
5803 #[test]
5804 fn memory_configuration_constructors_and_serde() {
5805 assert!(MemoryConfiguration::enabled().enabled);
5806 assert!(!MemoryConfiguration::disabled().enabled);
5807 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
5808
5809 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
5810 assert_eq!(json, serde_json::json!({ "enabled": true }));
5811 }
5812
5813 #[test]
5814 fn session_config_with_memory_serializes() {
5815 let (wire, _runtime) = SessionConfig::default()
5816 .with_memory(MemoryConfiguration::enabled())
5817 .into_wire(Some(SessionId::from("memory-on")))
5818 .expect("no duplicate handlers");
5819 let json = serde_json::to_value(&wire).unwrap();
5820 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5821
5822 let (wire_off, _) = SessionConfig::default()
5823 .with_memory(MemoryConfiguration::disabled())
5824 .into_wire(Some(SessionId::from("memory-off")))
5825 .expect("no duplicate handlers");
5826 let json_off = serde_json::to_value(&wire_off).unwrap();
5827 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
5828
5829 let (empty_wire, _) = SessionConfig::default()
5831 .into_wire(Some(SessionId::from("memory-unset")))
5832 .expect("no duplicate handlers");
5833 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5834 assert!(empty_json.get("memory").is_none());
5835 }
5836
5837 #[test]
5838 fn resume_session_config_with_memory_serializes() {
5839 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
5840 .with_memory(MemoryConfiguration::enabled())
5841 .into_wire()
5842 .expect("no duplicate handlers");
5843 let json = serde_json::to_value(&wire).unwrap();
5844 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5845
5846 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
5848 .into_wire()
5849 .expect("no duplicate handlers");
5850 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5851 assert!(empty_json.get("memory").is_none());
5852 }
5853
5854 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
5855 CopilotExpAssignmentResponse {
5856 features: vec!["copilot_exp_flag".to_string()],
5857 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
5858 configs: vec![ExpConfigEntry {
5859 id: "cfg-1".to_string(),
5860 parameters: HashMap::from([
5861 ("threshold".to_string(), ExpFlagValue::Integer(5)),
5862 ("enabled".to_string(), ExpFlagValue::Bool(true)),
5863 ]),
5864 }],
5865 assignment_context: context.to_string(),
5866 ..Default::default()
5867 }
5868 }
5869
5870 #[test]
5871 fn exp_flag_value_round_trips_all_variants() {
5872 let values = serde_json::json!({
5873 "s": "text",
5874 "i": 7,
5875 "f": 1.5,
5876 "b": true,
5877 "n": null,
5878 });
5879 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
5880 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
5881 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
5882 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
5883 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
5884 assert_eq!(parsed["n"], ExpFlagValue::Null);
5885 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
5886 }
5887
5888 #[test]
5889 fn session_config_with_exp_assignments_serializes() {
5890 let assignments = sample_exp_assignments("ctx-123");
5891 let expected = serde_json::to_value(&assignments).unwrap();
5892 let (wire, _runtime) = SessionConfig::default()
5893 .with_exp_assignments(assignments)
5894 .into_wire(Some(SessionId::from("exp-on")))
5895 .expect("no duplicate handlers");
5896 let json = serde_json::to_value(&wire).unwrap();
5897 assert_eq!(json["expAssignments"], expected);
5898 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
5899 assert_eq!(
5900 json["expAssignments"]["Flights"]["copilot_exp_flag"],
5901 "treatment"
5902 );
5903
5904 let (empty_wire, _) = SessionConfig::default()
5906 .into_wire(Some(SessionId::from("exp-unset")))
5907 .expect("no duplicate handlers");
5908 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5909 assert!(empty_json.get("expAssignments").is_none());
5910 }
5911
5912 #[test]
5913 fn resume_session_config_with_exp_assignments_serializes() {
5914 let assignments = sample_exp_assignments("ctx-456");
5915 let expected = serde_json::to_value(&assignments).unwrap();
5916 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
5917 .with_exp_assignments(assignments)
5918 .into_wire()
5919 .expect("no duplicate handlers");
5920 let json = serde_json::to_value(&wire).unwrap();
5921 assert_eq!(json["expAssignments"], expected);
5922
5923 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
5925 .into_wire()
5926 .expect("no duplicate handlers");
5927 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5928 assert!(empty_json.get("expAssignments").is_none());
5929 }
5930
5931 #[test]
5932 fn session_config_clone_preserves_exp_assignments() {
5933 let assignments = sample_exp_assignments("ctx-clone");
5934 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
5935 let cloned = config.clone();
5936
5937 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5938
5939 let (wire, _runtime) = cloned
5940 .into_wire(Some(SessionId::from("exp-clone")))
5941 .expect("no duplicate handlers");
5942 let json = serde_json::to_value(&wire).unwrap();
5943 assert_eq!(
5944 json["expAssignments"],
5945 serde_json::to_value(&assignments).unwrap()
5946 );
5947 }
5948
5949 #[test]
5950 fn resume_session_config_clone_preserves_exp_assignments() {
5951 let assignments = sample_exp_assignments("ctx-clone-resume");
5952 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
5953 .with_exp_assignments(assignments.clone());
5954 let cloned = config.clone();
5955
5956 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5957
5958 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
5959 let json = serde_json::to_value(&wire).unwrap();
5960 assert_eq!(
5961 json["expAssignments"],
5962 serde_json::to_value(&assignments).unwrap()
5963 );
5964 }
5965
5966 #[test]
5967 #[allow(clippy::field_reassign_with_default)]
5968 fn session_config_into_wire_serializes_bucket_b_fields() {
5969 use std::path::PathBuf;
5970
5971 use super::{CloudSessionOptions, CloudSessionRepository};
5972
5973 let mut cfg = SessionConfig::default();
5974 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5975 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5976 cfg.github_token = Some("ghs_secret".to_string());
5977 cfg.include_sub_agent_streaming_events = Some(false);
5978 cfg.enable_session_telemetry = Some(false);
5979 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
5980 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
5981 cfg.enable_on_demand_instruction_discovery = Some(false);
5982 cfg.cloud = Some(CloudSessionOptions::with_repository(
5983 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
5984 ));
5985
5986 let (wire, _runtime) = cfg
5987 .into_wire(Some(SessionId::from("custom-id")))
5988 .expect("no duplicate handlers");
5989 let wire_json = serde_json::to_value(&wire).unwrap();
5990 assert_eq!(wire_json["sessionId"], "custom-id");
5991 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5992 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5993 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5994 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
5995 assert_eq!(wire_json["enableSessionTelemetry"], false);
5996 assert_eq!(wire_json["reasoningSummary"], "concise");
5997 assert_eq!(wire_json["remoteSession"], "export");
5998 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5999 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6000 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6001 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6002
6003 let (empty_wire, _) = SessionConfig::default()
6005 .into_wire(Some(SessionId::from("empty")))
6006 .expect("default has no duplicate handlers");
6007 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6008 assert!(empty_json.get("gitHubToken").is_none());
6009 assert!(empty_json.get("enableSessionTelemetry").is_none());
6010 assert!(empty_json.get("reasoningSummary").is_none());
6011 assert!(empty_json.get("remoteSession").is_none());
6012 assert!(
6013 empty_json
6014 .get("enableOnDemandInstructionDiscovery")
6015 .is_none()
6016 );
6017 assert!(empty_json.get("cloud").is_none());
6018 }
6019
6020 #[test]
6021 fn session_config_into_wire_serializes_named_providers_and_models() {
6022 let cfg = SessionConfig::default()
6023 .with_providers(vec![
6024 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6025 .with_provider_type("openai")
6026 .with_wire_api("responses")
6027 .with_api_key("sk-test"),
6028 ])
6029 .with_models(vec![
6030 ProviderModelConfig::new("gpt-x", "my-openai")
6031 .with_wire_model("gpt-x-2025")
6032 .with_max_output_tokens(2048),
6033 ]);
6034
6035 let (wire, _) = cfg
6036 .into_wire(Some(SessionId::from("sess-providers")))
6037 .expect("no duplicate handlers");
6038 let wire_json = serde_json::to_value(&wire).unwrap();
6039 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6040 assert_eq!(
6041 wire_json["providers"][0]["baseUrl"],
6042 "https://api.example.com/v1"
6043 );
6044 assert_eq!(wire_json["providers"][0]["type"], "openai");
6045 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6046 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6047 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
6048 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
6049 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
6050 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
6051
6052 let (empty_wire, _) = SessionConfig::default()
6053 .into_wire(Some(SessionId::from("empty")))
6054 .expect("default has no duplicate handlers");
6055 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6056 assert!(empty_json.get("providers").is_none());
6057 assert!(empty_json.get("models").is_none());
6058 }
6059
6060 #[test]
6061 fn resume_config_into_wire_serializes_named_providers_and_models() {
6062 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
6063 .with_providers(vec![
6064 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
6065 .with_provider_type("azure")
6066 .with_azure(AzureProviderOptions {
6067 api_version: Some("2024-10-21".to_string()),
6068 }),
6069 ])
6070 .with_models(vec![
6071 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
6072 ]);
6073
6074 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6075 let wire_json = serde_json::to_value(&wire).unwrap();
6076 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
6077 assert_eq!(wire_json["providers"][0]["type"], "azure");
6078 assert_eq!(
6079 wire_json["providers"][0]["azure"]["apiVersion"],
6080 "2024-10-21"
6081 );
6082 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
6083 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
6084 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
6085
6086 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
6087 .into_wire()
6088 .expect("default has no duplicate handlers");
6089 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6090 assert!(empty_json.get("providers").is_none());
6091 assert!(empty_json.get("models").is_none());
6092 }
6093
6094 #[test]
6095 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
6096 use std::path::PathBuf;
6097
6098 let cfg = SessionConfig {
6099 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
6100 large_output: Some(
6101 LargeToolOutputConfig::new()
6102 .with_enabled(true)
6103 .with_max_size_bytes(1024)
6104 .with_output_directory(PathBuf::from("/tmp/large-output")),
6105 ),
6106 ..Default::default()
6107 };
6108
6109 let (wire, _) = cfg
6110 .into_wire(Some(SessionId::from("sess-1")))
6111 .expect("no duplicate handlers");
6112 let wire_json = serde_json::to_value(&wire).unwrap();
6113 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
6114 assert_eq!(wire_json["largeOutput"]["enabled"], true);
6115 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
6116 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
6117
6118 let (empty_wire, _) = SessionConfig::default()
6119 .into_wire(Some(SessionId::from("empty")))
6120 .expect("default has no duplicate handlers");
6121 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6122 assert!(empty_json.get("pluginDirectories").is_none());
6123 assert!(empty_json.get("largeOutput").is_none());
6124 }
6125
6126 #[test]
6127 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
6128 use std::path::PathBuf;
6129
6130 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6131 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6132 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6133 cfg.github_token = Some("ghs_secret".to_string());
6134 cfg.include_sub_agent_streaming_events = Some(true);
6135 cfg.enable_session_telemetry = Some(false);
6136 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
6137 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
6138 cfg.enable_on_demand_instruction_discovery = Some(false);
6139
6140 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6141 let wire_json = serde_json::to_value(&wire).unwrap();
6142 assert_eq!(wire_json["sessionId"], "sess-1");
6143 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6144 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6145 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6146 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
6147 assert_eq!(wire_json["enableSessionTelemetry"], false);
6148 assert_eq!(wire_json["reasoningSummary"], "detailed");
6149 assert_eq!(wire_json["remoteSession"], "on");
6150 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6151
6152 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6154 .into_wire()
6155 .expect("default resume has no duplicate handlers");
6156 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6157 assert!(empty_json.get("reasoningSummary").is_none());
6158 assert!(empty_json.get("remoteSession").is_none());
6159 assert!(
6160 empty_json
6161 .get("enableOnDemandInstructionDiscovery")
6162 .is_none()
6163 );
6164 }
6165
6166 #[test]
6167 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
6168 use std::path::PathBuf;
6169
6170 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6171 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6172 cfg.large_output = Some(
6173 LargeToolOutputConfig::new()
6174 .with_enabled(false)
6175 .with_max_size_bytes(2048)
6176 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6177 );
6178
6179 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6180 let wire_json = serde_json::to_value(&wire).unwrap();
6181 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6182 assert_eq!(wire_json["largeOutput"]["enabled"], false);
6183 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6184 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6185
6186 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6187 .into_wire()
6188 .expect("default resume has no duplicate handlers");
6189 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6190 assert!(empty_json.get("pluginDirectories").is_none());
6191 assert!(empty_json.get("largeOutput").is_none());
6192 }
6193
6194 #[test]
6195 fn session_config_builder_composes() {
6196 use indexmap::IndexMap;
6197
6198 let cfg = SessionConfig::default()
6199 .with_session_id(SessionId::from("sess-1"))
6200 .with_model("claude-sonnet-4")
6201 .with_client_name("test-app")
6202 .with_reasoning_effort("medium")
6203 .with_reasoning_summary(ReasoningSummary::Concise)
6204 .with_context_tier("long_context")
6205 .with_streaming(true)
6206 .with_tools([Tool::new("greet")])
6207 .with_available_tools(["bash", "view"])
6208 .with_excluded_tools(["dangerous"])
6209 .with_mcp_servers(IndexMap::new())
6210 .with_mcp_oauth_token_storage("persistent")
6211 .with_enable_config_discovery(true)
6212 .with_enable_on_demand_instruction_discovery(true)
6213 .with_skill_directories([PathBuf::from("/tmp/skills")])
6214 .with_disabled_skills(["broken-skill"])
6215 .with_agent("researcher")
6216 .with_config_directory(PathBuf::from("/tmp/config"))
6217 .with_working_directory(PathBuf::from("/tmp/work"))
6218 .with_github_token("ghp_test")
6219 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6220 .with_enable_session_telemetry(false)
6221 .with_include_sub_agent_streaming_events(false)
6222 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6223
6224 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
6225 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
6226 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6227 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
6228 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
6229 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
6230 assert_eq!(cfg.streaming, Some(true));
6231 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6232 assert_eq!(
6233 cfg.available_tools.as_deref(),
6234 Some(&["bash".to_string(), "view".to_string()][..])
6235 );
6236 assert_eq!(
6237 cfg.excluded_tools.as_deref(),
6238 Some(&["dangerous".to_string()][..])
6239 );
6240 assert!(cfg.mcp_servers.is_some());
6241 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6242 assert_eq!(cfg.enable_config_discovery, Some(true));
6243 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
6244 assert_eq!(
6245 cfg.skill_directories.as_deref(),
6246 Some(&[PathBuf::from("/tmp/skills")][..])
6247 );
6248 assert_eq!(
6249 cfg.disabled_skills.as_deref(),
6250 Some(&["broken-skill".to_string()][..])
6251 );
6252 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6253 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6254 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6255 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6256 assert_eq!(
6257 cfg.capi,
6258 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6259 );
6260 assert_eq!(cfg.enable_session_telemetry, Some(false));
6261 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
6262 assert_eq!(
6263 cfg.extension_info,
6264 Some(ExtensionInfo::new("github-app", "counter"))
6265 );
6266 }
6267
6268 #[test]
6269 fn resume_session_config_builder_composes() {
6270 use indexmap::IndexMap;
6271
6272 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
6273 .with_client_name("test-app")
6274 .with_reasoning_summary(ReasoningSummary::None)
6275 .with_context_tier("default")
6276 .with_streaming(true)
6277 .with_tools([Tool::new("greet")])
6278 .with_available_tools(["bash", "view"])
6279 .with_excluded_tools(["dangerous"])
6280 .with_mcp_servers(IndexMap::new())
6281 .with_mcp_oauth_token_storage("persistent")
6282 .with_enable_config_discovery(true)
6283 .with_enable_on_demand_instruction_discovery(false)
6284 .with_skill_directories([PathBuf::from("/tmp/skills")])
6285 .with_disabled_skills(["broken-skill"])
6286 .with_agent("researcher")
6287 .with_config_directory(PathBuf::from("/tmp/config"))
6288 .with_working_directory(PathBuf::from("/tmp/work"))
6289 .with_github_token("ghp_test")
6290 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6291 .with_enable_session_telemetry(false)
6292 .with_include_sub_agent_streaming_events(true)
6293 .with_suppress_resume_event(true)
6294 .with_continue_pending_work(true)
6295 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6296
6297 assert_eq!(cfg.session_id.as_str(), "sess-2");
6298 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6299 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
6300 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
6301 assert_eq!(cfg.streaming, Some(true));
6302 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6303 assert_eq!(
6304 cfg.available_tools.as_deref(),
6305 Some(&["bash".to_string(), "view".to_string()][..])
6306 );
6307 assert_eq!(
6308 cfg.excluded_tools.as_deref(),
6309 Some(&["dangerous".to_string()][..])
6310 );
6311 assert!(cfg.mcp_servers.is_some());
6312 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6313 assert_eq!(cfg.enable_config_discovery, Some(true));
6314 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
6315 assert_eq!(
6316 cfg.skill_directories.as_deref(),
6317 Some(&[PathBuf::from("/tmp/skills")][..])
6318 );
6319 assert_eq!(
6320 cfg.disabled_skills.as_deref(),
6321 Some(&["broken-skill".to_string()][..])
6322 );
6323 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6324 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6325 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6326 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6327 assert_eq!(
6328 cfg.capi,
6329 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6330 );
6331 assert_eq!(cfg.enable_session_telemetry, Some(false));
6332 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
6333 assert_eq!(cfg.suppress_resume_event, Some(true));
6334 assert_eq!(cfg.continue_pending_work, Some(true));
6335 assert_eq!(
6336 cfg.extension_info,
6337 Some(ExtensionInfo::new("github-app", "counter"))
6338 );
6339 }
6340
6341 #[test]
6345 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
6346 let cfg =
6347 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
6348 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6349 let json = serde_json::to_value(&wire).unwrap();
6350 assert_eq!(json["continuePendingWork"], true);
6351
6352 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6354 .into_wire()
6355 .expect("no duplicate handlers");
6356 let json = serde_json::to_value(&wire).unwrap();
6357 assert!(json.get("continuePendingWork").is_none());
6358 }
6359
6360 #[test]
6364 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
6365 let cfg =
6366 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
6367 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6368 let json = serde_json::to_value(&wire).unwrap();
6369 assert_eq!(json["disableResume"], true);
6370 assert!(json.get("suppressResumeEvent").is_none());
6371 }
6372
6373 #[test]
6376 fn session_config_serializes_instruction_directories_to_camel_case() {
6377 let cfg =
6378 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
6379 let (wire, _) = cfg
6380 .into_wire(Some(SessionId::from("instr-on")))
6381 .expect("no duplicate handlers");
6382 let json = serde_json::to_value(&wire).unwrap();
6383 assert_eq!(
6384 json["instructionDirectories"],
6385 serde_json::json!(["/tmp/instr"])
6386 );
6387
6388 let (wire, _) = SessionConfig::default()
6390 .into_wire(Some(SessionId::from("instr-off")))
6391 .expect("no duplicate handlers");
6392 let json = serde_json::to_value(&wire).unwrap();
6393 assert!(json.get("instructionDirectories").is_none());
6394 }
6395
6396 #[test]
6399 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
6400 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
6401 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
6402 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6403 let json = serde_json::to_value(&wire).unwrap();
6404 assert_eq!(
6405 json["instructionDirectories"],
6406 serde_json::json!(["/tmp/instr"])
6407 );
6408
6409 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6410 .into_wire()
6411 .expect("no duplicate handlers");
6412 let json = serde_json::to_value(&wire).unwrap();
6413 assert!(json.get("instructionDirectories").is_none());
6414 }
6415
6416 #[test]
6417 fn custom_agent_config_builder_composes() {
6418 use indexmap::IndexMap;
6419
6420 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
6421 .with_display_name("Research Assistant")
6422 .with_description("Investigates technical questions.")
6423 .with_tools(["bash", "view"])
6424 .with_mcp_servers(IndexMap::new())
6425 .with_infer(true)
6426 .with_skills(["rust-coding-skill"]);
6427
6428 assert_eq!(cfg.name, "researcher");
6429 assert_eq!(cfg.prompt, "You are a research assistant.");
6430 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
6431 assert_eq!(
6432 cfg.description.as_deref(),
6433 Some("Investigates technical questions.")
6434 );
6435 assert_eq!(
6436 cfg.tools.as_deref(),
6437 Some(&["bash".to_string(), "view".to_string()][..])
6438 );
6439 assert!(cfg.mcp_servers.is_some());
6440 assert_eq!(cfg.infer, Some(true));
6441 assert_eq!(
6442 cfg.skills.as_deref(),
6443 Some(&["rust-coding-skill".to_string()][..])
6444 );
6445 }
6446
6447 #[test]
6448 fn mcp_servers_serialize_in_insertion_order() {
6449 use indexmap::IndexMap;
6450
6451 let order = [
6457 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
6458 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
6459 ];
6460 let mut servers = IndexMap::new();
6461 for name in order {
6462 servers.insert(
6463 name.to_string(),
6464 McpServerConfig::Stdio(McpStdioServerConfig {
6465 command: "run".to_string(),
6466 ..Default::default()
6467 }),
6468 );
6469 }
6470
6471 let (wire, _runtime) = SessionConfig::default()
6472 .with_mcp_servers(servers)
6473 .into_wire(None)
6474 .expect("into_wire should succeed");
6475 let json = serde_json::to_string(&wire).expect("serialize wire");
6476
6477 let positions: Vec<usize> = order
6478 .iter()
6479 .map(|name| {
6480 json.find(&format!("\"{name}\""))
6481 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
6482 })
6483 .collect();
6484 let mut ascending = positions.clone();
6485 ascending.sort_unstable();
6486 assert_eq!(
6487 positions, ascending,
6488 "mcp server keys must serialize in insertion order: {json}"
6489 );
6490 }
6491
6492 #[test]
6493 fn infinite_session_config_builder_composes() {
6494 let cfg = InfiniteSessionConfig::new()
6495 .with_enabled(true)
6496 .with_background_compaction_threshold(0.75)
6497 .with_buffer_exhaustion_threshold(0.92);
6498
6499 assert_eq!(cfg.enabled, Some(true));
6500 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
6501 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
6502 }
6503
6504 #[test]
6505 fn provider_config_builder_composes() {
6506 use std::collections::HashMap;
6507
6508 let mut headers = HashMap::new();
6509 headers.insert("X-Custom".to_string(), "value".to_string());
6510
6511 let cfg = ProviderConfig::new("https://api.example.com")
6512 .with_provider_type("openai")
6513 .with_wire_api("completions")
6514 .with_transport("websockets")
6515 .with_api_key("sk-test")
6516 .with_bearer_token("bearer-test")
6517 .with_headers(headers)
6518 .with_model_id("gpt-4")
6519 .with_wire_model("azure-gpt-4-deployment")
6520 .with_max_prompt_tokens(8192)
6521 .with_max_output_tokens(2048);
6522
6523 assert_eq!(cfg.base_url, "https://api.example.com");
6524 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
6525 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
6526 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
6527 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
6528 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
6529 assert_eq!(
6530 cfg.headers
6531 .as_ref()
6532 .and_then(|h| h.get("X-Custom"))
6533 .map(String::as_str),
6534 Some("value"),
6535 );
6536 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
6537 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
6538 assert_eq!(cfg.max_prompt_tokens, Some(8192));
6539 assert_eq!(cfg.max_output_tokens, Some(2048));
6540
6541 let wire = serde_json::to_value(&cfg).unwrap();
6543 assert_eq!(wire["modelId"], "gpt-4");
6544 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
6545 assert_eq!(wire["maxPromptTokens"], 8192);
6546 assert_eq!(wire["maxOutputTokens"], 2048);
6547
6548 let unset = ProviderConfig::new("https://api.example.com");
6549 let wire_unset = serde_json::to_value(&unset).unwrap();
6550 assert!(wire_unset.get("modelId").is_none());
6551 assert!(wire_unset.get("wireModel").is_none());
6552 assert!(wire_unset.get("maxPromptTokens").is_none());
6553 assert!(wire_unset.get("maxOutputTokens").is_none());
6554 }
6555
6556 #[test]
6557 fn capi_session_options_builder_composes_and_serializes() {
6558 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
6559
6560 assert_eq!(cfg.enable_web_socket_responses, Some(false));
6561
6562 let wire = serde_json::to_value(&cfg).unwrap();
6563 assert_eq!(
6564 wire,
6565 serde_json::json!({ "enableWebSocketResponses": false })
6566 );
6567
6568 let unset = CapiSessionOptions::new();
6569 let wire_unset = serde_json::to_value(&unset).unwrap();
6570 assert!(wire_unset.get("enableWebSocketResponses").is_none());
6571 }
6572
6573 #[test]
6574 fn session_config_with_capi_serializes() {
6575 let (wire, _) = SessionConfig::default()
6576 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6577 .into_wire(Some(SessionId::from("capi-create")))
6578 .expect("no duplicate handlers");
6579 let json = serde_json::to_value(&wire).unwrap();
6580 assert_eq!(
6581 json["capi"],
6582 serde_json::json!({ "enableWebSocketResponses": false })
6583 );
6584
6585 let (empty_wire, _) = SessionConfig::default()
6586 .into_wire(Some(SessionId::from("capi-create-unset")))
6587 .expect("no duplicate handlers");
6588 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6589 assert!(empty_json.get("capi").is_none());
6590 }
6591
6592 #[test]
6593 fn resume_session_config_with_capi_serializes() {
6594 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
6595 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6596 .into_wire()
6597 .expect("no duplicate handlers");
6598 let json = serde_json::to_value(&wire).unwrap();
6599 assert_eq!(
6600 json["capi"],
6601 serde_json::json!({ "enableWebSocketResponses": false })
6602 );
6603
6604 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
6605 .into_wire()
6606 .expect("no duplicate handlers");
6607 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6608 assert!(empty_json.get("capi").is_none());
6609 }
6610
6611 #[test]
6612 fn system_message_config_builder_composes() {
6613 use std::collections::HashMap;
6614
6615 let cfg = SystemMessageConfig::new()
6616 .with_mode("replace")
6617 .with_content("Custom system message.")
6618 .with_sections(HashMap::new());
6619
6620 assert_eq!(cfg.mode.as_deref(), Some("replace"));
6621 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
6622 assert!(cfg.sections.is_some());
6623 }
6624
6625 #[test]
6626 fn delivery_mode_serializes_to_kebab_case_strings() {
6627 assert_eq!(
6628 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
6629 "\"enqueue\""
6630 );
6631 assert_eq!(
6632 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
6633 "\"immediate\""
6634 );
6635 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
6636 assert_eq!(parsed, DeliveryMode::Immediate);
6637 }
6638
6639 #[test]
6640 fn agent_mode_serializes_to_kebab_case_strings() {
6641 assert_eq!(
6642 serde_json::to_string(&AgentMode::Interactive).unwrap(),
6643 "\"interactive\""
6644 );
6645 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
6646 assert_eq!(
6647 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
6648 "\"autopilot\""
6649 );
6650 assert_eq!(
6651 serde_json::to_string(&AgentMode::Shell).unwrap(),
6652 "\"shell\""
6653 );
6654 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
6655 assert_eq!(parsed, AgentMode::Plan);
6656 }
6657
6658 #[test]
6659 fn connection_state_distinguishes_variants() {
6660 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
6663 }
6664
6665 #[test]
6671 fn session_event_round_trips_agent_id_on_envelope() {
6672 let wire = json!({
6673 "id": "evt-1",
6674 "timestamp": "2026-04-30T12:00:00Z",
6675 "parentId": null,
6676 "agentId": "sub-agent-42",
6677 "type": "assistant.message",
6678 "data": { "message": "hi" }
6679 });
6680
6681 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
6682 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6683
6684 let roundtripped = serde_json::to_value(&event).unwrap();
6686 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6687
6688 let main_agent_event: SessionEvent = serde_json::from_value(json!({
6690 "id": "evt-2",
6691 "timestamp": "2026-04-30T12:00:01Z",
6692 "parentId": null,
6693 "type": "session.idle",
6694 "data": {}
6695 }))
6696 .unwrap();
6697 assert!(main_agent_event.agent_id.is_none());
6698 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
6699 assert!(roundtripped.get("agentId").is_none());
6700 }
6701
6702 #[test]
6704 fn typed_session_event_round_trips_agent_id_on_envelope() {
6705 let wire = json!({
6706 "id": "evt-1",
6707 "timestamp": "2026-04-30T12:00:00Z",
6708 "parentId": null,
6709 "agentId": "sub-agent-42",
6710 "type": "session.idle",
6711 "data": {}
6712 });
6713
6714 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
6715 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6716
6717 let roundtripped = serde_json::to_value(&event).unwrap();
6718 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6719 }
6720
6721 #[test]
6722 fn connection_state_variants_compile() {
6723 let _ = ConnectionState::Disconnected;
6727 let _ = ConnectionState::Connecting;
6728 let _ = ConnectionState::Connected;
6729 let _ = ConnectionState::Error;
6730 }
6731
6732 #[test]
6733 fn deserializes_runtime_attachment_variants() {
6734 let attachments: Vec<Attachment> = serde_json::from_value(json!([
6735 {
6736 "type": "file",
6737 "path": "/tmp/file.rs",
6738 "displayName": "file.rs",
6739 "lineRange": { "start": 7, "end": 12 }
6740 },
6741 {
6742 "type": "directory",
6743 "path": "/tmp/project",
6744 "displayName": "project"
6745 },
6746 {
6747 "type": "selection",
6748 "filePath": "/tmp/lib.rs",
6749 "displayName": "lib.rs",
6750 "text": "fn main() {}",
6751 "selection": {
6752 "start": { "line": 1, "character": 2 },
6753 "end": { "line": 3, "character": 4 }
6754 }
6755 },
6756 {
6757 "type": "blob",
6758 "data": "Zm9v",
6759 "mimeType": "image/png",
6760 "displayName": "image.png"
6761 },
6762 {
6763 "type": "github_reference",
6764 "number": 42,
6765 "title": "Fix rendering",
6766 "referenceType": "issue",
6767 "state": "open",
6768 "url": "https://github.com/example/repo/issues/42"
6769 }
6770 ]))
6771 .expect("attachments should deserialize");
6772
6773 assert_eq!(attachments.len(), 5);
6774 assert!(matches!(
6775 &attachments[0],
6776 Attachment::File {
6777 path,
6778 display_name,
6779 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
6780 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
6781 ));
6782 assert!(matches!(
6783 &attachments[1],
6784 Attachment::Directory { path, display_name }
6785 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
6786 ));
6787 assert!(matches!(
6788 &attachments[2],
6789 Attachment::Selection {
6790 file_path,
6791 display_name,
6792 selection:
6793 AttachmentSelectionRange {
6794 start: AttachmentSelectionPosition { line: 1, character: 2 },
6795 end: AttachmentSelectionPosition { line: 3, character: 4 },
6796 },
6797 ..
6798 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
6799 ));
6800 assert!(matches!(
6801 &attachments[3],
6802 Attachment::Blob {
6803 data,
6804 mime_type,
6805 display_name,
6806 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
6807 ));
6808 assert!(matches!(
6809 &attachments[4],
6810 Attachment::GitHubReference {
6811 number: 42,
6812 title,
6813 reference_type: GitHubReferenceType::Issue,
6814 state,
6815 url,
6816 } if title == "Fix rendering"
6817 && state == "open"
6818 && url == "https://github.com/example/repo/issues/42"
6819 ));
6820 }
6821
6822 #[test]
6823 fn ensures_display_names_for_variants_that_support_them() {
6824 let mut attachments = vec![
6825 Attachment::File {
6826 path: PathBuf::from("/tmp/file.rs"),
6827 display_name: None,
6828 line_range: None,
6829 },
6830 Attachment::Selection {
6831 file_path: PathBuf::from("/tmp/src/lib.rs"),
6832 display_name: None,
6833 text: "fn main() {}".to_string(),
6834 selection: AttachmentSelectionRange {
6835 start: AttachmentSelectionPosition {
6836 line: 0,
6837 character: 0,
6838 },
6839 end: AttachmentSelectionPosition {
6840 line: 0,
6841 character: 10,
6842 },
6843 },
6844 },
6845 Attachment::Blob {
6846 data: "Zm9v".to_string(),
6847 mime_type: "image/png".to_string(),
6848 display_name: None,
6849 },
6850 Attachment::GitHubReference {
6851 number: 7,
6852 title: "Track regressions".to_string(),
6853 reference_type: GitHubReferenceType::Issue,
6854 state: "open".to_string(),
6855 url: "https://example.com/issues/7".to_string(),
6856 },
6857 ];
6858
6859 ensure_attachment_display_names(&mut attachments);
6860
6861 assert_eq!(attachments[0].display_name(), Some("file.rs"));
6862 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
6863 assert_eq!(attachments[2].display_name(), Some("attachment"));
6864 assert_eq!(attachments[3].display_name(), None);
6865 assert_eq!(
6866 attachments[3].label(),
6867 Some("Track regressions".to_string())
6868 );
6869 }
6870
6871 #[test]
6872 fn github_anchored_attachment_variants_round_trip() {
6873 let cases = vec![
6874 (
6875 "github_commit",
6876 json!({
6877 "type": "github_commit",
6878 "message": "Fix the thing",
6879 "oid": "abc123",
6880 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
6881 "url": "https://github.com/octocat/repo/commit/abc123"
6882 }),
6883 ),
6884 (
6885 "github_release",
6886 json!({
6887 "type": "github_release",
6888 "name": "v1.2.3",
6889 "repo": { "name": "repo", "owner": "octocat" },
6890 "tagName": "v1.2.3",
6891 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
6892 }),
6893 ),
6894 (
6895 "github_actions_job",
6896 json!({
6897 "type": "github_actions_job",
6898 "conclusion": "failure",
6899 "jobId": 99,
6900 "jobName": "build",
6901 "repo": { "name": "repo", "owner": "octocat" },
6902 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
6903 "workflowName": "CI"
6904 }),
6905 ),
6906 (
6907 "github_repository",
6908 json!({
6909 "type": "github_repository",
6910 "description": "An example repository",
6911 "ref": "main",
6912 "repo": { "name": "repo", "owner": "octocat" },
6913 "url": "https://github.com/octocat/repo"
6914 }),
6915 ),
6916 (
6917 "github_file_diff",
6918 json!({
6919 "type": "github_file_diff",
6920 "base": {
6921 "path": "src/lib.rs",
6922 "ref": "main",
6923 "repo": { "name": "repo", "owner": "octocat" }
6924 },
6925 "head": {
6926 "path": "src/lib.rs",
6927 "ref": "feature",
6928 "repo": { "name": "repo", "owner": "octocat" }
6929 },
6930 "url": "https://github.com/octocat/repo/compare/main...feature"
6931 }),
6932 ),
6933 (
6934 "github_tree_comparison",
6935 json!({
6936 "type": "github_tree_comparison",
6937 "base": {
6938 "repo": { "name": "repo", "owner": "octocat" },
6939 "revision": "main"
6940 },
6941 "head": {
6942 "repo": { "name": "repo", "owner": "octocat" },
6943 "revision": "feature"
6944 },
6945 "url": "https://github.com/octocat/repo/compare/main...feature"
6946 }),
6947 ),
6948 (
6949 "github_url",
6950 json!({
6951 "type": "github_url",
6952 "url": "https://github.com/octocat/repo/wiki"
6953 }),
6954 ),
6955 (
6956 "github_file",
6957 json!({
6958 "type": "github_file",
6959 "path": "src/main.rs",
6960 "ref": "main",
6961 "repo": { "name": "repo", "owner": "octocat" },
6962 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
6963 }),
6964 ),
6965 (
6966 "github_snippet",
6967 json!({
6968 "type": "github_snippet",
6969 "lineRange": { "start": 10, "end": 20 },
6970 "path": "src/main.rs",
6971 "ref": "main",
6972 "repo": { "name": "repo", "owner": "octocat" },
6973 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
6974 }),
6975 ),
6976 ];
6977
6978 for (expected_type, input) in cases {
6979 let attachment: Attachment = serde_json::from_value(input.clone())
6980 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
6981
6982 let serialized_string = serde_json::to_string(&attachment)
6987 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
6988
6989 assert_eq!(
6991 serialized_string.matches("\"type\":").count(),
6992 1,
6993 "{expected_type} must serialize a single `type` key"
6994 );
6995
6996 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
6997 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
6998 assert_eq!(
6999 serialized.get("type").and_then(|value| value.as_str()),
7000 Some(expected_type),
7001 "{expected_type} must serialize the correct discriminator"
7002 );
7003
7004 assert_eq!(
7006 serialized, input,
7007 "{expected_type} should round-trip without data loss"
7008 );
7009 let reparsed: Attachment = serde_json::from_value(serialized)
7010 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
7011 assert_eq!(
7012 reparsed, attachment,
7013 "{expected_type} should re-deserialize to the same value"
7014 );
7015 }
7016 }
7017}
7018
7019#[cfg(test)]
7020mod permission_builder_tests {
7021 use std::sync::Arc;
7022
7023 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
7024 use crate::permission;
7025 use crate::types::{
7026 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
7027 SessionId,
7028 };
7029
7030 fn data() -> PermissionRequestData {
7031 PermissionRequestData {
7032 extra: serde_json::json!({"tool": "shell"}),
7033 ..Default::default()
7034 }
7035 }
7036
7037 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7040 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7041 }
7042
7043 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7044 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7045 }
7046
7047 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
7048 handler
7049 .handle(SessionId::from("s1"), RequestId::new("1"), data())
7050 .await
7051 }
7052
7053 #[tokio::test]
7054 async fn approve_all_with_handler_present_approves() {
7055 let cfg = SessionConfig::default()
7056 .with_permission_handler(Arc::new(ApproveAllHandler))
7057 .approve_all_permissions();
7058 let h = resolve_create(cfg).expect("policy + handler yields handler");
7059 assert!(matches!(
7060 dispatch(&h).await,
7061 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7062 ));
7063 }
7064
7065 #[tokio::test]
7066 async fn approve_all_standalone_produces_handler() {
7067 let cfg = SessionConfig::default().approve_all_permissions();
7068 let h = resolve_create(cfg).expect("policy alone yields handler");
7069 assert!(matches!(
7070 dispatch(&h).await,
7071 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7072 ));
7073 }
7074
7075 #[tokio::test]
7078 async fn approve_all_is_order_independent() {
7079 let a = SessionConfig::default()
7080 .with_permission_handler(Arc::new(ApproveAllHandler))
7081 .approve_all_permissions();
7082 let b = SessionConfig::default()
7083 .approve_all_permissions()
7084 .with_permission_handler(Arc::new(ApproveAllHandler));
7085 let ha = resolve_create(a).unwrap();
7086 let hb = resolve_create(b).unwrap();
7087 assert!(matches!(
7088 dispatch(&ha).await,
7089 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7090 ));
7091 assert!(matches!(
7092 dispatch(&hb).await,
7093 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7094 ));
7095 }
7096
7097 #[tokio::test]
7098 async fn deny_all_is_order_independent() {
7099 let a = SessionConfig::default()
7100 .with_permission_handler(Arc::new(ApproveAllHandler))
7101 .deny_all_permissions();
7102 let b = SessionConfig::default()
7103 .deny_all_permissions()
7104 .with_permission_handler(Arc::new(ApproveAllHandler));
7105 let ha = resolve_create(a).unwrap();
7106 let hb = resolve_create(b).unwrap();
7107 assert!(matches!(
7108 dispatch(&ha).await,
7109 PermissionResult::Decision(PermissionDecision::Reject(_))
7110 ));
7111 assert!(matches!(
7112 dispatch(&hb).await,
7113 PermissionResult::Decision(PermissionDecision::Reject(_))
7114 ));
7115 }
7116
7117 #[tokio::test]
7118 async fn approve_permissions_if_consults_predicate() {
7119 let cfg = SessionConfig::default().approve_permissions_if(|d| {
7120 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7121 });
7122 let h = resolve_create(cfg).unwrap();
7123 assert!(matches!(
7124 dispatch(&h).await,
7125 PermissionResult::Decision(PermissionDecision::Reject(_))
7126 ));
7127 }
7128
7129 #[tokio::test]
7130 async fn approve_permissions_if_is_order_independent() {
7131 let predicate = |d: &PermissionRequestData| {
7132 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7133 };
7134 let a = SessionConfig::default()
7135 .with_permission_handler(Arc::new(ApproveAllHandler))
7136 .approve_permissions_if(predicate);
7137 let b = SessionConfig::default()
7138 .approve_permissions_if(predicate)
7139 .with_permission_handler(Arc::new(ApproveAllHandler));
7140 let ha = resolve_create(a).unwrap();
7141 let hb = resolve_create(b).unwrap();
7142 assert!(matches!(
7143 dispatch(&ha).await,
7144 PermissionResult::Decision(PermissionDecision::Reject(_))
7145 ));
7146 assert!(matches!(
7147 dispatch(&hb).await,
7148 PermissionResult::Decision(PermissionDecision::Reject(_))
7149 ));
7150 }
7151
7152 #[tokio::test]
7153 async fn resume_session_config_approve_all_works() {
7154 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
7155 .with_permission_handler(Arc::new(ApproveAllHandler))
7156 .approve_all_permissions();
7157 let h = resolve_resume(cfg).unwrap();
7158 assert!(matches!(
7159 dispatch(&h).await,
7160 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7161 ));
7162 }
7163
7164 #[tokio::test]
7165 async fn resume_session_config_approve_all_is_order_independent() {
7166 let a = ResumeSessionConfig::new(SessionId::from("s1"))
7167 .with_permission_handler(Arc::new(ApproveAllHandler))
7168 .approve_all_permissions();
7169 let b = ResumeSessionConfig::new(SessionId::from("s1"))
7170 .approve_all_permissions()
7171 .with_permission_handler(Arc::new(ApproveAllHandler));
7172 let ha = resolve_resume(a).unwrap();
7173 let hb = resolve_resume(b).unwrap();
7174 assert!(matches!(
7175 dispatch(&ha).await,
7176 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7177 ));
7178 assert!(matches!(
7179 dispatch(&hb).await,
7180 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7181 ));
7182 }
7183}