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, SessionFsSqliteTransactionError,
37 SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionStatement,
38};
39pub use crate::trace_context::{TraceContext, TraceContextProvider};
40use crate::transforms::SystemMessageTransform;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45#[allow(dead_code)]
46#[non_exhaustive]
47pub(crate) enum ConnectionState {
48 Disconnected,
50 Connecting,
52 Connected,
54 Error,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
63#[non_exhaustive]
64pub enum SessionLifecycleEventType {
65 #[serde(rename = "session.created")]
67 Created,
68 #[serde(rename = "session.deleted")]
70 Deleted,
71 #[serde(rename = "session.updated")]
73 Updated,
74 #[serde(rename = "session.foreground")]
76 Foreground,
77 #[serde(rename = "session.background")]
79 Background,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct SessionLifecycleEventMetadata {
85 #[serde(rename = "startTime")]
87 pub start_time: String,
88 #[serde(rename = "modifiedTime")]
90 pub modified_time: String,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub summary: Option<String>,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct SessionLifecycleEvent {
100 #[serde(rename = "type")]
102 pub event_type: SessionLifecycleEventType,
103 #[serde(rename = "sessionId")]
105 pub session_id: SessionId,
106 #[serde(skip_serializing_if = "Option::is_none")]
108 pub metadata: Option<SessionLifecycleEventMetadata>,
109}
110
111#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
117#[serde(transparent)]
118pub struct SessionId(String);
119
120impl SessionId {
121 pub fn new(id: impl Into<String>) -> Self {
123 Self(id.into())
124 }
125
126 pub fn as_str(&self) -> &str {
128 &self.0
129 }
130
131 pub fn into_inner(self) -> String {
133 self.0
134 }
135}
136
137impl std::ops::Deref for SessionId {
138 type Target = str;
139
140 fn deref(&self) -> &str {
141 &self.0
142 }
143}
144
145impl std::fmt::Display for SessionId {
146 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 f.write_str(&self.0)
148 }
149}
150
151impl From<String> for SessionId {
152 fn from(s: String) -> Self {
153 Self(s)
154 }
155}
156
157impl From<&str> for SessionId {
158 fn from(s: &str) -> Self {
159 Self(s.to_owned())
160 }
161}
162
163impl AsRef<str> for SessionId {
164 fn as_ref(&self) -> &str {
165 &self.0
166 }
167}
168
169impl std::borrow::Borrow<str> for SessionId {
170 fn borrow(&self) -> &str {
171 &self.0
172 }
173}
174
175impl From<SessionId> for String {
176 fn from(id: SessionId) -> String {
177 id.0
178 }
179}
180
181impl PartialEq<str> for SessionId {
182 fn eq(&self, other: &str) -> bool {
183 self.0 == other
184 }
185}
186
187impl PartialEq<String> for SessionId {
188 fn eq(&self, other: &String) -> bool {
189 &self.0 == other
190 }
191}
192
193impl PartialEq<SessionId> for String {
194 fn eq(&self, other: &SessionId) -> bool {
195 self == &other.0
196 }
197}
198
199impl PartialEq<&str> for SessionId {
200 fn eq(&self, other: &&str) -> bool {
201 self.0 == *other
202 }
203}
204
205impl PartialEq<&SessionId> for SessionId {
206 fn eq(&self, other: &&SessionId) -> bool {
207 self.0 == other.0
208 }
209}
210
211impl PartialEq<SessionId> for &SessionId {
212 fn eq(&self, other: &SessionId) -> bool {
213 self.0 == other.0
214 }
215}
216
217#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
223#[serde(transparent)]
224pub struct RequestId(String);
225
226impl RequestId {
227 pub fn new(id: impl Into<String>) -> Self {
229 Self(id.into())
230 }
231
232 pub fn into_inner(self) -> String {
234 self.0
235 }
236}
237
238impl std::ops::Deref for RequestId {
239 type Target = str;
240
241 fn deref(&self) -> &str {
242 &self.0
243 }
244}
245
246impl std::fmt::Display for RequestId {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 f.write_str(&self.0)
249 }
250}
251
252impl From<String> for RequestId {
253 fn from(s: String) -> Self {
254 Self(s)
255 }
256}
257
258impl From<&str> for RequestId {
259 fn from(s: &str) -> Self {
260 Self(s.to_owned())
261 }
262}
263
264impl AsRef<str> for RequestId {
265 fn as_ref(&self) -> &str {
266 &self.0
267 }
268}
269
270impl std::borrow::Borrow<str> for RequestId {
271 fn borrow(&self) -> &str {
272 &self.0
273 }
274}
275
276impl From<RequestId> for String {
277 fn from(id: RequestId) -> String {
278 id.0
279 }
280}
281
282impl PartialEq<str> for RequestId {
283 fn eq(&self, other: &str) -> bool {
284 self.0 == other
285 }
286}
287
288impl PartialEq<String> for RequestId {
289 fn eq(&self, other: &String) -> bool {
290 &self.0 == other
291 }
292}
293
294impl PartialEq<RequestId> for String {
295 fn eq(&self, other: &RequestId) -> bool {
296 self == &other.0
297 }
298}
299
300impl PartialEq<&str> for RequestId {
301 fn eq(&self, other: &&str) -> bool {
302 self.0 == *other
303 }
304}
305
306#[derive(Clone, Default, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322#[non_exhaustive]
323pub struct Tool {
324 pub name: String,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub namespaced_name: Option<String>,
330 #[serde(default)]
332 pub description: String,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub instructions: Option<String>,
336 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
338 pub parameters: IndexMap<String, Value>,
339 #[serde(default, skip_serializing_if = "is_false")]
343 pub overrides_built_in_tool: bool,
344 #[serde(default, skip_serializing_if = "is_false")]
348 pub skip_permission: bool,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pub defer: Option<DeferMode>,
356 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
361 pub metadata: IndexMap<String, Value>,
362 #[serde(skip)]
374 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
375}
376
377#[inline]
378fn is_false(b: &bool) -> bool {
379 !*b
380}
381
382#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
385#[serde(rename_all = "lowercase")]
386pub enum DeferMode {
387 Auto,
389 Never,
391}
392
393impl Tool {
394 pub fn new(name: impl Into<String>) -> Self {
414 Self {
415 name: name.into(),
416 ..Default::default()
417 }
418 }
419
420 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
423 self.namespaced_name = Some(namespaced_name.into());
424 self
425 }
426
427 pub fn with_description(mut self, description: impl Into<String>) -> Self {
429 self.description = description.into();
430 self
431 }
432
433 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
435 self.instructions = Some(instructions.into());
436 self
437 }
438
439 pub fn with_parameters(mut self, parameters: Value) -> Self {
453 self.parameters = crate::tool::tool_parameters(parameters);
454 self
455 }
456
457 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
461 self.overrides_built_in_tool = overrides;
462 self
463 }
464
465 pub fn with_skip_permission(mut self, skip: bool) -> Self {
469 self.skip_permission = skip;
470 self
471 }
472
473 pub fn with_defer(mut self, defer: DeferMode) -> Self {
477 self.defer = Some(defer);
478 self
479 }
480
481 pub fn with_metadata(mut self, metadata: IndexMap<String, Value>) -> Self {
484 self.metadata = metadata;
485 self
486 }
487
488 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
492 self.handler = Some(handler);
493 self
494 }
495
496 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
501 self.handler.as_ref()
502 }
503}
504
505impl std::fmt::Debug for Tool {
506 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
507 f.debug_struct("Tool")
508 .field("name", &self.name)
509 .field("namespaced_name", &self.namespaced_name)
510 .field("description", &self.description)
511 .field("instructions", &self.instructions)
512 .field("parameters", &self.parameters)
513 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
514 .field("skip_permission", &self.skip_permission)
515 .field("defer", &self.defer)
516 .field("metadata", &self.metadata)
517 .field(
518 "handler",
519 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
520 )
521 .finish()
522 }
523}
524
525#[non_exhaustive]
528#[derive(Debug, Clone)]
529pub struct CommandContext {
530 pub session_id: SessionId,
532 pub command: String,
534 pub command_name: String,
536 pub args: String,
538}
539
540#[async_trait::async_trait]
546pub trait CommandHandler: Send + Sync {
547 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
549}
550
551#[non_exhaustive]
557#[derive(Clone)]
558pub struct CommandDefinition {
559 pub name: String,
561 pub description: Option<String>,
563 pub handler: Arc<dyn CommandHandler>,
565}
566
567impl CommandDefinition {
568 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
571 Self {
572 name: name.into(),
573 description: None,
574 handler,
575 }
576 }
577
578 pub fn with_description(mut self, description: impl Into<String>) -> Self {
580 self.description = Some(description.into());
581 self
582 }
583}
584
585impl std::fmt::Debug for CommandDefinition {
586 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
587 f.debug_struct("CommandDefinition")
588 .field("name", &self.name)
589 .field("description", &self.description)
590 .field("handler", &"<set>")
591 .finish()
592 }
593}
594
595impl Serialize for CommandDefinition {
596 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
597 use serde::ser::SerializeStruct;
598 let len = if self.description.is_some() { 2 } else { 1 };
599 let mut state = serializer.serialize_struct("CommandDefinition", len)?;
600 state.serialize_field("name", &self.name)?;
601 if let Some(description) = &self.description {
602 state.serialize_field("description", description)?;
603 }
604 state.end()
605 }
606}
607
608#[derive(Debug, Clone, Default, Serialize, Deserialize)]
615#[serde(rename_all = "camelCase")]
616#[non_exhaustive]
617pub struct CustomAgentConfig {
618 pub name: String,
620 #[serde(default, skip_serializing_if = "Option::is_none")]
622 pub display_name: Option<String>,
623 #[serde(default, skip_serializing_if = "Option::is_none")]
625 pub description: Option<String>,
626 #[serde(default, skip_serializing_if = "Option::is_none")]
628 pub tools: Option<Vec<String>>,
629 pub prompt: String,
631 #[serde(default, skip_serializing_if = "Option::is_none")]
633 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
634 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub infer: Option<bool>,
637 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub skills: Option<Vec<String>>,
640 #[serde(default, skip_serializing_if = "Option::is_none")]
645 pub model: Option<String>,
646 #[serde(default, skip_serializing_if = "Option::is_none")]
651 pub reasoning_effort: Option<String>,
652}
653
654impl CustomAgentConfig {
655 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
662 Self {
663 name: name.into(),
664 prompt: prompt.into(),
665 ..Self::default()
666 }
667 }
668
669 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
671 self.display_name = Some(display_name.into());
672 self
673 }
674
675 pub fn with_description(mut self, description: impl Into<String>) -> Self {
677 self.description = Some(description.into());
678 self
679 }
680
681 pub fn with_tools<I, S>(mut self, tools: I) -> Self
684 where
685 I: IntoIterator<Item = S>,
686 S: Into<String>,
687 {
688 self.tools = Some(tools.into_iter().map(Into::into).collect());
689 self
690 }
691
692 pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
694 self.mcp_servers = Some(mcp_servers);
695 self
696 }
697
698 pub fn with_infer(mut self, infer: bool) -> Self {
700 self.infer = Some(infer);
701 self
702 }
703
704 pub fn with_skills<I, S>(mut self, skills: I) -> Self
706 where
707 I: IntoIterator<Item = S>,
708 S: Into<String>,
709 {
710 self.skills = Some(skills.into_iter().map(Into::into).collect());
711 self
712 }
713
714 pub fn with_model(mut self, model: impl Into<String>) -> Self {
716 self.model = Some(model.into());
717 self
718 }
719
720 pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into<String>) -> Self {
722 self.reasoning_effort = Some(reasoning_effort.into());
723 self
724 }
725}
726
727#[derive(Debug, Clone, Default, Serialize, Deserialize)]
734#[serde(rename_all = "camelCase")]
735pub struct DefaultAgentConfig {
736 #[serde(default, skip_serializing_if = "Option::is_none")]
738 pub excluded_tools: Option<Vec<String>>,
739}
740
741#[derive(Debug, Clone, Default, Serialize, Deserialize)]
747#[serde(rename_all = "camelCase")]
748#[non_exhaustive]
749pub struct LargeToolOutputConfig {
750 #[serde(default, skip_serializing_if = "Option::is_none")]
752 pub enabled: Option<bool>,
753 #[serde(default, skip_serializing_if = "Option::is_none")]
756 pub max_size_bytes: Option<u64>,
757 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
760 pub output_directory: Option<PathBuf>,
761}
762
763impl LargeToolOutputConfig {
764 pub fn new() -> Self {
767 Self::default()
768 }
769
770 pub fn with_enabled(mut self, enabled: bool) -> Self {
772 self.enabled = Some(enabled);
773 self
774 }
775
776 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
778 self.max_size_bytes = Some(max_size_bytes);
779 self
780 }
781
782 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
784 self.output_directory = Some(output_directory.into());
785 self
786 }
787}
788
789#[derive(Debug, Clone, Default, Serialize, Deserialize)]
795#[serde(rename_all = "camelCase")]
796#[non_exhaustive]
797pub struct ToolSearchConfig {
798 #[serde(default, skip_serializing_if = "Option::is_none")]
800 pub enabled: Option<bool>,
801 #[serde(default, skip_serializing_if = "Option::is_none")]
804 pub defer_threshold: Option<u32>,
805}
806
807impl ToolSearchConfig {
808 pub fn new() -> Self {
811 Self::default()
812 }
813
814 pub fn with_enabled(mut self, enabled: bool) -> Self {
816 self.enabled = Some(enabled);
817 self
818 }
819
820 pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
823 self.defer_threshold = Some(defer_threshold);
824 self
825 }
826}
827
828#[derive(Debug, Clone, Default, Serialize, Deserialize)]
835#[serde(rename_all = "camelCase")]
836#[non_exhaustive]
837pub struct InfiniteSessionConfig {
838 #[serde(default, skip_serializing_if = "Option::is_none")]
840 pub enabled: Option<bool>,
841 #[serde(default, skip_serializing_if = "Option::is_none")]
844 pub background_compaction_threshold: Option<f64>,
845 #[serde(default, skip_serializing_if = "Option::is_none")]
848 pub buffer_exhaustion_threshold: Option<f64>,
849}
850
851impl InfiniteSessionConfig {
852 pub fn new() -> Self {
855 Self::default()
856 }
857
858 pub fn with_enabled(mut self, enabled: bool) -> Self {
861 self.enabled = Some(enabled);
862 self
863 }
864
865 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
868 self.background_compaction_threshold = Some(threshold);
869 self
870 }
871
872 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
875 self.buffer_exhaustion_threshold = Some(threshold);
876 self
877 }
878}
879
880#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
891#[serde(rename_all = "camelCase")]
892#[non_exhaustive]
893pub struct MemoryConfiguration {
894 pub enabled: bool,
896}
897
898impl MemoryConfiguration {
899 pub fn enabled() -> Self {
901 Self { enabled: true }
902 }
903
904 pub fn disabled() -> Self {
906 Self { enabled: false }
907 }
908
909 pub fn with_enabled(mut self, enabled: bool) -> Self {
911 self.enabled = enabled;
912 self
913 }
914}
915
916#[derive(Debug, Clone, Serialize, Deserialize)]
918#[serde(rename_all = "camelCase")]
919#[non_exhaustive]
920pub struct CloudSessionRepository {
921 pub owner: String,
923 pub name: String,
925 #[serde(skip_serializing_if = "Option::is_none")]
927 pub branch: Option<String>,
928}
929
930impl CloudSessionRepository {
931 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
933 Self {
934 owner: owner.into(),
935 name: name.into(),
936 branch: None,
937 }
938 }
939
940 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
942 self.branch = Some(branch.into());
943 self
944 }
945}
946
947#[derive(Debug, Clone, Default, Serialize, Deserialize)]
949#[serde(rename_all = "camelCase")]
950#[non_exhaustive]
951pub struct CloudSessionOptions {
952 #[serde(skip_serializing_if = "Option::is_none")]
954 pub repository: Option<CloudSessionRepository>,
955}
956
957impl CloudSessionOptions {
958 pub fn with_repository(repository: CloudSessionRepository) -> Self {
960 Self {
961 repository: Some(repository),
962 }
963 }
964}
965
966#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
968#[serde(rename_all = "camelCase")]
969pub struct ExtensionInfo {
970 pub source: String,
972 pub name: String,
974}
975
976impl ExtensionInfo {
977 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
979 Self {
980 source: source.into(),
981 name: name.into(),
982 }
983 }
984}
985
986#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
997#[serde(rename_all = "camelCase")]
998pub struct CanvasProviderIdentity {
999 pub id: String,
1001 #[serde(skip_serializing_if = "Option::is_none")]
1003 pub name: Option<String>,
1004}
1005
1006impl CanvasProviderIdentity {
1007 pub fn new(id: impl Into<String>) -> Self {
1009 Self {
1010 id: id.into(),
1011 name: None,
1012 }
1013 }
1014
1015 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1017 self.name = Some(name.into());
1018 self
1019 }
1020}
1021
1022#[derive(Debug, Clone, Serialize, Deserialize)]
1056#[serde(tag = "type", rename_all = "lowercase")]
1057#[non_exhaustive]
1058pub enum McpServerConfig {
1059 #[serde(alias = "local")]
1063 Stdio(McpStdioServerConfig),
1064 Http(McpHttpServerConfig),
1066 Sse(McpHttpServerConfig),
1068}
1069
1070#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1074#[serde(rename_all = "camelCase")]
1075pub struct McpStdioServerConfig {
1076 #[serde(default, skip_serializing_if = "Option::is_none")]
1082 pub tools: Option<Vec<String>>,
1083 #[serde(default, skip_serializing_if = "Option::is_none")]
1085 pub timeout: Option<i64>,
1086 pub command: String,
1088 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1090 pub args: Vec<String>,
1091 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1094 pub env: HashMap<String, String>,
1095 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1097 pub working_directory: Option<String>,
1098}
1099
1100#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1104#[serde(rename_all = "camelCase")]
1105pub struct McpHttpServerConfig {
1106 #[serde(default, skip_serializing_if = "Option::is_none")]
1112 pub tools: Option<Vec<String>>,
1113 #[serde(default, skip_serializing_if = "Option::is_none")]
1115 pub timeout: Option<i64>,
1116 pub url: String,
1118 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1120 pub headers: HashMap<String, String>,
1121}
1122
1123#[derive(Clone, Default, Serialize, Deserialize)]
1129#[serde(rename_all = "camelCase")]
1130#[non_exhaustive]
1131pub struct ProviderConfig {
1132 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1135 pub provider_type: Option<String>,
1136 #[serde(default, skip_serializing_if = "Option::is_none")]
1139 pub wire_api: Option<String>,
1140 #[serde(default, skip_serializing_if = "Option::is_none")]
1145 pub transport: Option<String>,
1146 pub base_url: String,
1148 #[serde(default, skip_serializing_if = "Option::is_none")]
1150 pub api_key: Option<String>,
1151 #[serde(default, skip_serializing_if = "Option::is_none")]
1155 pub bearer_token: Option<String>,
1156 #[serde(skip)]
1159 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1160 #[serde(default, skip_serializing_if = "Option::is_none")]
1161 pub(crate) has_bearer_token_provider: Option<bool>,
1162 #[serde(default, skip_serializing_if = "Option::is_none")]
1164 pub azure: Option<AzureProviderOptions>,
1165 #[serde(default, skip_serializing_if = "Option::is_none")]
1167 pub headers: Option<HashMap<String, String>>,
1168 #[serde(default, skip_serializing_if = "Option::is_none")]
1172 pub model_id: Option<String>,
1173 #[serde(default, skip_serializing_if = "Option::is_none")]
1180 pub wire_model: Option<String>,
1181 #[serde(default, skip_serializing_if = "Option::is_none")]
1186 pub max_prompt_tokens: Option<i64>,
1187 #[serde(default, skip_serializing_if = "Option::is_none")]
1190 pub max_output_tokens: Option<i64>,
1191}
1192
1193impl std::fmt::Debug for ProviderConfig {
1194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1195 f.debug_struct("ProviderConfig")
1196 .field("provider_type", &self.provider_type)
1197 .field("wire_api", &self.wire_api)
1198 .field("transport", &self.transport)
1199 .field("base_url", &self.base_url)
1200 .field("api_key", &self.api_key)
1201 .field("bearer_token", &self.bearer_token)
1202 .field(
1203 "bearer_token_provider",
1204 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1205 )
1206 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1207 .field("azure", &self.azure)
1208 .field("headers", &self.headers)
1209 .field("model_id", &self.model_id)
1210 .field("wire_model", &self.wire_model)
1211 .field("max_prompt_tokens", &self.max_prompt_tokens)
1212 .field("max_output_tokens", &self.max_output_tokens)
1213 .finish()
1214 }
1215}
1216
1217impl ProviderConfig {
1218 pub fn new(base_url: impl Into<String>) -> Self {
1221 Self {
1222 base_url: base_url.into(),
1223 ..Self::default()
1224 }
1225 }
1226
1227 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1229 self.provider_type = Some(provider_type.into());
1230 self
1231 }
1232
1233 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1235 self.wire_api = Some(wire_api.into());
1236 self
1237 }
1238
1239 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1242 self.transport = Some(transport.into());
1243 self
1244 }
1245
1246 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1248 self.api_key = Some(api_key.into());
1249 self
1250 }
1251
1252 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1255 self.bearer_token = Some(bearer_token.into());
1256 self
1257 }
1258
1259 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1265 self.bearer_token_provider = Some(provider);
1266 self
1267 }
1268
1269 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1271 self.azure = Some(azure);
1272 self
1273 }
1274
1275 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1277 self.headers = Some(headers);
1278 self
1279 }
1280
1281 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1284 self.model_id = Some(model_id.into());
1285 self
1286 }
1287
1288 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1293 self.wire_model = Some(wire_model.into());
1294 self
1295 }
1296
1297 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1301 self.max_prompt_tokens = Some(max);
1302 self
1303 }
1304
1305 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1308 self.max_output_tokens = Some(max);
1309 self
1310 }
1311}
1312
1313#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1326#[serde(rename_all = "camelCase")]
1327#[non_exhaustive]
1328pub struct CapiSessionOptions {
1329 #[serde(default, skip_serializing_if = "Option::is_none")]
1335 pub enable_web_socket_responses: Option<bool>,
1336}
1337
1338impl CapiSessionOptions {
1339 pub fn new() -> Self {
1341 Self::default()
1342 }
1343
1344 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1346 self.enable_web_socket_responses = Some(enable);
1347 self
1348 }
1349}
1350
1351#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1353#[serde(rename_all = "camelCase")]
1354pub struct AzureProviderOptions {
1355 #[serde(default, skip_serializing_if = "Option::is_none")]
1357 pub api_version: Option<String>,
1358}
1359
1360#[derive(Clone, Default, Serialize, Deserialize)]
1371#[serde(rename_all = "camelCase")]
1372#[non_exhaustive]
1373pub struct NamedProviderConfig {
1374 pub name: String,
1377 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1380 pub provider_type: Option<String>,
1381 #[serde(default, skip_serializing_if = "Option::is_none")]
1384 pub wire_api: Option<String>,
1385 pub base_url: String,
1387 #[serde(default, skip_serializing_if = "Option::is_none")]
1389 pub api_key: Option<String>,
1390 #[serde(default, skip_serializing_if = "Option::is_none")]
1393 pub bearer_token: Option<String>,
1394 #[serde(skip)]
1397 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1398 #[serde(default, skip_serializing_if = "Option::is_none")]
1399 pub(crate) has_bearer_token_provider: Option<bool>,
1400 #[serde(default, skip_serializing_if = "Option::is_none")]
1402 pub azure: Option<AzureProviderOptions>,
1403 #[serde(default, skip_serializing_if = "Option::is_none")]
1405 pub headers: Option<HashMap<String, String>>,
1406}
1407
1408impl std::fmt::Debug for NamedProviderConfig {
1409 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1410 f.debug_struct("NamedProviderConfig")
1411 .field("name", &self.name)
1412 .field("provider_type", &self.provider_type)
1413 .field("wire_api", &self.wire_api)
1414 .field("base_url", &self.base_url)
1415 .field("api_key", &self.api_key)
1416 .field("bearer_token", &self.bearer_token)
1417 .field(
1418 "bearer_token_provider",
1419 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1420 )
1421 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1422 .field("azure", &self.azure)
1423 .field("headers", &self.headers)
1424 .finish()
1425 }
1426}
1427
1428impl NamedProviderConfig {
1429 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1432 Self {
1433 name: name.into(),
1434 base_url: base_url.into(),
1435 ..Self::default()
1436 }
1437 }
1438
1439 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1441 self.provider_type = Some(provider_type.into());
1442 self
1443 }
1444
1445 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1447 self.wire_api = Some(wire_api.into());
1448 self
1449 }
1450
1451 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1453 self.api_key = Some(api_key.into());
1454 self
1455 }
1456
1457 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1460 self.bearer_token = Some(bearer_token.into());
1461 self
1462 }
1463
1464 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1470 self.bearer_token_provider = Some(provider);
1471 self
1472 }
1473
1474 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1476 self.azure = Some(azure);
1477 self
1478 }
1479
1480 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1482 self.headers = Some(headers);
1483 self
1484 }
1485}
1486
1487fn prepare_bearer_token_providers(
1488 provider: &mut Option<ProviderConfig>,
1489 providers: &mut Option<Vec<NamedProviderConfig>>,
1490) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1491 let mut bearer_token_providers = HashMap::new();
1492
1493 if let Some(provider) = provider.as_mut()
1494 && let Some(token_provider) = provider.bearer_token_provider.take()
1495 {
1496 provider.has_bearer_token_provider = Some(true);
1497 bearer_token_providers.insert("default".to_string(), token_provider);
1498 }
1499
1500 if let Some(providers) = providers.as_mut() {
1501 for provider in providers {
1502 if let Some(token_provider) = provider.bearer_token_provider.take() {
1503 provider.has_bearer_token_provider = Some(true);
1504 bearer_token_providers.insert(provider.name.clone(), token_provider);
1505 }
1506 }
1507 }
1508
1509 bearer_token_providers
1510}
1511
1512#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1520#[serde(rename_all = "camelCase")]
1521#[non_exhaustive]
1522pub struct ProviderModelConfig {
1523 pub id: String,
1526 pub provider: String,
1528 #[serde(default, skip_serializing_if = "Option::is_none")]
1531 pub wire_model: Option<String>,
1532 #[serde(default, skip_serializing_if = "Option::is_none")]
1535 pub model_id: Option<String>,
1536 #[serde(default, skip_serializing_if = "Option::is_none")]
1538 pub name: Option<String>,
1539 #[serde(default, skip_serializing_if = "Option::is_none")]
1541 pub max_prompt_tokens: Option<i64>,
1542 #[serde(default, skip_serializing_if = "Option::is_none")]
1544 pub max_context_window_tokens: Option<i64>,
1545 #[serde(default, skip_serializing_if = "Option::is_none")]
1547 pub max_output_tokens: Option<i64>,
1548 #[serde(default, skip_serializing_if = "Option::is_none")]
1551 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1552}
1553
1554impl ProviderModelConfig {
1555 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1558 Self {
1559 id: id.into(),
1560 provider: provider.into(),
1561 ..Self::default()
1562 }
1563 }
1564
1565 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1567 self.wire_model = Some(wire_model.into());
1568 self
1569 }
1570
1571 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1574 self.model_id = Some(model_id.into());
1575 self
1576 }
1577
1578 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1580 self.name = Some(name.into());
1581 self
1582 }
1583
1584 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1586 self.max_prompt_tokens = Some(max);
1587 self
1588 }
1589
1590 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1592 self.max_context_window_tokens = Some(max);
1593 self
1594 }
1595
1596 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1598 self.max_output_tokens = Some(max);
1599 self
1600 }
1601
1602 pub fn with_capabilities(
1604 mut self,
1605 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1606 ) -> Self {
1607 self.capabilities = Some(capabilities);
1608 self
1609 }
1610}
1611
1612#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1616#[serde(untagged)]
1617pub enum ExpFlagValue {
1618 Bool(bool),
1620 Integer(i64),
1622 Float(f64),
1624 String(String),
1626 Null,
1628}
1629
1630#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1634#[serde(rename_all = "PascalCase")]
1635pub struct ExpConfigEntry {
1636 pub id: String,
1638 pub parameters: HashMap<String, ExpFlagValue>,
1640}
1641
1642#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1648#[serde(rename_all = "PascalCase")]
1649pub struct CopilotExpAssignmentResponse {
1650 #[serde(default)]
1652 pub features: Vec<String>,
1653 #[serde(default)]
1655 pub flights: HashMap<String, String>,
1656 #[serde(default)]
1658 pub configs: Vec<ExpConfigEntry>,
1659 #[serde(default, skip_serializing_if = "Option::is_none")]
1661 pub parameter_groups: Option<Value>,
1662 #[serde(default, skip_serializing_if = "Option::is_none")]
1664 pub flighting_version: Option<i64>,
1665 #[serde(default, skip_serializing_if = "Option::is_none")]
1667 pub impression_id: Option<String>,
1668 #[serde(default)]
1670 pub assignment_context: String,
1671}
1672
1673#[derive(Clone)]
1725#[non_exhaustive]
1726pub struct SessionConfig {
1727 pub session_id: Option<SessionId>,
1729 pub model: Option<String>,
1731 pub client_name: Option<String>,
1733 pub reasoning_effort: Option<String>,
1735 pub reasoning_summary: Option<ReasoningSummary>,
1739 pub context_tier: Option<String>,
1742 pub streaming: Option<bool>,
1744 pub system_message: Option<SystemMessageConfig>,
1746 pub tools: Option<Vec<Tool>>,
1748 pub canvases: Option<Vec<CanvasDeclaration>>,
1750 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1755 pub request_canvas_renderer: Option<bool>,
1757 pub request_extensions: Option<bool>,
1759 pub extension_sdk_path: Option<String>,
1763 pub extension_info: Option<ExtensionInfo>,
1765 pub canvas_provider: Option<CanvasProviderIdentity>,
1768 pub available_tools: Option<Vec<String>>,
1770 pub excluded_tools: Option<Vec<String>>,
1772 pub excluded_builtin_agents: Option<Vec<String>>,
1778 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
1780 pub mcp_oauth_token_storage: Option<String>,
1789 pub enable_config_discovery: Option<bool>,
1792 pub skip_embedding_retrieval: Option<bool>,
1794 pub embedding_cache_storage: Option<String>,
1797 pub organization_custom_instructions: Option<String>,
1799 pub enable_on_demand_instruction_discovery: Option<bool>,
1801 pub enable_file_hooks: Option<bool>,
1803 pub enable_host_git_operations: Option<bool>,
1805 pub enable_session_store: Option<bool>,
1807 pub enable_skills: Option<bool>,
1809 pub enable_mcp_apps: Option<bool>,
1836 pub skill_directories: Option<Vec<PathBuf>>,
1838 pub instruction_directories: Option<Vec<PathBuf>>,
1841 pub plugin_directories: Option<Vec<PathBuf>>,
1843 pub large_output: Option<LargeToolOutputConfig>,
1845 pub tool_search: Option<ToolSearchConfig>,
1849 pub disabled_skills: Option<Vec<String>>,
1852 pub hooks: Option<bool>,
1856 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1858 pub default_agent: Option<DefaultAgentConfig>,
1862 pub agent: Option<String>,
1865 pub infinite_sessions: Option<InfiniteSessionConfig>,
1868 pub provider: Option<ProviderConfig>,
1872 pub capi: Option<CapiSessionOptions>,
1878 pub providers: Option<Vec<NamedProviderConfig>>,
1885 pub models: Option<Vec<ProviderModelConfig>>,
1891 pub enable_session_telemetry: Option<bool>,
1899 pub enable_citations: Option<bool>,
1901 pub session_limits: Option<SessionLimitsConfig>,
1903 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1906 pub memory: Option<MemoryConfiguration>,
1908 pub config_directory: Option<PathBuf>,
1911 pub working_directory: Option<PathBuf>,
1914 pub github_token: Option<String>,
1920 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
1926 pub cloud: Option<CloudSessionOptions>,
1929 pub include_sub_agent_streaming_events: Option<bool>,
1933 pub commands: Option<Vec<CommandDefinition>>,
1937 #[doc(hidden)]
1944 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
1945 pub enable_managed_settings: Option<bool>,
1952 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1957 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
1961 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
1964 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
1967 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
1971 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
1974 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
1977 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1981 pub(crate) permission_policy: Option<crate::permission::Policy>,
1985 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
1990 pub skip_custom_instructions: Option<bool>,
1994 pub custom_agents_local_only: Option<bool>,
1998 pub coauthor_enabled: Option<bool>,
2002 pub manage_schedule_enabled: Option<bool>,
2006}
2007
2008impl std::fmt::Debug for SessionConfig {
2009 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2010 f.debug_struct("SessionConfig")
2011 .field("session_id", &self.session_id)
2012 .field("model", &self.model)
2013 .field("client_name", &self.client_name)
2014 .field("reasoning_effort", &self.reasoning_effort)
2015 .field("reasoning_summary", &self.reasoning_summary)
2016 .field("context_tier", &self.context_tier)
2017 .field("streaming", &self.streaming)
2018 .field("system_message", &self.system_message)
2019 .field("tools", &self.tools)
2020 .field("canvases", &self.canvases)
2021 .field(
2022 "canvas_handler",
2023 &self.canvas_handler.as_ref().map(|_| "<set>"),
2024 )
2025 .field("request_canvas_renderer", &self.request_canvas_renderer)
2026 .field("request_extensions", &self.request_extensions)
2027 .field("extension_sdk_path", &self.extension_sdk_path)
2028 .field("extension_info", &self.extension_info)
2029 .field("canvas_provider", &self.canvas_provider)
2030 .field("available_tools", &self.available_tools)
2031 .field("excluded_tools", &self.excluded_tools)
2032 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2033 .field("mcp_servers", &self.mcp_servers)
2034 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2035 .field("embedding_cache_storage", &self.embedding_cache_storage)
2036 .field("enable_config_discovery", &self.enable_config_discovery)
2037 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2038 .field(
2039 "organization_custom_instructions",
2040 &self
2041 .organization_custom_instructions
2042 .as_ref()
2043 .map(|_| "<redacted>"),
2044 )
2045 .field(
2046 "enable_on_demand_instruction_discovery",
2047 &self.enable_on_demand_instruction_discovery,
2048 )
2049 .field("enable_file_hooks", &self.enable_file_hooks)
2050 .field(
2051 "enable_host_git_operations",
2052 &self.enable_host_git_operations,
2053 )
2054 .field("enable_session_store", &self.enable_session_store)
2055 .field("enable_skills", &self.enable_skills)
2056 .field("enable_mcp_apps", &self.enable_mcp_apps)
2057 .field("skill_directories", &self.skill_directories)
2058 .field("instruction_directories", &self.instruction_directories)
2059 .field("plugin_directories", &self.plugin_directories)
2060 .field("large_output", &self.large_output)
2061 .field("tool_search", &self.tool_search)
2062 .field("disabled_skills", &self.disabled_skills)
2063 .field("hooks", &self.hooks)
2064 .field("custom_agents", &self.custom_agents)
2065 .field("default_agent", &self.default_agent)
2066 .field("agent", &self.agent)
2067 .field("infinite_sessions", &self.infinite_sessions)
2068 .field("provider", &self.provider)
2069 .field("capi", &self.capi)
2070 .field("enable_session_telemetry", &self.enable_session_telemetry)
2071 .field("enable_citations", &self.enable_citations)
2072 .field("session_limits", &self.session_limits)
2073 .field("model_capabilities", &self.model_capabilities)
2074 .field("memory", &self.memory)
2075 .field("config_directory", &self.config_directory)
2076 .field("working_directory", &self.working_directory)
2077 .field(
2078 "github_token",
2079 &self.github_token.as_ref().map(|_| "<redacted>"),
2080 )
2081 .field("remote_session", &self.remote_session)
2082 .field("cloud", &self.cloud)
2083 .field(
2084 "include_sub_agent_streaming_events",
2085 &self.include_sub_agent_streaming_events,
2086 )
2087 .field("commands", &self.commands)
2088 .field("exp_assignments", &self.exp_assignments)
2089 .field("enable_managed_settings", &self.enable_managed_settings)
2090 .field(
2091 "session_fs_provider",
2092 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2093 )
2094 .field(
2095 "permission_handler",
2096 &self.permission_handler.as_ref().map(|_| "<set>"),
2097 )
2098 .field(
2099 "elicitation_handler",
2100 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2101 )
2102 .field(
2103 "mcp_auth_handler",
2104 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2105 )
2106 .field(
2107 "user_input_handler",
2108 &self.user_input_handler.as_ref().map(|_| "<set>"),
2109 )
2110 .field(
2111 "exit_plan_mode_handler",
2112 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2113 )
2114 .field(
2115 "auto_mode_switch_handler",
2116 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2117 )
2118 .field(
2119 "hooks_handler",
2120 &self.hooks_handler.as_ref().map(|_| "<set>"),
2121 )
2122 .field(
2123 "system_message_transform",
2124 &self.system_message_transform.as_ref().map(|_| "<set>"),
2125 )
2126 .finish()
2127 }
2128}
2129
2130impl Default for SessionConfig {
2131 fn default() -> Self {
2137 Self {
2138 session_id: None,
2139 model: None,
2140 client_name: None,
2141 reasoning_effort: None,
2142 reasoning_summary: None,
2143 context_tier: None,
2144 streaming: None,
2145 system_message: None,
2146 tools: None,
2147 canvases: None,
2148 canvas_handler: None,
2149 request_canvas_renderer: None,
2150 request_extensions: None,
2151 extension_sdk_path: None,
2152 extension_info: None,
2153 canvas_provider: None,
2154 available_tools: None,
2155 excluded_tools: None,
2156 excluded_builtin_agents: None,
2157 mcp_servers: None,
2158 mcp_oauth_token_storage: None,
2159 enable_config_discovery: None,
2160 skip_embedding_retrieval: None,
2161 organization_custom_instructions: None,
2162 enable_on_demand_instruction_discovery: None,
2163 enable_file_hooks: None,
2164 enable_host_git_operations: None,
2165 enable_session_store: None,
2166 enable_skills: None,
2167 embedding_cache_storage: None,
2168 enable_mcp_apps: None,
2169 skill_directories: None,
2170 instruction_directories: None,
2171 plugin_directories: None,
2172 large_output: None,
2173 tool_search: None,
2174 disabled_skills: None,
2175 hooks: None,
2176 custom_agents: None,
2177 default_agent: None,
2178 agent: None,
2179 infinite_sessions: None,
2180 provider: None,
2181 capi: None,
2182 providers: None,
2183 models: None,
2184 enable_session_telemetry: None,
2185 enable_citations: None,
2186 session_limits: None,
2187 model_capabilities: None,
2188 memory: None,
2189 config_directory: None,
2190 working_directory: None,
2191 github_token: None,
2192 remote_session: None,
2193 cloud: None,
2194 include_sub_agent_streaming_events: None,
2195 commands: None,
2196 exp_assignments: None,
2197 enable_managed_settings: None,
2198 session_fs_provider: None,
2199 permission_handler: None,
2200 elicitation_handler: None,
2201 mcp_auth_handler: None,
2202 user_input_handler: None,
2203 exit_plan_mode_handler: None,
2204 auto_mode_switch_handler: None,
2205 hooks_handler: None,
2206 permission_policy: None,
2207 system_message_transform: None,
2208 skip_custom_instructions: None,
2209 custom_agents_local_only: None,
2210 coauthor_enabled: None,
2211 manage_schedule_enabled: None,
2212 }
2213 }
2214}
2215
2216pub(crate) struct SessionConfigRuntime {
2222 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2223 pub permission_policy: Option<crate::permission::Policy>,
2224 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2225 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2226 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2227 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2228 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2229 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2230 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2231 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2232 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2233 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2234 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2235 pub commands: Option<Vec<CommandDefinition>>,
2236}
2237
2238impl SessionConfig {
2239 pub(crate) fn into_wire(
2251 mut self,
2252 session_id: Option<SessionId>,
2253 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2254 let permission_active =
2255 self.permission_handler.is_some() || self.permission_policy.is_some();
2256 let request_user_input = self.user_input_handler.is_some();
2257 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2258 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2259 let request_elicitation = self.elicitation_handler.is_some();
2260 let hooks_flag = self.hooks_handler.is_some();
2261
2262 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2263 if let Some(tools) = self.tools.as_mut() {
2264 for tool in tools.iter_mut() {
2265 if let Some(handler) = tool.handler.take()
2266 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2267 {
2268 return Err(crate::Error::with_message(
2269 crate::ErrorKind::InvalidConfig,
2270 format!("duplicate tool handler registered for name {:?}", tool.name),
2271 ));
2272 }
2273 }
2274 }
2275
2276 let wire_commands = self.commands.as_ref().map(|cmds| {
2277 cmds.iter()
2278 .map(|c| crate::wire::CommandWireDefinition {
2279 name: c.name.clone(),
2280 description: c.description.clone(),
2281 })
2282 .collect()
2283 });
2284 let wire_canvases = self.canvases.clone();
2285 let canvas_handler = self.canvas_handler.clone();
2286 let bearer_token_providers =
2287 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2288
2289 let wire = crate::wire::SessionCreateWire {
2290 session_id,
2291 model: self.model,
2292 client_name: self.client_name,
2293 reasoning_effort: self.reasoning_effort,
2294 reasoning_summary: self.reasoning_summary,
2295 context_tier: self.context_tier,
2296 streaming: self.streaming,
2297 system_message: self.system_message,
2298 tools: self.tools,
2299 canvases: wire_canvases,
2300 request_canvas_renderer: self.request_canvas_renderer,
2301 request_extensions: self.request_extensions,
2302 extension_sdk_path: self.extension_sdk_path,
2303 extension_info: self.extension_info,
2304 canvas_provider: self.canvas_provider,
2305 available_tools: self.available_tools,
2306 excluded_tools: self.excluded_tools,
2307 excluded_builtin_agents: self.excluded_builtin_agents,
2308 tool_filter_precedence: "excluded",
2309 mcp_servers: self.mcp_servers,
2310 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2311 embedding_cache_storage: self.embedding_cache_storage,
2312 env_value_mode: "direct",
2313 enable_config_discovery: self.enable_config_discovery,
2314 skip_embedding_retrieval: self.skip_embedding_retrieval,
2315 organization_custom_instructions: self.organization_custom_instructions,
2316 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2317 enable_file_hooks: self.enable_file_hooks,
2318 enable_host_git_operations: self.enable_host_git_operations,
2319 enable_session_store: self.enable_session_store,
2320 enable_skills: self.enable_skills,
2321 request_user_input,
2322 request_permission: permission_active,
2323 request_exit_plan_mode,
2324 request_auto_mode_switch,
2325 request_elicitation,
2326 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2327 hooks: hooks_flag,
2328 skill_directories: self.skill_directories,
2329 instruction_directories: self.instruction_directories,
2330 plugin_directories: self.plugin_directories,
2331 large_output: self.large_output,
2332 tool_search: self.tool_search,
2333 disabled_skills: self.disabled_skills,
2334 custom_agents: self.custom_agents,
2335 default_agent: self.default_agent,
2336 agent: self.agent,
2337 infinite_sessions: self.infinite_sessions,
2338 provider: self.provider,
2339 capi: self.capi,
2340 providers: self.providers,
2341 models: self.models,
2342 enable_session_telemetry: self.enable_session_telemetry,
2343 enable_citations: self.enable_citations,
2344 session_limits: self.session_limits,
2345 model_capabilities: self.model_capabilities,
2346 memory: self.memory,
2347 config_dir: self.config_directory,
2348 working_directory: self.working_directory,
2349 github_token: self.github_token,
2350 remote_session: self.remote_session,
2351 cloud: self.cloud,
2352 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2353 enable_github_telemetry_forwarding: None,
2354 commands: wire_commands,
2355 exp_assignments: self.exp_assignments,
2356 enable_managed_settings: self.enable_managed_settings,
2357 };
2358
2359 let runtime = SessionConfigRuntime {
2360 permission_handler: self.permission_handler,
2361 permission_policy: self.permission_policy,
2362 elicitation_handler: self.elicitation_handler,
2363 mcp_auth_handler: self.mcp_auth_handler,
2364 user_input_handler: self.user_input_handler,
2365 exit_plan_mode_handler: self.exit_plan_mode_handler,
2366 auto_mode_switch_handler: self.auto_mode_switch_handler,
2367 hooks_handler: self.hooks_handler,
2368 system_message_transform: self.system_message_transform,
2369 tool_handlers,
2370 canvas_handler,
2371 session_fs_provider: self.session_fs_provider,
2372 bearer_token_providers,
2373 commands: self.commands,
2374 };
2375
2376 Ok((wire, runtime))
2377 }
2378
2379 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2383 self.permission_handler = Some(handler);
2384 self
2385 }
2386
2387 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2390 self.elicitation_handler = Some(handler);
2391 self
2392 }
2393
2394 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2396 self.mcp_auth_handler = Some(handler);
2397 self
2398 }
2399
2400 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2403 self.user_input_handler = Some(handler);
2404 self
2405 }
2406
2407 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2409 self.exit_plan_mode_handler = Some(handler);
2410 self
2411 }
2412
2413 pub fn with_auto_mode_switch_handler(
2415 mut self,
2416 handler: Arc<dyn AutoModeSwitchHandler>,
2417 ) -> Self {
2418 self.auto_mode_switch_handler = Some(handler);
2419 self
2420 }
2421
2422 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2427 self.commands = Some(commands);
2428 self
2429 }
2430
2431 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2435 self.session_fs_provider = Some(provider);
2436 self
2437 }
2438
2439 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2442 self.hooks_handler = Some(hooks);
2443 self
2444 }
2445
2446 pub fn with_system_message_transform(
2450 mut self,
2451 transform: Arc<dyn SystemMessageTransform>,
2452 ) -> Self {
2453 self.system_message_transform = Some(transform);
2454 self
2455 }
2456
2457 pub fn approve_all_permissions(mut self) -> Self {
2463 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2464 self
2465 }
2466
2467 pub fn deny_all_permissions(mut self) -> Self {
2470 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2471 self
2472 }
2473
2474 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2479 where
2480 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2481 {
2482 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2483 self
2484 }
2485
2486 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2488 self.session_id = Some(id.into());
2489 self
2490 }
2491
2492 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2494 self.model = Some(model.into());
2495 self
2496 }
2497
2498 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2500 self.client_name = Some(name.into());
2501 self
2502 }
2503
2504 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2506 self.reasoning_effort = Some(effort.into());
2507 self
2508 }
2509
2510 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2512 self.reasoning_summary = Some(summary);
2513 self
2514 }
2515
2516 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2518 self.context_tier = Some(tier.into());
2519 self
2520 }
2521
2522 pub fn with_streaming(mut self, streaming: bool) -> Self {
2524 self.streaming = Some(streaming);
2525 self
2526 }
2527
2528 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2530 self.system_message = Some(system_message);
2531 self
2532 }
2533
2534 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2536 self.tools = Some(tools.into_iter().collect());
2537 self
2538 }
2539
2540 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2545 self.canvases = Some(canvases.into_iter().collect());
2546 self
2547 }
2548
2549 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2551 self.canvas_handler = Some(handler);
2552 self
2553 }
2554
2555 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2557 self.request_canvas_renderer = Some(request);
2558 self
2559 }
2560
2561 pub fn with_request_extensions(mut self, request: bool) -> Self {
2563 self.request_extensions = Some(request);
2564 self
2565 }
2566
2567 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2571 self.extension_sdk_path = Some(path.into());
2572 self
2573 }
2574
2575 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2577 self.extension_info = Some(extension_info);
2578 self
2579 }
2580
2581 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2584 self.canvas_provider = Some(canvas_provider);
2585 self
2586 }
2587
2588 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2590 where
2591 I: IntoIterator<Item = S>,
2592 S: Into<String>,
2593 {
2594 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2595 self
2596 }
2597
2598 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2600 where
2601 I: IntoIterator<Item = S>,
2602 S: Into<String>,
2603 {
2604 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2605 self
2606 }
2607
2608 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2610 where
2611 I: IntoIterator<Item = S>,
2612 S: Into<String>,
2613 {
2614 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2615 self
2616 }
2617
2618 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2620 self.mcp_servers = Some(servers);
2621 self
2622 }
2623
2624 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2632 self.mcp_oauth_token_storage = Some(mode.into());
2633 self
2634 }
2635
2636 pub fn with_embedding_cache_storage(
2638 mut self,
2639 embedding_cache_storage: impl Into<String>,
2640 ) -> Self {
2641 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2642 self
2643 }
2644
2645 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2648 self.enable_config_discovery = Some(enable);
2649 self
2650 }
2651
2652 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2654 self.skip_embedding_retrieval = Some(value);
2655 self
2656 }
2657
2658 pub fn with_organization_custom_instructions(
2660 mut self,
2661 instructions: impl Into<String>,
2662 ) -> Self {
2663 self.organization_custom_instructions = Some(instructions.into());
2664 self
2665 }
2666
2667 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2669 self.enable_on_demand_instruction_discovery = Some(value);
2670 self
2671 }
2672
2673 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2675 self.enable_file_hooks = Some(value);
2676 self
2677 }
2678
2679 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2681 self.enable_host_git_operations = Some(value);
2682 self
2683 }
2684
2685 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2687 self.enable_session_store = Some(value);
2688 self
2689 }
2690
2691 pub fn with_enable_skills(mut self, value: bool) -> Self {
2693 self.enable_skills = Some(value);
2694 self
2695 }
2696
2697 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2703 self.enable_mcp_apps = Some(enable);
2704 self
2705 }
2706
2707 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2709 where
2710 I: IntoIterator<Item = P>,
2711 P: Into<PathBuf>,
2712 {
2713 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2714 self
2715 }
2716
2717 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2721 where
2722 I: IntoIterator<Item = P>,
2723 P: Into<PathBuf>,
2724 {
2725 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2726 self
2727 }
2728
2729 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2731 where
2732 I: IntoIterator<Item = P>,
2733 P: Into<PathBuf>,
2734 {
2735 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2736 self
2737 }
2738
2739 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2741 self.large_output = Some(config);
2742 self
2743 }
2744
2745 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
2748 self.tool_search = Some(config);
2749 self
2750 }
2751
2752 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2754 where
2755 I: IntoIterator<Item = S>,
2756 S: Into<String>,
2757 {
2758 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2759 self
2760 }
2761
2762 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2764 mut self,
2765 agents: I,
2766 ) -> Self {
2767 self.custom_agents = Some(agents.into_iter().collect());
2768 self
2769 }
2770
2771 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2773 self.default_agent = Some(agent);
2774 self
2775 }
2776
2777 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2780 self.agent = Some(name.into());
2781 self
2782 }
2783
2784 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2787 self.infinite_sessions = Some(config);
2788 self
2789 }
2790
2791 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2793 self.provider = Some(provider);
2794 self
2795 }
2796
2797 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
2799 self.capi = Some(capi);
2800 self
2801 }
2802
2803 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
2809 self.providers = Some(providers);
2810 self
2811 }
2812
2813 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
2819 self.models = Some(models);
2820 self
2821 }
2822
2823 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2827 self.enable_session_telemetry = Some(enable);
2828 self
2829 }
2830
2831 pub fn with_enable_citations(mut self, enable: bool) -> Self {
2833 self.enable_citations = Some(enable);
2834 self
2835 }
2836
2837 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
2839 self.session_limits = Some(limits);
2840 self
2841 }
2842
2843 pub fn with_model_capabilities(
2845 mut self,
2846 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2847 ) -> Self {
2848 self.model_capabilities = Some(capabilities);
2849 self
2850 }
2851
2852 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
2854 self.memory = Some(memory);
2855 self
2856 }
2857
2858 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2860 self.config_directory = Some(dir.into());
2861 self
2862 }
2863
2864 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2867 self.working_directory = Some(dir.into());
2868 self
2869 }
2870
2871 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2876 self.github_token = Some(token.into());
2877 self
2878 }
2879
2880 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2883 self.include_sub_agent_streaming_events = Some(include);
2884 self
2885 }
2886
2887 pub fn with_remote_session(
2889 mut self,
2890 mode: crate::generated::api_types::RemoteSessionMode,
2891 ) -> Self {
2892 self.remote_session = Some(mode);
2893 self
2894 }
2895
2896 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
2898 self.cloud = Some(cloud);
2899 self
2900 }
2901
2902 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
2904 self.skip_custom_instructions = Some(value);
2905 self
2906 }
2907
2908 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
2910 self.custom_agents_local_only = Some(value);
2911 self
2912 }
2913
2914 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
2916 self.coauthor_enabled = Some(value);
2917 self
2918 }
2919
2920 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
2922 self.manage_schedule_enabled = Some(value);
2923 self
2924 }
2925
2926 #[doc(hidden)]
2934 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
2935 self.exp_assignments = Some(assignments);
2936 self
2937 }
2938
2939 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
2945 self.enable_managed_settings = Some(enabled);
2946 self
2947 }
2948}
2949#[derive(Clone)]
2956#[non_exhaustive]
2957pub struct ResumeSessionConfig {
2958 pub session_id: SessionId,
2960 pub model: Option<String>,
2963 pub client_name: Option<String>,
2965 pub reasoning_effort: Option<String>,
2967 pub reasoning_summary: Option<ReasoningSummary>,
2971 pub context_tier: Option<String>,
2974 pub streaming: Option<bool>,
2976 pub system_message: Option<SystemMessageConfig>,
2979 pub tools: Option<Vec<Tool>>,
2981 pub canvases: Option<Vec<CanvasDeclaration>>,
2983 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2986 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
2988 pub request_canvas_renderer: Option<bool>,
2990 pub request_extensions: Option<bool>,
2992 pub extension_sdk_path: Option<String>,
2996 pub extension_info: Option<ExtensionInfo>,
2998 pub canvas_provider: Option<CanvasProviderIdentity>,
3001 pub available_tools: Option<Vec<String>>,
3003 pub excluded_tools: Option<Vec<String>>,
3005 pub excluded_builtin_agents: Option<Vec<String>>,
3011 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3013 pub mcp_oauth_token_storage: Option<String>,
3016 pub enable_config_discovery: Option<bool>,
3019 pub skip_embedding_retrieval: Option<bool>,
3021 pub embedding_cache_storage: Option<String>,
3023 pub organization_custom_instructions: Option<String>,
3025 pub enable_on_demand_instruction_discovery: Option<bool>,
3027 pub enable_file_hooks: Option<bool>,
3029 pub enable_host_git_operations: Option<bool>,
3031 pub enable_session_store: Option<bool>,
3033 pub enable_skills: Option<bool>,
3035 pub enable_mcp_apps: Option<bool>,
3041 pub skill_directories: Option<Vec<PathBuf>>,
3043 pub instruction_directories: Option<Vec<PathBuf>>,
3046 pub plugin_directories: Option<Vec<PathBuf>>,
3048 pub large_output: Option<LargeToolOutputConfig>,
3050 pub tool_search: Option<ToolSearchConfig>,
3053 pub disabled_skills: Option<Vec<String>>,
3055 pub hooks: Option<bool>,
3057 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3059 pub default_agent: Option<DefaultAgentConfig>,
3061 pub agent: Option<String>,
3063 pub infinite_sessions: Option<InfiniteSessionConfig>,
3065 pub provider: Option<ProviderConfig>,
3067 pub capi: Option<CapiSessionOptions>,
3073 pub providers: Option<Vec<NamedProviderConfig>>,
3079 pub models: Option<Vec<ProviderModelConfig>>,
3085 pub enable_session_telemetry: Option<bool>,
3093 pub enable_citations: Option<bool>,
3095 pub session_limits: Option<SessionLimitsConfig>,
3097 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3099 pub memory: Option<MemoryConfiguration>,
3101 pub config_directory: Option<PathBuf>,
3103 pub working_directory: Option<PathBuf>,
3105 pub github_token: Option<String>,
3108 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3111 pub include_sub_agent_streaming_events: Option<bool>,
3113 pub commands: Option<Vec<CommandDefinition>>,
3117 #[doc(hidden)]
3122 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3123 pub enable_managed_settings: Option<bool>,
3129 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3134 pub suppress_resume_event: Option<bool>,
3137 pub continue_pending_work: Option<bool>,
3145 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3148 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3151 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3153 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3156 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3159 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3162 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3164 pub(crate) permission_policy: Option<crate::permission::Policy>,
3166 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3168 pub skip_custom_instructions: Option<bool>,
3170 pub custom_agents_local_only: Option<bool>,
3172 pub coauthor_enabled: Option<bool>,
3174 pub manage_schedule_enabled: Option<bool>,
3176}
3177
3178impl std::fmt::Debug for ResumeSessionConfig {
3179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3180 f.debug_struct("ResumeSessionConfig")
3181 .field("session_id", &self.session_id)
3182 .field("model", &self.model)
3183 .field("client_name", &self.client_name)
3184 .field("reasoning_effort", &self.reasoning_effort)
3185 .field("reasoning_summary", &self.reasoning_summary)
3186 .field("context_tier", &self.context_tier)
3187 .field("streaming", &self.streaming)
3188 .field("system_message", &self.system_message)
3189 .field("tools", &self.tools)
3190 .field("canvases", &self.canvases)
3191 .field(
3192 "canvas_handler",
3193 &self.canvas_handler.as_ref().map(|_| "<set>"),
3194 )
3195 .field("open_canvases", &self.open_canvases)
3196 .field("request_canvas_renderer", &self.request_canvas_renderer)
3197 .field("request_extensions", &self.request_extensions)
3198 .field("extension_sdk_path", &self.extension_sdk_path)
3199 .field("extension_info", &self.extension_info)
3200 .field("canvas_provider", &self.canvas_provider)
3201 .field("available_tools", &self.available_tools)
3202 .field("excluded_tools", &self.excluded_tools)
3203 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3204 .field("mcp_servers", &self.mcp_servers)
3205 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3206 .field("embedding_cache_storage", &self.embedding_cache_storage)
3207 .field("enable_config_discovery", &self.enable_config_discovery)
3208 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3209 .field(
3210 "organization_custom_instructions",
3211 &self
3212 .organization_custom_instructions
3213 .as_ref()
3214 .map(|_| "<redacted>"),
3215 )
3216 .field(
3217 "enable_on_demand_instruction_discovery",
3218 &self.enable_on_demand_instruction_discovery,
3219 )
3220 .field("enable_file_hooks", &self.enable_file_hooks)
3221 .field(
3222 "enable_host_git_operations",
3223 &self.enable_host_git_operations,
3224 )
3225 .field("enable_session_store", &self.enable_session_store)
3226 .field("enable_skills", &self.enable_skills)
3227 .field("enable_mcp_apps", &self.enable_mcp_apps)
3228 .field("skill_directories", &self.skill_directories)
3229 .field("instruction_directories", &self.instruction_directories)
3230 .field("plugin_directories", &self.plugin_directories)
3231 .field("large_output", &self.large_output)
3232 .field("tool_search", &self.tool_search)
3233 .field("disabled_skills", &self.disabled_skills)
3234 .field("hooks", &self.hooks)
3235 .field("custom_agents", &self.custom_agents)
3236 .field("default_agent", &self.default_agent)
3237 .field("agent", &self.agent)
3238 .field("infinite_sessions", &self.infinite_sessions)
3239 .field("provider", &self.provider)
3240 .field("capi", &self.capi)
3241 .field("enable_session_telemetry", &self.enable_session_telemetry)
3242 .field("enable_citations", &self.enable_citations)
3243 .field("session_limits", &self.session_limits)
3244 .field("model_capabilities", &self.model_capabilities)
3245 .field("memory", &self.memory)
3246 .field("config_directory", &self.config_directory)
3247 .field("working_directory", &self.working_directory)
3248 .field(
3249 "github_token",
3250 &self.github_token.as_ref().map(|_| "<redacted>"),
3251 )
3252 .field("remote_session", &self.remote_session)
3253 .field(
3254 "include_sub_agent_streaming_events",
3255 &self.include_sub_agent_streaming_events,
3256 )
3257 .field("commands", &self.commands)
3258 .field("exp_assignments", &self.exp_assignments)
3259 .field("enable_managed_settings", &self.enable_managed_settings)
3260 .field(
3261 "session_fs_provider",
3262 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3263 )
3264 .field(
3265 "permission_handler",
3266 &self.permission_handler.as_ref().map(|_| "<set>"),
3267 )
3268 .field(
3269 "elicitation_handler",
3270 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3271 )
3272 .field(
3273 "user_input_handler",
3274 &self.user_input_handler.as_ref().map(|_| "<set>"),
3275 )
3276 .field(
3277 "exit_plan_mode_handler",
3278 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3279 )
3280 .field(
3281 "auto_mode_switch_handler",
3282 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3283 )
3284 .field(
3285 "hooks_handler",
3286 &self.hooks_handler.as_ref().map(|_| "<set>"),
3287 )
3288 .field(
3289 "system_message_transform",
3290 &self.system_message_transform.as_ref().map(|_| "<set>"),
3291 )
3292 .field("suppress_resume_event", &self.suppress_resume_event)
3293 .field("continue_pending_work", &self.continue_pending_work)
3294 .finish()
3295 }
3296}
3297
3298impl ResumeSessionConfig {
3299 pub(crate) fn into_wire(
3307 mut self,
3308 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3309 let permission_active =
3310 self.permission_handler.is_some() || self.permission_policy.is_some();
3311 let request_user_input = self.user_input_handler.is_some();
3312 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3313 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3314 let request_elicitation = self.elicitation_handler.is_some();
3315 let hooks_flag = self.hooks_handler.is_some();
3316
3317 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3318 if let Some(tools) = self.tools.as_mut() {
3319 for tool in tools.iter_mut() {
3320 if let Some(handler) = tool.handler.take()
3321 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3322 {
3323 return Err(crate::Error::with_message(
3324 crate::ErrorKind::InvalidConfig,
3325 format!("duplicate tool handler registered for name {:?}", tool.name),
3326 ));
3327 }
3328 }
3329 }
3330
3331 let wire_commands = self.commands.as_ref().map(|cmds| {
3332 cmds.iter()
3333 .map(|c| crate::wire::CommandWireDefinition {
3334 name: c.name.clone(),
3335 description: c.description.clone(),
3336 })
3337 .collect()
3338 });
3339 let wire_canvases = self.canvases.clone();
3340 let canvas_handler = self.canvas_handler.clone();
3341 let bearer_token_providers =
3342 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3343
3344 let wire = crate::wire::SessionResumeWire {
3345 session_id: self.session_id,
3346 model: self.model,
3347 client_name: self.client_name,
3348 reasoning_effort: self.reasoning_effort,
3349 reasoning_summary: self.reasoning_summary,
3350 context_tier: self.context_tier,
3351 streaming: self.streaming,
3352 system_message: self.system_message,
3353 tools: self.tools,
3354 canvases: wire_canvases,
3355 open_canvases: self.open_canvases,
3356 request_canvas_renderer: self.request_canvas_renderer,
3357 request_extensions: self.request_extensions,
3358 extension_sdk_path: self.extension_sdk_path,
3359 extension_info: self.extension_info,
3360 canvas_provider: self.canvas_provider,
3361 available_tools: self.available_tools,
3362 excluded_tools: self.excluded_tools,
3363 excluded_builtin_agents: self.excluded_builtin_agents,
3364 tool_filter_precedence: "excluded",
3365 mcp_servers: self.mcp_servers,
3366 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3367 embedding_cache_storage: self.embedding_cache_storage,
3368 env_value_mode: "direct",
3369 enable_config_discovery: self.enable_config_discovery,
3370 skip_embedding_retrieval: self.skip_embedding_retrieval,
3371 organization_custom_instructions: self.organization_custom_instructions,
3372 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3373 enable_file_hooks: self.enable_file_hooks,
3374 enable_host_git_operations: self.enable_host_git_operations,
3375 enable_session_store: self.enable_session_store,
3376 enable_skills: self.enable_skills,
3377 request_user_input,
3378 request_permission: permission_active,
3379 request_exit_plan_mode,
3380 request_auto_mode_switch,
3381 request_elicitation,
3382 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3383 hooks: hooks_flag,
3384 skill_directories: self.skill_directories,
3385 instruction_directories: self.instruction_directories,
3386 plugin_directories: self.plugin_directories,
3387 large_output: self.large_output,
3388 tool_search: self.tool_search,
3389 disabled_skills: self.disabled_skills,
3390 custom_agents: self.custom_agents,
3391 default_agent: self.default_agent,
3392 agent: self.agent,
3393 infinite_sessions: self.infinite_sessions,
3394 provider: self.provider,
3395 capi: self.capi,
3396 providers: self.providers,
3397 models: self.models,
3398 enable_session_telemetry: self.enable_session_telemetry,
3399 enable_citations: self.enable_citations,
3400 session_limits: self.session_limits,
3401 model_capabilities: self.model_capabilities,
3402 memory: self.memory,
3403 config_dir: self.config_directory,
3404 working_directory: self.working_directory,
3405 github_token: self.github_token,
3406 remote_session: self.remote_session,
3407 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3408 enable_github_telemetry_forwarding: None,
3409 commands: wire_commands,
3410 exp_assignments: self.exp_assignments,
3411 enable_managed_settings: self.enable_managed_settings,
3412 suppress_resume_event: self.suppress_resume_event,
3413 continue_pending_work: self.continue_pending_work,
3414 };
3415
3416 let runtime = SessionConfigRuntime {
3417 permission_handler: self.permission_handler,
3418 permission_policy: self.permission_policy,
3419 elicitation_handler: self.elicitation_handler,
3420 mcp_auth_handler: self.mcp_auth_handler,
3421 user_input_handler: self.user_input_handler,
3422 exit_plan_mode_handler: self.exit_plan_mode_handler,
3423 auto_mode_switch_handler: self.auto_mode_switch_handler,
3424 hooks_handler: self.hooks_handler,
3425 system_message_transform: self.system_message_transform,
3426 tool_handlers,
3427 canvas_handler,
3428 session_fs_provider: self.session_fs_provider,
3429 bearer_token_providers,
3430 commands: self.commands,
3431 };
3432
3433 Ok((wire, runtime))
3434 }
3435
3436 pub fn new(session_id: SessionId) -> Self {
3441 Self {
3442 session_id,
3443 model: None,
3444 client_name: None,
3445 reasoning_effort: None,
3446 reasoning_summary: None,
3447 context_tier: None,
3448 streaming: None,
3449 system_message: None,
3450 tools: None,
3451 canvases: None,
3452 canvas_handler: None,
3453 open_canvases: None,
3454 request_canvas_renderer: None,
3455 request_extensions: None,
3456 extension_sdk_path: None,
3457 extension_info: None,
3458 canvas_provider: None,
3459 available_tools: None,
3460 excluded_tools: None,
3461 excluded_builtin_agents: None,
3462 mcp_servers: None,
3463 mcp_oauth_token_storage: None,
3464 enable_config_discovery: None,
3465 skip_embedding_retrieval: None,
3466 organization_custom_instructions: None,
3467 enable_on_demand_instruction_discovery: None,
3468 enable_file_hooks: None,
3469 enable_host_git_operations: None,
3470 enable_session_store: None,
3471 enable_skills: None,
3472 embedding_cache_storage: None,
3473 enable_mcp_apps: None,
3474 skill_directories: None,
3475 instruction_directories: None,
3476 plugin_directories: None,
3477 large_output: None,
3478 tool_search: None,
3479 disabled_skills: None,
3480 hooks: None,
3481 custom_agents: None,
3482 default_agent: None,
3483 agent: None,
3484 infinite_sessions: None,
3485 provider: None,
3486 capi: None,
3487 providers: None,
3488 models: None,
3489 enable_session_telemetry: None,
3490 enable_citations: None,
3491 session_limits: None,
3492 model_capabilities: None,
3493 memory: None,
3494 config_directory: None,
3495 working_directory: None,
3496 github_token: None,
3497 remote_session: None,
3498 include_sub_agent_streaming_events: None,
3499 commands: None,
3500 exp_assignments: None,
3501 enable_managed_settings: None,
3502 session_fs_provider: None,
3503 suppress_resume_event: None,
3504 continue_pending_work: None,
3505 permission_handler: None,
3506 elicitation_handler: None,
3507 mcp_auth_handler: None,
3508 user_input_handler: None,
3509 exit_plan_mode_handler: None,
3510 auto_mode_switch_handler: None,
3511 hooks_handler: None,
3512 permission_policy: None,
3513 system_message_transform: None,
3514 skip_custom_instructions: None,
3515 custom_agents_local_only: None,
3516 coauthor_enabled: None,
3517 manage_schedule_enabled: None,
3518 }
3519 }
3520
3521 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3523 self.permission_handler = Some(handler);
3524 self
3525 }
3526
3527 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3529 self.elicitation_handler = Some(handler);
3530 self
3531 }
3532
3533 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3535 self.mcp_auth_handler = Some(handler);
3536 self
3537 }
3538
3539 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3541 self.user_input_handler = Some(handler);
3542 self
3543 }
3544
3545 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3547 self.exit_plan_mode_handler = Some(handler);
3548 self
3549 }
3550
3551 pub fn with_auto_mode_switch_handler(
3553 mut self,
3554 handler: Arc<dyn AutoModeSwitchHandler>,
3555 ) -> Self {
3556 self.auto_mode_switch_handler = Some(handler);
3557 self
3558 }
3559
3560 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3563 self.hooks_handler = Some(hooks);
3564 self
3565 }
3566
3567 pub fn with_system_message_transform(
3569 mut self,
3570 transform: Arc<dyn SystemMessageTransform>,
3571 ) -> Self {
3572 self.system_message_transform = Some(transform);
3573 self
3574 }
3575
3576 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3580 self.commands = Some(commands);
3581 self
3582 }
3583
3584 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3587 self.session_fs_provider = Some(provider);
3588 self
3589 }
3590
3591 pub fn approve_all_permissions(mut self) -> Self {
3594 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3595 self
3596 }
3597
3598 pub fn deny_all_permissions(mut self) -> Self {
3601 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3602 self
3603 }
3604
3605 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3608 where
3609 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3610 {
3611 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3612 self
3613 }
3614
3615 pub fn with_model(mut self, model: impl Into<String>) -> Self {
3617 self.model = Some(model.into());
3618 self
3619 }
3620
3621 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3623 self.client_name = Some(name.into());
3624 self
3625 }
3626
3627 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3629 self.reasoning_effort = Some(effort.into());
3630 self
3631 }
3632
3633 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3635 self.reasoning_summary = Some(summary);
3636 self
3637 }
3638
3639 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3642 self.context_tier = Some(tier.into());
3643 self
3644 }
3645
3646 pub fn with_streaming(mut self, streaming: bool) -> Self {
3648 self.streaming = Some(streaming);
3649 self
3650 }
3651
3652 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3655 self.system_message = Some(system_message);
3656 self
3657 }
3658
3659 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3661 self.tools = Some(tools.into_iter().collect());
3662 self
3663 }
3664
3665 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3667 self.canvases = Some(canvases.into_iter().collect());
3668 self
3669 }
3670
3671 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
3673 self.canvas_handler = Some(handler);
3674 self
3675 }
3676
3677 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
3679 mut self,
3680 open_canvases: I,
3681 ) -> Self {
3682 self.open_canvases = Some(open_canvases.into_iter().collect());
3683 self
3684 }
3685
3686 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
3688 self.request_canvas_renderer = Some(request);
3689 self
3690 }
3691
3692 pub fn with_request_extensions(mut self, request: bool) -> Self {
3694 self.request_extensions = Some(request);
3695 self
3696 }
3697
3698 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
3702 self.extension_sdk_path = Some(path.into());
3703 self
3704 }
3705
3706 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
3708 self.extension_info = Some(extension_info);
3709 self
3710 }
3711
3712 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
3715 self.canvas_provider = Some(canvas_provider);
3716 self
3717 }
3718
3719 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
3721 where
3722 I: IntoIterator<Item = S>,
3723 S: Into<String>,
3724 {
3725 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
3726 self
3727 }
3728
3729 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
3731 where
3732 I: IntoIterator<Item = S>,
3733 S: Into<String>,
3734 {
3735 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
3736 self
3737 }
3738
3739 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
3741 where
3742 I: IntoIterator<Item = S>,
3743 S: Into<String>,
3744 {
3745 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
3746 self
3747 }
3748
3749 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
3751 self.mcp_servers = Some(servers);
3752 self
3753 }
3754
3755 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3758 self.mcp_oauth_token_storage = Some(mode.into());
3759 self
3760 }
3761
3762 pub fn with_embedding_cache_storage(
3764 mut self,
3765 embedding_cache_storage: impl Into<String>,
3766 ) -> Self {
3767 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3768 self
3769 }
3770
3771 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3774 self.enable_config_discovery = Some(enable);
3775 self
3776 }
3777
3778 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3780 self.skip_embedding_retrieval = Some(value);
3781 self
3782 }
3783
3784 pub fn with_organization_custom_instructions(
3786 mut self,
3787 instructions: impl Into<String>,
3788 ) -> Self {
3789 self.organization_custom_instructions = Some(instructions.into());
3790 self
3791 }
3792
3793 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3795 self.enable_on_demand_instruction_discovery = Some(value);
3796 self
3797 }
3798
3799 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3801 self.enable_file_hooks = Some(value);
3802 self
3803 }
3804
3805 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3807 self.enable_host_git_operations = Some(value);
3808 self
3809 }
3810
3811 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3813 self.enable_session_store = Some(value);
3814 self
3815 }
3816
3817 pub fn with_enable_skills(mut self, value: bool) -> Self {
3819 self.enable_skills = Some(value);
3820 self
3821 }
3822
3823 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3829 self.enable_mcp_apps = Some(enable);
3830 self
3831 }
3832
3833 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3835 where
3836 I: IntoIterator<Item = P>,
3837 P: Into<PathBuf>,
3838 {
3839 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3840 self
3841 }
3842
3843 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3847 where
3848 I: IntoIterator<Item = P>,
3849 P: Into<PathBuf>,
3850 {
3851 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3852 self
3853 }
3854
3855 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3857 where
3858 I: IntoIterator<Item = P>,
3859 P: Into<PathBuf>,
3860 {
3861 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3862 self
3863 }
3864
3865 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3867 self.large_output = Some(config);
3868 self
3869 }
3870
3871 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
3874 self.tool_search = Some(config);
3875 self
3876 }
3877
3878 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3880 where
3881 I: IntoIterator<Item = S>,
3882 S: Into<String>,
3883 {
3884 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3885 self
3886 }
3887
3888 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3890 mut self,
3891 agents: I,
3892 ) -> Self {
3893 self.custom_agents = Some(agents.into_iter().collect());
3894 self
3895 }
3896
3897 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3899 self.default_agent = Some(agent);
3900 self
3901 }
3902
3903 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3905 self.agent = Some(name.into());
3906 self
3907 }
3908
3909 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3911 self.infinite_sessions = Some(config);
3912 self
3913 }
3914
3915 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3917 self.provider = Some(provider);
3918 self
3919 }
3920
3921 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3923 self.capi = Some(capi);
3924 self
3925 }
3926
3927 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3933 self.providers = Some(providers);
3934 self
3935 }
3936
3937 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3943 self.models = Some(models);
3944 self
3945 }
3946
3947 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3951 self.enable_session_telemetry = Some(enable);
3952 self
3953 }
3954
3955 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3957 self.enable_citations = Some(enable);
3958 self
3959 }
3960
3961 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3963 self.session_limits = Some(limits);
3964 self
3965 }
3966
3967 pub fn with_model_capabilities(
3969 mut self,
3970 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3971 ) -> Self {
3972 self.model_capabilities = Some(capabilities);
3973 self
3974 }
3975
3976 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3978 self.memory = Some(memory);
3979 self
3980 }
3981
3982 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3984 self.config_directory = Some(dir.into());
3985 self
3986 }
3987
3988 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3990 self.working_directory = Some(dir.into());
3991 self
3992 }
3993
3994 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3998 self.github_token = Some(token.into());
3999 self
4000 }
4001
4002 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4004 self.include_sub_agent_streaming_events = Some(include);
4005 self
4006 }
4007
4008 pub fn with_remote_session(
4010 mut self,
4011 mode: crate::generated::api_types::RemoteSessionMode,
4012 ) -> Self {
4013 self.remote_session = Some(mode);
4014 self
4015 }
4016
4017 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4020 self.suppress_resume_event = Some(suppress);
4021 self
4022 }
4023
4024 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4030 self.continue_pending_work = Some(continue_pending);
4031 self
4032 }
4033
4034 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4036 self.skip_custom_instructions = Some(value);
4037 self
4038 }
4039
4040 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4042 self.custom_agents_local_only = Some(value);
4043 self
4044 }
4045
4046 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4048 self.coauthor_enabled = Some(value);
4049 self
4050 }
4051
4052 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4054 self.manage_schedule_enabled = Some(value);
4055 self
4056 }
4057
4058 #[doc(hidden)]
4062 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4063 self.exp_assignments = Some(assignments);
4064 self
4065 }
4066
4067 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4070 self.enable_managed_settings = Some(enabled);
4071 self
4072 }
4073}
4074
4075#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4081#[serde(rename_all = "camelCase")]
4082#[non_exhaustive]
4083pub struct SystemMessageConfig {
4084 #[serde(skip_serializing_if = "Option::is_none")]
4086 pub mode: Option<String>,
4087 #[serde(skip_serializing_if = "Option::is_none")]
4089 pub content: Option<String>,
4090 #[serde(skip_serializing_if = "Option::is_none")]
4092 pub sections: Option<HashMap<String, SectionOverride>>,
4093}
4094
4095impl SystemMessageConfig {
4096 pub fn new() -> Self {
4099 Self::default()
4100 }
4101
4102 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4105 self.mode = Some(mode.into());
4106 self
4107 }
4108
4109 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4112 self.content = Some(content.into());
4113 self
4114 }
4115
4116 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4118 self.sections = Some(sections);
4119 self
4120 }
4121}
4122
4123#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4129#[serde(rename_all = "camelCase")]
4130pub struct SectionOverride {
4131 #[serde(skip_serializing_if = "Option::is_none")]
4134 pub action: Option<String>,
4135 #[serde(skip_serializing_if = "Option::is_none")]
4137 pub content: Option<String>,
4138}
4139
4140#[derive(Debug, Clone, Serialize, Deserialize)]
4142#[serde(rename_all = "camelCase")]
4143pub struct CreateSessionResult {
4144 pub session_id: SessionId,
4146 #[serde(skip_serializing_if = "Option::is_none")]
4148 pub workspace_path: Option<PathBuf>,
4149 #[serde(default, alias = "remote_url")]
4151 pub remote_url: Option<String>,
4152 #[serde(skip_serializing_if = "Option::is_none")]
4154 pub capabilities: Option<SessionCapabilities>,
4155}
4156
4157#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4159#[serde(rename_all = "camelCase")]
4160pub(crate) struct ResumeSessionResult {
4161 #[serde(default)]
4163 pub session_id: Option<SessionId>,
4164 #[serde(default, skip_serializing_if = "Option::is_none")]
4166 pub workspace_path: Option<PathBuf>,
4167 #[serde(default, alias = "remote_url")]
4169 pub remote_url: Option<String>,
4170 #[serde(default, skip_serializing_if = "Option::is_none")]
4172 pub capabilities: Option<SessionCapabilities>,
4173 #[serde(
4175 default,
4176 alias = "openCanvasInstances",
4177 skip_serializing_if = "Option::is_none"
4178 )]
4179 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4180}
4181
4182#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4184#[serde(rename_all = "lowercase")]
4185pub enum LogLevel {
4186 #[default]
4188 Info,
4189 Warning,
4191 Error,
4193}
4194
4195#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4200#[serde(rename_all = "camelCase")]
4201pub struct LogOptions {
4202 #[serde(skip_serializing_if = "Option::is_none")]
4204 pub level: Option<LogLevel>,
4205 #[serde(skip_serializing_if = "Option::is_none")]
4208 pub ephemeral: Option<bool>,
4209}
4210
4211impl LogOptions {
4212 pub fn with_level(mut self, level: LogLevel) -> Self {
4214 self.level = Some(level);
4215 self
4216 }
4217
4218 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4220 self.ephemeral = Some(ephemeral);
4221 self
4222 }
4223}
4224
4225#[derive(Debug, Clone, Default)]
4229pub struct SetModelOptions {
4230 pub reasoning_effort: Option<String>,
4233 pub reasoning_summary: Option<ReasoningSummary>,
4237 pub context_tier: Option<ContextTier>,
4240 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4244}
4245
4246impl SetModelOptions {
4247 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4249 self.reasoning_effort = Some(effort.into());
4250 self
4251 }
4252
4253 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4255 self.reasoning_summary = Some(summary);
4256 self
4257 }
4258
4259 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4261 self.context_tier = Some(tier);
4262 self
4263 }
4264
4265 pub fn with_model_capabilities(
4267 mut self,
4268 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4269 ) -> Self {
4270 self.model_capabilities = Some(caps);
4271 self
4272 }
4273}
4274
4275#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4282#[serde(rename_all = "camelCase")]
4283pub struct PingResponse {
4284 #[serde(default)]
4286 pub message: String,
4287 #[serde(default)]
4289 pub timestamp: String,
4290 #[serde(skip_serializing_if = "Option::is_none")]
4292 pub protocol_version: Option<u32>,
4293}
4294
4295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4297#[serde(rename_all = "camelCase")]
4298pub struct AttachmentLineRange {
4299 pub start: u32,
4301 pub end: u32,
4303}
4304
4305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4307#[serde(rename_all = "camelCase")]
4308pub struct AttachmentSelectionPosition {
4309 pub line: u32,
4311 pub character: u32,
4313}
4314
4315#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4317#[serde(rename_all = "camelCase")]
4318pub struct AttachmentSelectionRange {
4319 pub start: AttachmentSelectionPosition,
4321 pub end: AttachmentSelectionPosition,
4323}
4324
4325#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4327#[serde(rename_all = "snake_case")]
4328#[non_exhaustive]
4329pub enum GitHubReferenceType {
4330 Issue,
4332 Pr,
4334 Discussion,
4336}
4337
4338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4344#[serde(rename_all = "camelCase")]
4345pub struct GitHubRepoPointer {
4346 #[serde(skip_serializing_if = "Option::is_none")]
4348 pub id: Option<i64>,
4349 pub name: String,
4351 pub owner: String,
4353}
4354
4355#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4357#[serde(rename_all = "camelCase")]
4358pub struct GitHubFileDiffSide {
4359 pub path: String,
4361 pub r#ref: String,
4363 pub repo: GitHubRepoPointer,
4365}
4366
4367#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4369#[serde(rename_all = "camelCase")]
4370pub struct GitHubTreeComparisonSide {
4371 pub repo: GitHubRepoPointer,
4373 pub revision: String,
4375}
4376
4377#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4379#[serde(rename_all = "camelCase")]
4380pub struct GitHubSnippetLineRange {
4381 pub start: i64,
4383 pub end: i64,
4385}
4386
4387#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4389#[serde(
4390 tag = "type",
4391 rename_all = "camelCase",
4392 rename_all_fields = "camelCase"
4393)]
4394#[non_exhaustive]
4395pub enum Attachment {
4396 File {
4398 path: PathBuf,
4400 #[serde(skip_serializing_if = "Option::is_none")]
4402 display_name: Option<String>,
4403 #[serde(skip_serializing_if = "Option::is_none")]
4405 line_range: Option<AttachmentLineRange>,
4406 },
4407 Directory {
4409 path: PathBuf,
4411 #[serde(skip_serializing_if = "Option::is_none")]
4413 display_name: Option<String>,
4414 },
4415 Selection {
4417 file_path: PathBuf,
4419 text: String,
4421 #[serde(skip_serializing_if = "Option::is_none")]
4423 display_name: Option<String>,
4424 selection: AttachmentSelectionRange,
4426 },
4427 Blob {
4429 data: String,
4431 mime_type: String,
4433 #[serde(skip_serializing_if = "Option::is_none")]
4435 display_name: Option<String>,
4436 },
4437 #[serde(rename = "github_reference")]
4439 GitHubReference {
4440 number: u64,
4442 title: String,
4444 reference_type: GitHubReferenceType,
4446 state: String,
4448 url: String,
4450 },
4451 #[serde(rename = "github_commit")]
4453 GitHubCommit {
4454 message: String,
4456 oid: String,
4458 repo: GitHubRepoPointer,
4460 url: String,
4462 },
4463 #[serde(rename = "github_release")]
4465 GitHubRelease {
4466 name: String,
4468 repo: GitHubRepoPointer,
4470 tag_name: String,
4472 url: String,
4474 },
4475 #[serde(rename = "github_actions_job")]
4477 GitHubActionsJob {
4478 #[serde(skip_serializing_if = "Option::is_none")]
4481 conclusion: Option<String>,
4482 job_id: i64,
4484 job_name: String,
4486 repo: GitHubRepoPointer,
4488 url: String,
4490 workflow_name: String,
4492 },
4493 #[serde(rename = "github_repository")]
4495 GitHubRepository {
4496 #[serde(skip_serializing_if = "Option::is_none")]
4498 description: Option<String>,
4499 #[serde(skip_serializing_if = "Option::is_none")]
4502 r#ref: Option<String>,
4503 repo: GitHubRepoPointer,
4505 url: String,
4507 },
4508 #[serde(rename = "github_file_diff")]
4510 GitHubFileDiff {
4511 #[serde(skip_serializing_if = "Option::is_none")]
4513 base: Option<GitHubFileDiffSide>,
4514 #[serde(skip_serializing_if = "Option::is_none")]
4516 head: Option<GitHubFileDiffSide>,
4517 url: String,
4519 },
4520 #[serde(rename = "github_tree_comparison")]
4522 GitHubTreeComparison {
4523 base: GitHubTreeComparisonSide,
4525 head: GitHubTreeComparisonSide,
4527 url: String,
4529 },
4530 #[serde(rename = "github_url")]
4532 GitHubUrl {
4533 url: String,
4535 },
4536 #[serde(rename = "github_file")]
4538 GitHubFile {
4539 path: String,
4541 r#ref: String,
4543 repo: GitHubRepoPointer,
4545 url: String,
4547 },
4548 #[serde(rename = "github_snippet")]
4550 GitHubSnippet {
4551 line_range: GitHubSnippetLineRange,
4553 path: String,
4555 r#ref: String,
4557 repo: GitHubRepoPointer,
4559 url: String,
4561 },
4562}
4563
4564impl Attachment {
4565 pub fn display_name(&self) -> Option<&str> {
4567 match self {
4568 Self::File { display_name, .. }
4569 | Self::Directory { display_name, .. }
4570 | Self::Selection { display_name, .. }
4571 | Self::Blob { display_name, .. } => display_name.as_deref(),
4572 Self::GitHubReference { .. }
4573 | Self::GitHubCommit { .. }
4574 | Self::GitHubRelease { .. }
4575 | Self::GitHubActionsJob { .. }
4576 | Self::GitHubRepository { .. }
4577 | Self::GitHubFileDiff { .. }
4578 | Self::GitHubTreeComparison { .. }
4579 | Self::GitHubUrl { .. }
4580 | Self::GitHubFile { .. }
4581 | Self::GitHubSnippet { .. } => None,
4582 }
4583 }
4584
4585 pub fn label(&self) -> Option<String> {
4587 if let Some(display_name) = self
4588 .display_name()
4589 .map(str::trim)
4590 .filter(|name| !name.is_empty())
4591 {
4592 return Some(display_name.to_string());
4593 }
4594
4595 match self {
4596 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4597 format!("#{}", number)
4598 } else {
4599 title.trim().to_string()
4600 }),
4601 _ => self.derived_display_name(),
4602 }
4603 }
4604
4605 pub fn ensure_display_name(&mut self) {
4607 if self
4608 .display_name()
4609 .map(str::trim)
4610 .is_some_and(|name| !name.is_empty())
4611 {
4612 return;
4613 }
4614
4615 let Some(derived_display_name) = self.derived_display_name() else {
4616 return;
4617 };
4618
4619 match self {
4620 Self::File { display_name, .. }
4621 | Self::Directory { display_name, .. }
4622 | Self::Selection { display_name, .. }
4623 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4624 Self::GitHubReference { .. }
4625 | Self::GitHubCommit { .. }
4626 | Self::GitHubRelease { .. }
4627 | Self::GitHubActionsJob { .. }
4628 | Self::GitHubRepository { .. }
4629 | Self::GitHubFileDiff { .. }
4630 | Self::GitHubTreeComparison { .. }
4631 | Self::GitHubUrl { .. }
4632 | Self::GitHubFile { .. }
4633 | Self::GitHubSnippet { .. } => {}
4634 }
4635 }
4636
4637 fn derived_display_name(&self) -> Option<String> {
4638 match self {
4639 Self::File { path, .. } | Self::Directory { path, .. } => {
4640 Some(attachment_name_from_path(path))
4641 }
4642 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4643 Self::Blob { .. } => Some("attachment".to_string()),
4644 Self::GitHubReference { .. }
4645 | Self::GitHubCommit { .. }
4646 | Self::GitHubRelease { .. }
4647 | Self::GitHubActionsJob { .. }
4648 | Self::GitHubRepository { .. }
4649 | Self::GitHubFileDiff { .. }
4650 | Self::GitHubTreeComparison { .. }
4651 | Self::GitHubUrl { .. }
4652 | Self::GitHubFile { .. }
4653 | Self::GitHubSnippet { .. } => None,
4654 }
4655 }
4656}
4657
4658fn attachment_name_from_path(path: &Path) -> String {
4659 path.file_name()
4660 .map(|name| name.to_string_lossy().into_owned())
4661 .filter(|name| !name.is_empty())
4662 .unwrap_or_else(|| {
4663 let full = path.to_string_lossy();
4664 if full.is_empty() {
4665 "attachment".to_string()
4666 } else {
4667 full.into_owned()
4668 }
4669 })
4670}
4671
4672pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
4674 for attachment in attachments {
4675 attachment.ensure_display_name();
4676 }
4677}
4678
4679#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4684#[serde(rename_all = "lowercase")]
4685#[non_exhaustive]
4686pub enum DeliveryMode {
4687 Enqueue,
4689 Immediate,
4691}
4692
4693#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4698#[serde(rename_all = "lowercase")]
4699#[non_exhaustive]
4700pub enum AgentMode {
4701 Interactive,
4703 Plan,
4705 Autopilot,
4707 Shell,
4709}
4710
4711#[derive(Debug, Clone)]
4740#[non_exhaustive]
4741pub struct MessageOptions {
4742 pub prompt: String,
4744 pub mode: Option<DeliveryMode>,
4750 pub agent_mode: Option<AgentMode>,
4754 pub attachments: Option<Vec<Attachment>>,
4756 pub wait_timeout: Option<Duration>,
4759 pub request_headers: Option<HashMap<String, String>>,
4763 pub traceparent: Option<String>,
4770 pub tracestate: Option<String>,
4774 pub display_prompt: Option<String>,
4776}
4777
4778impl MessageOptions {
4779 pub fn new(prompt: impl Into<String>) -> Self {
4781 Self {
4782 prompt: prompt.into(),
4783 mode: None,
4784 agent_mode: None,
4785 attachments: None,
4786 wait_timeout: None,
4787 request_headers: None,
4788 traceparent: None,
4789 tracestate: None,
4790 display_prompt: None,
4791 }
4792 }
4793
4794 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
4800 self.mode = Some(mode);
4801 self
4802 }
4803
4804 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
4808 self.agent_mode = Some(agent_mode);
4809 self
4810 }
4811
4812 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
4814 self.attachments = Some(attachments);
4815 self
4816 }
4817
4818 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
4820 self.wait_timeout = Some(timeout);
4821 self
4822 }
4823
4824 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
4826 self.request_headers = Some(headers);
4827 self
4828 }
4829
4830 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
4835 self.traceparent = ctx.traceparent;
4836 self.tracestate = ctx.tracestate;
4837 self
4838 }
4839
4840 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
4842 self.traceparent = Some(traceparent.into());
4843 self
4844 }
4845
4846 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
4848 self.tracestate = Some(tracestate.into());
4849 self
4850 }
4851
4852 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
4854 self.display_prompt = Some(display_prompt.into());
4855 self
4856 }
4857}
4858
4859impl From<&str> for MessageOptions {
4860 fn from(prompt: &str) -> Self {
4861 Self::new(prompt)
4862 }
4863}
4864
4865impl From<String> for MessageOptions {
4866 fn from(prompt: String) -> Self {
4867 Self::new(prompt)
4868 }
4869}
4870
4871impl From<&String> for MessageOptions {
4872 fn from(prompt: &String) -> Self {
4873 Self::new(prompt.clone())
4874 }
4875}
4876
4877#[derive(Debug, Clone, Serialize, Deserialize)]
4879#[serde(rename_all = "camelCase")]
4880#[non_exhaustive]
4881pub struct GetStatusResponse {
4882 pub version: String,
4884 pub protocol_version: u32,
4886}
4887
4888#[derive(Debug, Clone, Serialize, Deserialize)]
4890#[serde(rename_all = "camelCase")]
4891#[non_exhaustive]
4892pub struct GetAuthStatusResponse {
4893 pub is_authenticated: bool,
4895 #[serde(skip_serializing_if = "Option::is_none")]
4898 pub auth_type: Option<String>,
4899 #[serde(skip_serializing_if = "Option::is_none")]
4901 pub host: Option<String>,
4902 #[serde(skip_serializing_if = "Option::is_none")]
4904 pub login: Option<String>,
4905 #[serde(skip_serializing_if = "Option::is_none")]
4907 pub status_message: Option<String>,
4908}
4909
4910#[derive(Debug, Clone, Serialize, Deserialize)]
4914#[serde(rename_all = "camelCase")]
4915pub struct SessionEventNotification {
4916 pub session_id: SessionId,
4918 pub event: SessionEvent,
4920}
4921
4922#[derive(Debug, Clone, Serialize, Deserialize)]
4929#[serde(rename_all = "camelCase")]
4930pub struct SessionEvent {
4931 pub id: String,
4933 pub timestamp: String,
4935 pub parent_id: Option<String>,
4937 #[serde(skip_serializing_if = "Option::is_none")]
4939 pub ephemeral: Option<bool>,
4940 #[serde(skip_serializing_if = "Option::is_none")]
4943 pub agent_id: Option<String>,
4944 #[serde(skip_serializing_if = "Option::is_none")]
4946 pub debug_cli_received_at_ms: Option<i64>,
4947 #[serde(skip_serializing_if = "Option::is_none")]
4949 pub debug_ws_forwarded_at_ms: Option<i64>,
4950 #[serde(rename = "type")]
4952 pub event_type: String,
4953 pub data: Value,
4955}
4956
4957impl SessionEvent {
4958 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
4963 use serde::de::IntoDeserializer;
4964 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
4965 self.event_type.as_str().into_deserializer();
4966 crate::generated::SessionEventType::deserialize(deserializer)
4967 .unwrap_or(crate::generated::SessionEventType::Unknown)
4968 }
4969
4970 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
4976 serde_json::from_value(self.data.clone()).ok()
4977 }
4978
4979 pub fn is_transient_error(&self) -> bool {
4983 self.event_type == "session.error"
4984 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
4985 }
4986}
4987
4988#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4993#[serde(rename_all = "camelCase")]
4994#[non_exhaustive]
4995pub struct ToolInvocation {
4996 pub session_id: SessionId,
4998 pub tool_call_id: String,
5000 pub tool_name: String,
5002 pub arguments: Value,
5004 #[serde(skip)]
5012 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5013 #[serde(default, skip_serializing_if = "Option::is_none")]
5018 pub traceparent: Option<String>,
5019 #[serde(default, skip_serializing_if = "Option::is_none")]
5022 pub tracestate: Option<String>,
5023}
5024
5025impl ToolInvocation {
5026 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5047 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5048 }
5049
5050 pub fn trace_context(&self) -> TraceContext {
5053 TraceContext {
5054 traceparent: self.traceparent.clone(),
5055 tracestate: self.tracestate.clone(),
5056 }
5057 }
5058}
5059
5060#[derive(Debug, Clone, Serialize, Deserialize)]
5062#[serde(rename_all = "camelCase")]
5063pub struct ToolBinaryResult {
5064 pub data: String,
5066 pub mime_type: String,
5068 pub r#type: String,
5070 #[serde(default, skip_serializing_if = "Option::is_none")]
5072 pub description: Option<String>,
5073}
5074
5075#[derive(Debug, Clone, Serialize, Deserialize)]
5082#[serde(rename_all = "camelCase")]
5083#[non_exhaustive]
5084pub struct ToolResultExpanded {
5085 pub text_result_for_llm: String,
5087 pub result_type: String,
5089 #[serde(default, skip_serializing_if = "Option::is_none")]
5091 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5092 #[serde(skip_serializing_if = "Option::is_none")]
5094 pub session_log: Option<String>,
5095 #[serde(skip_serializing_if = "Option::is_none")]
5097 pub error: Option<String>,
5098 #[serde(default, skip_serializing_if = "Option::is_none")]
5100 pub tool_telemetry: Option<HashMap<String, Value>>,
5101 #[serde(default, skip_serializing_if = "Option::is_none")]
5103 pub tool_references: Option<Vec<String>>,
5104}
5105
5106impl ToolResultExpanded {
5107 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5111 Self {
5112 text_result_for_llm: text_result_for_llm.into(),
5113 result_type: result_type.into(),
5114 binary_results_for_llm: None,
5115 session_log: None,
5116 error: None,
5117 tool_telemetry: None,
5118 tool_references: None,
5119 }
5120 }
5121
5122 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5124 self.binary_results_for_llm = Some(results);
5125 self
5126 }
5127
5128 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5130 self.session_log = Some(session_log.into());
5131 self
5132 }
5133
5134 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5136 self.error = Some(error.into());
5137 self
5138 }
5139
5140 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5142 self.tool_telemetry = Some(telemetry);
5143 self
5144 }
5145
5146 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5148 where
5149 I: IntoIterator<Item = S>,
5150 S: Into<String>,
5151 {
5152 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5153 self
5154 }
5155}
5156
5157#[derive(Debug, Clone, Serialize, Deserialize)]
5159#[serde(untagged)]
5160#[non_exhaustive]
5161pub enum ToolResult {
5162 Text(String),
5164 Expanded(ToolResultExpanded),
5166}
5167
5168#[derive(Debug, Clone, Serialize, Deserialize)]
5170#[serde(rename_all = "camelCase")]
5171pub struct ToolResultResponse {
5172 pub result: ToolResult,
5174}
5175
5176#[derive(Debug, Clone, Serialize, Deserialize)]
5178#[serde(rename_all = "camelCase")]
5179pub struct SessionMetadata {
5180 pub session_id: SessionId,
5182 pub start_time: String,
5184 pub modified_time: String,
5186 #[serde(skip_serializing_if = "Option::is_none")]
5188 pub summary: Option<String>,
5189 pub is_remote: bool,
5191}
5192
5193#[derive(Debug, Clone, Serialize, Deserialize)]
5195#[serde(rename_all = "camelCase")]
5196pub struct ListSessionsResponse {
5197 pub sessions: Vec<SessionMetadata>,
5199}
5200
5201#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5205#[serde(rename_all = "camelCase")]
5206pub struct SessionListFilter {
5207 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5209 pub working_directory: Option<String>,
5210 #[serde(default, skip_serializing_if = "Option::is_none")]
5212 pub git_root: Option<String>,
5213 #[serde(default, skip_serializing_if = "Option::is_none")]
5215 pub repository: Option<String>,
5216 #[serde(default, skip_serializing_if = "Option::is_none")]
5218 pub branch: Option<String>,
5219}
5220
5221#[derive(Debug, Clone, Serialize, Deserialize)]
5223#[serde(rename_all = "camelCase")]
5224pub struct GetSessionMetadataResponse {
5225 #[serde(skip_serializing_if = "Option::is_none")]
5227 pub session: Option<SessionMetadata>,
5228}
5229
5230#[derive(Debug, Clone, Serialize, Deserialize)]
5232#[serde(rename_all = "camelCase")]
5233pub struct GetLastSessionIdResponse {
5234 #[serde(skip_serializing_if = "Option::is_none")]
5236 pub session_id: Option<SessionId>,
5237}
5238
5239#[derive(Debug, Clone, Serialize, Deserialize)]
5241#[serde(rename_all = "camelCase")]
5242pub struct GetForegroundSessionResponse {
5243 #[serde(skip_serializing_if = "Option::is_none")]
5245 pub session_id: Option<SessionId>,
5246}
5247
5248#[derive(Debug, Clone, Serialize, Deserialize)]
5250#[serde(rename_all = "camelCase")]
5251pub struct GetMessagesResponse {
5252 pub events: Vec<SessionEvent>,
5254}
5255
5256#[derive(Debug, Clone, Serialize, Deserialize)]
5258#[serde(rename_all = "camelCase")]
5259pub struct ElicitationResult {
5260 pub action: String,
5262 #[serde(skip_serializing_if = "Option::is_none")]
5264 pub content: Option<Value>,
5265}
5266
5267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5273#[serde(rename_all = "camelCase")]
5274#[non_exhaustive]
5275pub enum ElicitationMode {
5276 Form,
5278 Url,
5280 #[serde(other)]
5282 Unknown,
5283}
5284
5285#[derive(Debug, Clone, Serialize, Deserialize)]
5292#[serde(rename_all = "camelCase")]
5293pub struct ElicitationRequest {
5294 pub message: String,
5296 #[serde(skip_serializing_if = "Option::is_none")]
5298 pub requested_schema: Option<Value>,
5299 #[serde(skip_serializing_if = "Option::is_none")]
5301 pub mode: Option<ElicitationMode>,
5302 #[serde(skip_serializing_if = "Option::is_none")]
5304 pub elicitation_source: Option<String>,
5305 #[serde(skip_serializing_if = "Option::is_none")]
5307 pub url: Option<String>,
5308}
5309
5310#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5315#[serde(rename_all = "camelCase")]
5316pub struct SessionCapabilities {
5317 #[serde(skip_serializing_if = "Option::is_none")]
5319 pub ui: Option<UiCapabilities>,
5320}
5321
5322#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5324#[serde(rename_all = "camelCase")]
5325pub struct UiCapabilities {
5326 #[serde(skip_serializing_if = "Option::is_none")]
5328 pub elicitation: Option<bool>,
5329 #[serde(skip_serializing_if = "Option::is_none")]
5340 pub mcp_apps: Option<bool>,
5341 #[serde(skip_serializing_if = "Option::is_none")]
5343 pub canvases: Option<bool>,
5344}
5345
5346#[derive(Debug, Clone, Default)]
5348pub struct UiInputOptions<'a> {
5349 pub title: Option<&'a str>,
5351 pub description: Option<&'a str>,
5353 pub min_length: Option<u64>,
5355 pub max_length: Option<u64>,
5357 pub format: Option<InputFormat>,
5359 pub default: Option<&'a str>,
5361}
5362
5363#[derive(Debug, Clone, Copy)]
5365#[non_exhaustive]
5366pub enum InputFormat {
5367 Email,
5369 Uri,
5371 Date,
5373 DateTime,
5375}
5376
5377impl InputFormat {
5378 pub fn as_str(&self) -> &'static str {
5380 match self {
5381 Self::Email => "email",
5382 Self::Uri => "uri",
5383 Self::Date => "date",
5384 Self::DateTime => "date-time",
5385 }
5386 }
5387}
5388
5389pub use crate::generated::api_types::{
5394 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5395 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5396 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5397 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
5398};
5399
5400#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5406#[serde(rename_all = "kebab-case")]
5407#[non_exhaustive]
5408pub enum PermissionRequestKind {
5409 Shell,
5411 Write,
5413 Read,
5415 Url,
5417 Mcp,
5419 CustomTool,
5421 Memory,
5423 Hook,
5425 #[serde(other)]
5428 Unknown,
5429}
5430
5431#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5437#[serde(rename_all = "camelCase")]
5438pub struct PermissionRequestData {
5439 #[serde(default, skip_serializing_if = "Option::is_none")]
5443 pub kind: Option<PermissionRequestKind>,
5444 #[serde(default, skip_serializing_if = "Option::is_none")]
5447 pub tool_call_id: Option<String>,
5448 #[serde(flatten)]
5451 pub extra: Value,
5452}
5453
5454#[derive(Debug, Clone, Serialize, Deserialize)]
5456#[serde(rename_all = "camelCase")]
5457pub struct ExitPlanModeData {
5458 #[serde(default)]
5460 pub summary: String,
5461 #[serde(default, skip_serializing_if = "Option::is_none")]
5463 pub plan_content: Option<String>,
5464 #[serde(default)]
5466 pub actions: Vec<String>,
5467 #[serde(default = "default_recommended_action")]
5469 pub recommended_action: String,
5470}
5471
5472fn default_recommended_action() -> String {
5473 "autopilot".to_string()
5474}
5475
5476impl Default for ExitPlanModeData {
5477 fn default() -> Self {
5478 Self {
5479 summary: String::new(),
5480 plan_content: None,
5481 actions: Vec::new(),
5482 recommended_action: default_recommended_action(),
5483 }
5484 }
5485}
5486
5487#[cfg(test)]
5488mod tests {
5489 use std::collections::HashMap;
5490 use std::path::PathBuf;
5491
5492 use serde_json::json;
5493
5494 use super::{
5495 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5496 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5497 CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry,
5498 ExpFlagValue, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig,
5499 LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration,
5500 NamedProviderConfig, ProviderConfig, ProviderModelConfig, ReasoningSummary,
5501 ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, SystemMessageConfig, Tool,
5502 ToolBinaryResult, ToolResult, ToolResultExpanded, ToolResultResponse,
5503 ensure_attachment_display_names,
5504 };
5505 use crate::generated::session_events::TypedSessionEvent;
5506
5507 #[test]
5508 fn tool_builder_composes() {
5509 let tool = Tool::new("greet")
5510 .with_description("Say hello")
5511 .with_namespaced_name("hello/greet")
5512 .with_instructions("Pass the user's name")
5513 .with_parameters(json!({
5514 "type": "object",
5515 "properties": { "name": { "type": "string" } },
5516 "required": ["name"]
5517 }))
5518 .with_overrides_built_in_tool(true)
5519 .with_skip_permission(true);
5520 assert_eq!(tool.name, "greet");
5521 assert_eq!(tool.description, "Say hello");
5522 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5523 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5524 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5525 assert!(tool.overrides_built_in_tool);
5526 assert!(tool.skip_permission);
5527 }
5528
5529 #[test]
5530 fn tool_defer_serialization() {
5531 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5532 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5533 let value = serde_json::to_value(&tool).unwrap();
5534 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5535
5536 let plain = Tool::new("plain");
5537 let value = serde_json::to_value(&plain).unwrap();
5538 assert!(value.get("defer").is_none());
5539 }
5540
5541 #[test]
5542 fn tool_metadata_serialization() {
5543 use indexmap::IndexMap;
5544
5545 let mut metadata = IndexMap::new();
5546 metadata.insert(
5547 "github.com/copilot:safeForTelemetry".to_string(),
5548 json!({ "name": true, "inputsNames": false }),
5549 );
5550 let tool = Tool::new("lookup").with_metadata(metadata);
5551 let value = serde_json::to_value(&tool).unwrap();
5552 assert_eq!(
5553 value
5554 .get("metadata")
5555 .unwrap()
5556 .get("github.com/copilot:safeForTelemetry")
5557 .unwrap(),
5558 &json!({ "name": true, "inputsNames": false })
5559 );
5560
5561 let plain = Tool::new("plain");
5563 let value = serde_json::to_value(&plain).unwrap();
5564 assert!(value.get("metadata").is_none());
5565 }
5566
5567 #[test]
5568 fn custom_agent_config_builder_with_model() {
5569 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5570 .with_model("claude-haiku-4.5")
5571 .with_display_name("My Agent");
5572 assert_eq!(agent.name, "my-agent");
5573 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5574 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5575 }
5576
5577 #[test]
5578 fn custom_agent_config_serializes_model() {
5579 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5580 let wire = serde_json::to_value(&agent).unwrap();
5581 assert_eq!(wire["model"], "claude-haiku-4.5");
5582 assert_eq!(wire["name"], "model-agent");
5583 }
5584
5585 #[test]
5586 fn custom_agent_config_omits_model_when_none() {
5587 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5588 let wire = serde_json::to_value(&agent).unwrap();
5589 assert!(wire.get("model").is_none());
5590 }
5591
5592 #[test]
5593 fn custom_agent_config_builder_with_reasoning_effort() {
5594 let agent =
5595 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5596 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
5597 }
5598
5599 #[test]
5600 fn custom_agent_config_serializes_reasoning_effort() {
5601 let agent =
5602 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5603 let wire = serde_json::to_value(&agent).unwrap();
5604 assert_eq!(wire["reasoningEffort"], "high");
5605 }
5606
5607 #[test]
5608 fn custom_agent_config_omits_reasoning_effort_when_none() {
5609 let agent = CustomAgentConfig::new("default-agent", "prompt");
5610 let wire = serde_json::to_value(&agent).unwrap();
5611 assert!(wire.get("reasoningEffort").is_none());
5612 }
5613
5614 #[test]
5615 #[should_panic(expected = "tool parameter schema must be a JSON object")]
5616 fn tool_with_parameters_panics_on_non_object_value() {
5617 let _ = Tool::new("noop").with_parameters(json!(null));
5618 }
5619
5620 #[test]
5621 fn tool_result_expanded_serializes_binary_results_for_llm() {
5622 let response = ToolResultResponse {
5623 result: ToolResult::Expanded(ToolResultExpanded {
5624 text_result_for_llm: "rendered chart".to_string(),
5625 result_type: "success".to_string(),
5626 binary_results_for_llm: Some(vec![ToolBinaryResult {
5627 data: "aW1n".to_string(),
5628 mime_type: "image/png".to_string(),
5629 r#type: "image".to_string(),
5630 description: Some("chart preview".to_string()),
5631 }]),
5632 session_log: None,
5633 error: None,
5634 tool_telemetry: None,
5635 tool_references: None,
5636 }),
5637 };
5638
5639 let wire = serde_json::to_value(&response).unwrap();
5640
5641 assert_eq!(
5642 wire,
5643 json!({
5644 "result": {
5645 "textResultForLlm": "rendered chart",
5646 "resultType": "success",
5647 "binaryResultsForLlm": [
5648 {
5649 "data": "aW1n",
5650 "mimeType": "image/png",
5651 "type": "image",
5652 "description": "chart preview"
5653 }
5654 ]
5655 }
5656 })
5657 );
5658 }
5659
5660 #[test]
5661 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
5662 let response = ToolResultResponse {
5663 result: ToolResult::Expanded(ToolResultExpanded {
5664 text_result_for_llm: "ok".to_string(),
5665 result_type: "success".to_string(),
5666 binary_results_for_llm: None,
5667 session_log: None,
5668 error: None,
5669 tool_telemetry: None,
5670 tool_references: None,
5671 }),
5672 };
5673
5674 let wire = serde_json::to_value(&response).unwrap();
5675
5676 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5677 assert!(wire["result"].get("binaryResultsForLlm").is_none());
5678 }
5679
5680 #[test]
5681 fn tool_result_expanded_serializes_tool_references() {
5682 let response = ToolResultResponse {
5683 result: ToolResult::Expanded(
5684 ToolResultExpanded::new("found 2 tools", "success")
5685 .with_tool_references(["get_weather", "check_status"]),
5686 ),
5687 };
5688
5689 let wire = serde_json::to_value(&response).unwrap();
5690
5691 assert_eq!(
5692 wire,
5693 json!({
5694 "result": {
5695 "textResultForLlm": "found 2 tools",
5696 "resultType": "success",
5697 "toolReferences": ["get_weather", "check_status"]
5698 }
5699 })
5700 );
5701 }
5702
5703 #[test]
5704 fn tool_result_expanded_omits_tool_references_when_none() {
5705 let response = ToolResultResponse {
5706 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
5707 };
5708
5709 let wire = serde_json::to_value(&response).unwrap();
5710
5711 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5712 assert!(wire["result"].get("toolReferences").is_none());
5713 }
5714
5715 #[test]
5716 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
5717 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
5720 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
5721
5722 assert_eq!(
5723 expanded.tool_references.as_deref(),
5724 Some(["alpha".to_string(), "beta".to_string()].as_slice())
5725 );
5726 }
5727
5728 #[test]
5729 fn tool_result_expanded_deserializes_tool_references() {
5730 let wire = json!({
5731 "textResultForLlm": "found tools",
5732 "resultType": "success",
5733 "toolReferences": ["alpha", "beta"]
5734 });
5735
5736 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
5737
5738 assert_eq!(
5739 expanded.tool_references.as_deref(),
5740 Some(["alpha".to_string(), "beta".to_string()].as_slice())
5741 );
5742 }
5743
5744 #[test]
5745 fn session_config_default_wire_flags_off_without_handlers() {
5746 let cfg = SessionConfig::default();
5747 assert_eq!(cfg.mcp_oauth_token_storage, None);
5748 let (wire, _runtime) = cfg
5752 .into_wire(Some(SessionId::from("default-flags")))
5753 .expect("default config has no duplicate handlers");
5754 assert!(!wire.request_user_input);
5755 assert!(!wire.request_permission);
5756 assert!(!wire.request_elicitation);
5757 assert!(!wire.request_exit_plan_mode);
5758 assert!(!wire.request_auto_mode_switch);
5759 assert!(!wire.hooks);
5760 assert!(!wire.request_mcp_apps);
5761 }
5762
5763 #[test]
5764 fn resume_session_config_new_wire_flags_off_without_handlers() {
5765 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
5766 assert_eq!(cfg.mcp_oauth_token_storage, None);
5767 let (wire, _runtime) = cfg
5768 .into_wire()
5769 .expect("default resume config has no duplicate handlers");
5770 assert!(!wire.request_user_input);
5771 assert!(!wire.request_permission);
5772 assert!(!wire.request_elicitation);
5773 assert!(!wire.request_exit_plan_mode);
5774 assert!(!wire.request_auto_mode_switch);
5775 assert!(!wire.hooks);
5776 assert!(!wire.request_mcp_apps);
5777 }
5778
5779 #[test]
5780 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5781 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
5782 assert_eq!(cfg.enable_mcp_apps, Some(true));
5783
5784 let (wire, _runtime) = cfg
5785 .into_wire(Some(SessionId::from("enable-mcp-apps")))
5786 .expect("enable_mcp_apps config has no duplicate handlers");
5787 assert!(wire.request_mcp_apps);
5788
5789 let json = serde_json::to_value(&wire).unwrap();
5790 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5791 }
5792
5793 #[test]
5794 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5795 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
5796 .with_enable_mcp_apps(true);
5797 assert_eq!(cfg.enable_mcp_apps, Some(true));
5798
5799 let (wire, _runtime) = cfg
5800 .into_wire()
5801 .expect("resume enable_mcp_apps config has no duplicate handlers");
5802 assert!(wire.request_mcp_apps);
5803
5804 let json = serde_json::to_value(&wire).unwrap();
5805 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5806 }
5807
5808 #[test]
5809 fn memory_configuration_constructors_and_serde() {
5810 assert!(MemoryConfiguration::enabled().enabled);
5811 assert!(!MemoryConfiguration::disabled().enabled);
5812 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
5813
5814 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
5815 assert_eq!(json, serde_json::json!({ "enabled": true }));
5816 }
5817
5818 #[test]
5819 fn session_config_with_memory_serializes() {
5820 let (wire, _runtime) = SessionConfig::default()
5821 .with_memory(MemoryConfiguration::enabled())
5822 .into_wire(Some(SessionId::from("memory-on")))
5823 .expect("no duplicate handlers");
5824 let json = serde_json::to_value(&wire).unwrap();
5825 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5826
5827 let (wire_off, _) = SessionConfig::default()
5828 .with_memory(MemoryConfiguration::disabled())
5829 .into_wire(Some(SessionId::from("memory-off")))
5830 .expect("no duplicate handlers");
5831 let json_off = serde_json::to_value(&wire_off).unwrap();
5832 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
5833
5834 let (empty_wire, _) = SessionConfig::default()
5836 .into_wire(Some(SessionId::from("memory-unset")))
5837 .expect("no duplicate handlers");
5838 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5839 assert!(empty_json.get("memory").is_none());
5840 }
5841
5842 #[test]
5843 fn resume_session_config_with_memory_serializes() {
5844 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
5845 .with_memory(MemoryConfiguration::enabled())
5846 .into_wire()
5847 .expect("no duplicate handlers");
5848 let json = serde_json::to_value(&wire).unwrap();
5849 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5850
5851 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
5853 .into_wire()
5854 .expect("no duplicate handlers");
5855 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5856 assert!(empty_json.get("memory").is_none());
5857 }
5858
5859 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
5860 CopilotExpAssignmentResponse {
5861 features: vec!["copilot_exp_flag".to_string()],
5862 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
5863 configs: vec![ExpConfigEntry {
5864 id: "cfg-1".to_string(),
5865 parameters: HashMap::from([
5866 ("threshold".to_string(), ExpFlagValue::Integer(5)),
5867 ("enabled".to_string(), ExpFlagValue::Bool(true)),
5868 ]),
5869 }],
5870 assignment_context: context.to_string(),
5871 ..Default::default()
5872 }
5873 }
5874
5875 #[test]
5876 fn exp_flag_value_round_trips_all_variants() {
5877 let values = serde_json::json!({
5878 "s": "text",
5879 "i": 7,
5880 "f": 1.5,
5881 "b": true,
5882 "n": null,
5883 });
5884 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
5885 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
5886 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
5887 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
5888 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
5889 assert_eq!(parsed["n"], ExpFlagValue::Null);
5890 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
5891 }
5892
5893 #[test]
5894 fn session_config_with_exp_assignments_serializes() {
5895 let assignments = sample_exp_assignments("ctx-123");
5896 let expected = serde_json::to_value(&assignments).unwrap();
5897 let (wire, _runtime) = SessionConfig::default()
5898 .with_exp_assignments(assignments)
5899 .into_wire(Some(SessionId::from("exp-on")))
5900 .expect("no duplicate handlers");
5901 let json = serde_json::to_value(&wire).unwrap();
5902 assert_eq!(json["expAssignments"], expected);
5903 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
5904 assert_eq!(
5905 json["expAssignments"]["Flights"]["copilot_exp_flag"],
5906 "treatment"
5907 );
5908
5909 let (empty_wire, _) = SessionConfig::default()
5911 .into_wire(Some(SessionId::from("exp-unset")))
5912 .expect("no duplicate handlers");
5913 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5914 assert!(empty_json.get("expAssignments").is_none());
5915 }
5916
5917 #[test]
5918 fn resume_session_config_with_exp_assignments_serializes() {
5919 let assignments = sample_exp_assignments("ctx-456");
5920 let expected = serde_json::to_value(&assignments).unwrap();
5921 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
5922 .with_exp_assignments(assignments)
5923 .into_wire()
5924 .expect("no duplicate handlers");
5925 let json = serde_json::to_value(&wire).unwrap();
5926 assert_eq!(json["expAssignments"], expected);
5927
5928 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
5930 .into_wire()
5931 .expect("no duplicate handlers");
5932 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5933 assert!(empty_json.get("expAssignments").is_none());
5934 }
5935
5936 #[test]
5937 fn session_config_clone_preserves_exp_assignments() {
5938 let assignments = sample_exp_assignments("ctx-clone");
5939 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
5940 let cloned = config.clone();
5941
5942 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5943
5944 let (wire, _runtime) = cloned
5945 .into_wire(Some(SessionId::from("exp-clone")))
5946 .expect("no duplicate handlers");
5947 let json = serde_json::to_value(&wire).unwrap();
5948 assert_eq!(
5949 json["expAssignments"],
5950 serde_json::to_value(&assignments).unwrap()
5951 );
5952 }
5953
5954 #[test]
5955 fn resume_session_config_clone_preserves_exp_assignments() {
5956 let assignments = sample_exp_assignments("ctx-clone-resume");
5957 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
5958 .with_exp_assignments(assignments.clone());
5959 let cloned = config.clone();
5960
5961 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5962
5963 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
5964 let json = serde_json::to_value(&wire).unwrap();
5965 assert_eq!(
5966 json["expAssignments"],
5967 serde_json::to_value(&assignments).unwrap()
5968 );
5969 }
5970
5971 #[test]
5972 #[allow(clippy::field_reassign_with_default)]
5973 fn session_config_into_wire_serializes_bucket_b_fields() {
5974 use std::path::PathBuf;
5975
5976 use super::{CloudSessionOptions, CloudSessionRepository};
5977
5978 let mut cfg = SessionConfig::default();
5979 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5980 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5981 cfg.github_token = Some("ghs_secret".to_string());
5982 cfg.include_sub_agent_streaming_events = Some(false);
5983 cfg.enable_session_telemetry = Some(false);
5984 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
5985 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
5986 cfg.enable_on_demand_instruction_discovery = Some(false);
5987 cfg.cloud = Some(CloudSessionOptions::with_repository(
5988 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
5989 ));
5990
5991 let (wire, _runtime) = cfg
5992 .into_wire(Some(SessionId::from("custom-id")))
5993 .expect("no duplicate handlers");
5994 let wire_json = serde_json::to_value(&wire).unwrap();
5995 assert_eq!(wire_json["sessionId"], "custom-id");
5996 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5997 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5998 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5999 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6000 assert_eq!(wire_json["enableSessionTelemetry"], false);
6001 assert_eq!(wire_json["reasoningSummary"], "concise");
6002 assert_eq!(wire_json["remoteSession"], "export");
6003 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6004 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6005 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6006 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6007
6008 let (empty_wire, _) = SessionConfig::default()
6010 .into_wire(Some(SessionId::from("empty")))
6011 .expect("default has no duplicate handlers");
6012 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6013 assert!(empty_json.get("gitHubToken").is_none());
6014 assert!(empty_json.get("enableSessionTelemetry").is_none());
6015 assert!(empty_json.get("reasoningSummary").is_none());
6016 assert!(empty_json.get("remoteSession").is_none());
6017 assert!(
6018 empty_json
6019 .get("enableOnDemandInstructionDiscovery")
6020 .is_none()
6021 );
6022 assert!(empty_json.get("cloud").is_none());
6023 }
6024
6025 #[test]
6026 fn session_config_into_wire_serializes_named_providers_and_models() {
6027 let cfg = SessionConfig::default()
6028 .with_providers(vec![
6029 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6030 .with_provider_type("openai")
6031 .with_wire_api("responses")
6032 .with_api_key("sk-test"),
6033 ])
6034 .with_models(vec![
6035 ProviderModelConfig::new("gpt-x", "my-openai")
6036 .with_wire_model("gpt-x-2025")
6037 .with_max_output_tokens(2048),
6038 ]);
6039
6040 let (wire, _) = cfg
6041 .into_wire(Some(SessionId::from("sess-providers")))
6042 .expect("no duplicate handlers");
6043 let wire_json = serde_json::to_value(&wire).unwrap();
6044 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6045 assert_eq!(
6046 wire_json["providers"][0]["baseUrl"],
6047 "https://api.example.com/v1"
6048 );
6049 assert_eq!(wire_json["providers"][0]["type"], "openai");
6050 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6051 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6052 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
6053 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
6054 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
6055 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
6056
6057 let (empty_wire, _) = SessionConfig::default()
6058 .into_wire(Some(SessionId::from("empty")))
6059 .expect("default has no duplicate handlers");
6060 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6061 assert!(empty_json.get("providers").is_none());
6062 assert!(empty_json.get("models").is_none());
6063 }
6064
6065 #[test]
6066 fn resume_config_into_wire_serializes_named_providers_and_models() {
6067 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
6068 .with_providers(vec![
6069 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
6070 .with_provider_type("azure")
6071 .with_azure(AzureProviderOptions {
6072 api_version: Some("2024-10-21".to_string()),
6073 }),
6074 ])
6075 .with_models(vec![
6076 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
6077 ]);
6078
6079 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6080 let wire_json = serde_json::to_value(&wire).unwrap();
6081 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
6082 assert_eq!(wire_json["providers"][0]["type"], "azure");
6083 assert_eq!(
6084 wire_json["providers"][0]["azure"]["apiVersion"],
6085 "2024-10-21"
6086 );
6087 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
6088 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
6089 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
6090
6091 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
6092 .into_wire()
6093 .expect("default has no duplicate handlers");
6094 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6095 assert!(empty_json.get("providers").is_none());
6096 assert!(empty_json.get("models").is_none());
6097 }
6098
6099 #[test]
6100 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
6101 use std::path::PathBuf;
6102
6103 let cfg = SessionConfig {
6104 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
6105 large_output: Some(
6106 LargeToolOutputConfig::new()
6107 .with_enabled(true)
6108 .with_max_size_bytes(1024)
6109 .with_output_directory(PathBuf::from("/tmp/large-output")),
6110 ),
6111 ..Default::default()
6112 };
6113
6114 let (wire, _) = cfg
6115 .into_wire(Some(SessionId::from("sess-1")))
6116 .expect("no duplicate handlers");
6117 let wire_json = serde_json::to_value(&wire).unwrap();
6118 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
6119 assert_eq!(wire_json["largeOutput"]["enabled"], true);
6120 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
6121 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
6122
6123 let (empty_wire, _) = SessionConfig::default()
6124 .into_wire(Some(SessionId::from("empty")))
6125 .expect("default has no duplicate handlers");
6126 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6127 assert!(empty_json.get("pluginDirectories").is_none());
6128 assert!(empty_json.get("largeOutput").is_none());
6129 }
6130
6131 #[test]
6132 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
6133 use std::path::PathBuf;
6134
6135 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6136 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6137 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6138 cfg.github_token = Some("ghs_secret".to_string());
6139 cfg.include_sub_agent_streaming_events = Some(true);
6140 cfg.enable_session_telemetry = Some(false);
6141 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
6142 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
6143 cfg.enable_on_demand_instruction_discovery = Some(false);
6144
6145 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6146 let wire_json = serde_json::to_value(&wire).unwrap();
6147 assert_eq!(wire_json["sessionId"], "sess-1");
6148 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6149 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6150 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6151 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
6152 assert_eq!(wire_json["enableSessionTelemetry"], false);
6153 assert_eq!(wire_json["reasoningSummary"], "detailed");
6154 assert_eq!(wire_json["remoteSession"], "on");
6155 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6156
6157 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6159 .into_wire()
6160 .expect("default resume has no duplicate handlers");
6161 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6162 assert!(empty_json.get("reasoningSummary").is_none());
6163 assert!(empty_json.get("remoteSession").is_none());
6164 assert!(
6165 empty_json
6166 .get("enableOnDemandInstructionDiscovery")
6167 .is_none()
6168 );
6169 }
6170
6171 #[test]
6172 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
6173 use std::path::PathBuf;
6174
6175 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6176 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6177 cfg.large_output = Some(
6178 LargeToolOutputConfig::new()
6179 .with_enabled(false)
6180 .with_max_size_bytes(2048)
6181 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6182 );
6183
6184 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6185 let wire_json = serde_json::to_value(&wire).unwrap();
6186 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6187 assert_eq!(wire_json["largeOutput"]["enabled"], false);
6188 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6189 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6190
6191 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6192 .into_wire()
6193 .expect("default resume has no duplicate handlers");
6194 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6195 assert!(empty_json.get("pluginDirectories").is_none());
6196 assert!(empty_json.get("largeOutput").is_none());
6197 }
6198
6199 #[test]
6200 fn session_config_builder_composes() {
6201 use indexmap::IndexMap;
6202
6203 let cfg = SessionConfig::default()
6204 .with_session_id(SessionId::from("sess-1"))
6205 .with_model("claude-sonnet-4")
6206 .with_client_name("test-app")
6207 .with_reasoning_effort("medium")
6208 .with_reasoning_summary(ReasoningSummary::Concise)
6209 .with_context_tier("long_context")
6210 .with_streaming(true)
6211 .with_tools([Tool::new("greet")])
6212 .with_available_tools(["bash", "view"])
6213 .with_excluded_tools(["dangerous"])
6214 .with_mcp_servers(IndexMap::new())
6215 .with_mcp_oauth_token_storage("persistent")
6216 .with_enable_config_discovery(true)
6217 .with_enable_on_demand_instruction_discovery(true)
6218 .with_skill_directories([PathBuf::from("/tmp/skills")])
6219 .with_disabled_skills(["broken-skill"])
6220 .with_agent("researcher")
6221 .with_config_directory(PathBuf::from("/tmp/config"))
6222 .with_working_directory(PathBuf::from("/tmp/work"))
6223 .with_github_token("ghp_test")
6224 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6225 .with_enable_session_telemetry(false)
6226 .with_include_sub_agent_streaming_events(false)
6227 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6228
6229 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
6230 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
6231 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6232 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
6233 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
6234 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
6235 assert_eq!(cfg.streaming, Some(true));
6236 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6237 assert_eq!(
6238 cfg.available_tools.as_deref(),
6239 Some(&["bash".to_string(), "view".to_string()][..])
6240 );
6241 assert_eq!(
6242 cfg.excluded_tools.as_deref(),
6243 Some(&["dangerous".to_string()][..])
6244 );
6245 assert!(cfg.mcp_servers.is_some());
6246 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6247 assert_eq!(cfg.enable_config_discovery, Some(true));
6248 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
6249 assert_eq!(
6250 cfg.skill_directories.as_deref(),
6251 Some(&[PathBuf::from("/tmp/skills")][..])
6252 );
6253 assert_eq!(
6254 cfg.disabled_skills.as_deref(),
6255 Some(&["broken-skill".to_string()][..])
6256 );
6257 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6258 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6259 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6260 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6261 assert_eq!(
6262 cfg.capi,
6263 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6264 );
6265 assert_eq!(cfg.enable_session_telemetry, Some(false));
6266 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
6267 assert_eq!(
6268 cfg.extension_info,
6269 Some(ExtensionInfo::new("github-app", "counter"))
6270 );
6271 }
6272
6273 #[test]
6274 fn resume_session_config_builder_composes() {
6275 use indexmap::IndexMap;
6276
6277 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
6278 .with_client_name("test-app")
6279 .with_reasoning_summary(ReasoningSummary::None)
6280 .with_context_tier("default")
6281 .with_streaming(true)
6282 .with_tools([Tool::new("greet")])
6283 .with_available_tools(["bash", "view"])
6284 .with_excluded_tools(["dangerous"])
6285 .with_mcp_servers(IndexMap::new())
6286 .with_mcp_oauth_token_storage("persistent")
6287 .with_enable_config_discovery(true)
6288 .with_enable_on_demand_instruction_discovery(false)
6289 .with_skill_directories([PathBuf::from("/tmp/skills")])
6290 .with_disabled_skills(["broken-skill"])
6291 .with_agent("researcher")
6292 .with_config_directory(PathBuf::from("/tmp/config"))
6293 .with_working_directory(PathBuf::from("/tmp/work"))
6294 .with_github_token("ghp_test")
6295 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6296 .with_enable_session_telemetry(false)
6297 .with_include_sub_agent_streaming_events(true)
6298 .with_suppress_resume_event(true)
6299 .with_continue_pending_work(true)
6300 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6301
6302 assert_eq!(cfg.session_id.as_str(), "sess-2");
6303 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6304 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
6305 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
6306 assert_eq!(cfg.streaming, Some(true));
6307 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6308 assert_eq!(
6309 cfg.available_tools.as_deref(),
6310 Some(&["bash".to_string(), "view".to_string()][..])
6311 );
6312 assert_eq!(
6313 cfg.excluded_tools.as_deref(),
6314 Some(&["dangerous".to_string()][..])
6315 );
6316 assert!(cfg.mcp_servers.is_some());
6317 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6318 assert_eq!(cfg.enable_config_discovery, Some(true));
6319 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
6320 assert_eq!(
6321 cfg.skill_directories.as_deref(),
6322 Some(&[PathBuf::from("/tmp/skills")][..])
6323 );
6324 assert_eq!(
6325 cfg.disabled_skills.as_deref(),
6326 Some(&["broken-skill".to_string()][..])
6327 );
6328 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6329 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6330 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6331 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6332 assert_eq!(
6333 cfg.capi,
6334 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6335 );
6336 assert_eq!(cfg.enable_session_telemetry, Some(false));
6337 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
6338 assert_eq!(cfg.suppress_resume_event, Some(true));
6339 assert_eq!(cfg.continue_pending_work, Some(true));
6340 assert_eq!(
6341 cfg.extension_info,
6342 Some(ExtensionInfo::new("github-app", "counter"))
6343 );
6344 }
6345
6346 #[test]
6350 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
6351 let cfg =
6352 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
6353 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6354 let json = serde_json::to_value(&wire).unwrap();
6355 assert_eq!(json["continuePendingWork"], true);
6356
6357 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6359 .into_wire()
6360 .expect("no duplicate handlers");
6361 let json = serde_json::to_value(&wire).unwrap();
6362 assert!(json.get("continuePendingWork").is_none());
6363 }
6364
6365 #[test]
6369 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
6370 let cfg =
6371 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
6372 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6373 let json = serde_json::to_value(&wire).unwrap();
6374 assert_eq!(json["disableResume"], true);
6375 assert!(json.get("suppressResumeEvent").is_none());
6376 }
6377
6378 #[test]
6381 fn session_config_serializes_instruction_directories_to_camel_case() {
6382 let cfg =
6383 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
6384 let (wire, _) = cfg
6385 .into_wire(Some(SessionId::from("instr-on")))
6386 .expect("no duplicate handlers");
6387 let json = serde_json::to_value(&wire).unwrap();
6388 assert_eq!(
6389 json["instructionDirectories"],
6390 serde_json::json!(["/tmp/instr"])
6391 );
6392
6393 let (wire, _) = SessionConfig::default()
6395 .into_wire(Some(SessionId::from("instr-off")))
6396 .expect("no duplicate handlers");
6397 let json = serde_json::to_value(&wire).unwrap();
6398 assert!(json.get("instructionDirectories").is_none());
6399 }
6400
6401 #[test]
6404 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
6405 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
6406 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
6407 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6408 let json = serde_json::to_value(&wire).unwrap();
6409 assert_eq!(
6410 json["instructionDirectories"],
6411 serde_json::json!(["/tmp/instr"])
6412 );
6413
6414 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6415 .into_wire()
6416 .expect("no duplicate handlers");
6417 let json = serde_json::to_value(&wire).unwrap();
6418 assert!(json.get("instructionDirectories").is_none());
6419 }
6420
6421 #[test]
6422 fn custom_agent_config_builder_composes() {
6423 use indexmap::IndexMap;
6424
6425 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
6426 .with_display_name("Research Assistant")
6427 .with_description("Investigates technical questions.")
6428 .with_tools(["bash", "view"])
6429 .with_mcp_servers(IndexMap::new())
6430 .with_infer(true)
6431 .with_skills(["rust-coding-skill"]);
6432
6433 assert_eq!(cfg.name, "researcher");
6434 assert_eq!(cfg.prompt, "You are a research assistant.");
6435 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
6436 assert_eq!(
6437 cfg.description.as_deref(),
6438 Some("Investigates technical questions.")
6439 );
6440 assert_eq!(
6441 cfg.tools.as_deref(),
6442 Some(&["bash".to_string(), "view".to_string()][..])
6443 );
6444 assert!(cfg.mcp_servers.is_some());
6445 assert_eq!(cfg.infer, Some(true));
6446 assert_eq!(
6447 cfg.skills.as_deref(),
6448 Some(&["rust-coding-skill".to_string()][..])
6449 );
6450 }
6451
6452 #[test]
6453 fn mcp_servers_serialize_in_insertion_order() {
6454 use indexmap::IndexMap;
6455
6456 let order = [
6462 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
6463 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
6464 ];
6465 let mut servers = IndexMap::new();
6466 for name in order {
6467 servers.insert(
6468 name.to_string(),
6469 McpServerConfig::Stdio(McpStdioServerConfig {
6470 command: "run".to_string(),
6471 ..Default::default()
6472 }),
6473 );
6474 }
6475
6476 let (wire, _runtime) = SessionConfig::default()
6477 .with_mcp_servers(servers)
6478 .into_wire(None)
6479 .expect("into_wire should succeed");
6480 let json = serde_json::to_string(&wire).expect("serialize wire");
6481
6482 let positions: Vec<usize> = order
6483 .iter()
6484 .map(|name| {
6485 json.find(&format!("\"{name}\""))
6486 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
6487 })
6488 .collect();
6489 let mut ascending = positions.clone();
6490 ascending.sort_unstable();
6491 assert_eq!(
6492 positions, ascending,
6493 "mcp server keys must serialize in insertion order: {json}"
6494 );
6495 }
6496
6497 #[test]
6498 fn infinite_session_config_builder_composes() {
6499 let cfg = InfiniteSessionConfig::new()
6500 .with_enabled(true)
6501 .with_background_compaction_threshold(0.75)
6502 .with_buffer_exhaustion_threshold(0.92);
6503
6504 assert_eq!(cfg.enabled, Some(true));
6505 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
6506 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
6507 }
6508
6509 #[test]
6510 fn provider_config_builder_composes() {
6511 use std::collections::HashMap;
6512
6513 let mut headers = HashMap::new();
6514 headers.insert("X-Custom".to_string(), "value".to_string());
6515
6516 let cfg = ProviderConfig::new("https://api.example.com")
6517 .with_provider_type("openai")
6518 .with_wire_api("completions")
6519 .with_transport("websockets")
6520 .with_api_key("sk-test")
6521 .with_bearer_token("bearer-test")
6522 .with_headers(headers)
6523 .with_model_id("gpt-4")
6524 .with_wire_model("azure-gpt-4-deployment")
6525 .with_max_prompt_tokens(8192)
6526 .with_max_output_tokens(2048);
6527
6528 assert_eq!(cfg.base_url, "https://api.example.com");
6529 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
6530 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
6531 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
6532 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
6533 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
6534 assert_eq!(
6535 cfg.headers
6536 .as_ref()
6537 .and_then(|h| h.get("X-Custom"))
6538 .map(String::as_str),
6539 Some("value"),
6540 );
6541 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
6542 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
6543 assert_eq!(cfg.max_prompt_tokens, Some(8192));
6544 assert_eq!(cfg.max_output_tokens, Some(2048));
6545
6546 let wire = serde_json::to_value(&cfg).unwrap();
6548 assert_eq!(wire["modelId"], "gpt-4");
6549 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
6550 assert_eq!(wire["maxPromptTokens"], 8192);
6551 assert_eq!(wire["maxOutputTokens"], 2048);
6552
6553 let unset = ProviderConfig::new("https://api.example.com");
6554 let wire_unset = serde_json::to_value(&unset).unwrap();
6555 assert!(wire_unset.get("modelId").is_none());
6556 assert!(wire_unset.get("wireModel").is_none());
6557 assert!(wire_unset.get("maxPromptTokens").is_none());
6558 assert!(wire_unset.get("maxOutputTokens").is_none());
6559 }
6560
6561 #[test]
6562 fn capi_session_options_builder_composes_and_serializes() {
6563 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
6564
6565 assert_eq!(cfg.enable_web_socket_responses, Some(false));
6566
6567 let wire = serde_json::to_value(&cfg).unwrap();
6568 assert_eq!(
6569 wire,
6570 serde_json::json!({ "enableWebSocketResponses": false })
6571 );
6572
6573 let unset = CapiSessionOptions::new();
6574 let wire_unset = serde_json::to_value(&unset).unwrap();
6575 assert!(wire_unset.get("enableWebSocketResponses").is_none());
6576 }
6577
6578 #[test]
6579 fn session_config_with_capi_serializes() {
6580 let (wire, _) = SessionConfig::default()
6581 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6582 .into_wire(Some(SessionId::from("capi-create")))
6583 .expect("no duplicate handlers");
6584 let json = serde_json::to_value(&wire).unwrap();
6585 assert_eq!(
6586 json["capi"],
6587 serde_json::json!({ "enableWebSocketResponses": false })
6588 );
6589
6590 let (empty_wire, _) = SessionConfig::default()
6591 .into_wire(Some(SessionId::from("capi-create-unset")))
6592 .expect("no duplicate handlers");
6593 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6594 assert!(empty_json.get("capi").is_none());
6595 }
6596
6597 #[test]
6598 fn resume_session_config_with_capi_serializes() {
6599 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
6600 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6601 .into_wire()
6602 .expect("no duplicate handlers");
6603 let json = serde_json::to_value(&wire).unwrap();
6604 assert_eq!(
6605 json["capi"],
6606 serde_json::json!({ "enableWebSocketResponses": false })
6607 );
6608
6609 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
6610 .into_wire()
6611 .expect("no duplicate handlers");
6612 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6613 assert!(empty_json.get("capi").is_none());
6614 }
6615
6616 #[test]
6617 fn system_message_config_builder_composes() {
6618 use std::collections::HashMap;
6619
6620 let cfg = SystemMessageConfig::new()
6621 .with_mode("replace")
6622 .with_content("Custom system message.")
6623 .with_sections(HashMap::new());
6624
6625 assert_eq!(cfg.mode.as_deref(), Some("replace"));
6626 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
6627 assert!(cfg.sections.is_some());
6628 }
6629
6630 #[test]
6631 fn delivery_mode_serializes_to_kebab_case_strings() {
6632 assert_eq!(
6633 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
6634 "\"enqueue\""
6635 );
6636 assert_eq!(
6637 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
6638 "\"immediate\""
6639 );
6640 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
6641 assert_eq!(parsed, DeliveryMode::Immediate);
6642 }
6643
6644 #[test]
6645 fn agent_mode_serializes_to_kebab_case_strings() {
6646 assert_eq!(
6647 serde_json::to_string(&AgentMode::Interactive).unwrap(),
6648 "\"interactive\""
6649 );
6650 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
6651 assert_eq!(
6652 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
6653 "\"autopilot\""
6654 );
6655 assert_eq!(
6656 serde_json::to_string(&AgentMode::Shell).unwrap(),
6657 "\"shell\""
6658 );
6659 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
6660 assert_eq!(parsed, AgentMode::Plan);
6661 }
6662
6663 #[test]
6664 fn connection_state_distinguishes_variants() {
6665 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
6668 }
6669
6670 #[test]
6676 fn session_event_round_trips_agent_id_on_envelope() {
6677 let wire = json!({
6678 "id": "evt-1",
6679 "timestamp": "2026-04-30T12:00:00Z",
6680 "parentId": null,
6681 "agentId": "sub-agent-42",
6682 "type": "assistant.message",
6683 "data": { "message": "hi" }
6684 });
6685
6686 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
6687 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6688
6689 let roundtripped = serde_json::to_value(&event).unwrap();
6691 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6692
6693 let main_agent_event: SessionEvent = serde_json::from_value(json!({
6695 "id": "evt-2",
6696 "timestamp": "2026-04-30T12:00:01Z",
6697 "parentId": null,
6698 "type": "session.idle",
6699 "data": {}
6700 }))
6701 .unwrap();
6702 assert!(main_agent_event.agent_id.is_none());
6703 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
6704 assert!(roundtripped.get("agentId").is_none());
6705 }
6706
6707 #[test]
6709 fn typed_session_event_round_trips_agent_id_on_envelope() {
6710 let wire = json!({
6711 "id": "evt-1",
6712 "timestamp": "2026-04-30T12:00:00Z",
6713 "parentId": null,
6714 "agentId": "sub-agent-42",
6715 "type": "session.idle",
6716 "data": {}
6717 });
6718
6719 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
6720 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6721
6722 let roundtripped = serde_json::to_value(&event).unwrap();
6723 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6724 }
6725
6726 #[test]
6727 fn connection_state_variants_compile() {
6728 let _ = ConnectionState::Disconnected;
6732 let _ = ConnectionState::Connecting;
6733 let _ = ConnectionState::Connected;
6734 let _ = ConnectionState::Error;
6735 }
6736
6737 #[test]
6738 fn deserializes_runtime_attachment_variants() {
6739 let attachments: Vec<Attachment> = serde_json::from_value(json!([
6740 {
6741 "type": "file",
6742 "path": "/tmp/file.rs",
6743 "displayName": "file.rs",
6744 "lineRange": { "start": 7, "end": 12 }
6745 },
6746 {
6747 "type": "directory",
6748 "path": "/tmp/project",
6749 "displayName": "project"
6750 },
6751 {
6752 "type": "selection",
6753 "filePath": "/tmp/lib.rs",
6754 "displayName": "lib.rs",
6755 "text": "fn main() {}",
6756 "selection": {
6757 "start": { "line": 1, "character": 2 },
6758 "end": { "line": 3, "character": 4 }
6759 }
6760 },
6761 {
6762 "type": "blob",
6763 "data": "Zm9v",
6764 "mimeType": "image/png",
6765 "displayName": "image.png"
6766 },
6767 {
6768 "type": "github_reference",
6769 "number": 42,
6770 "title": "Fix rendering",
6771 "referenceType": "issue",
6772 "state": "open",
6773 "url": "https://github.com/example/repo/issues/42"
6774 }
6775 ]))
6776 .expect("attachments should deserialize");
6777
6778 assert_eq!(attachments.len(), 5);
6779 assert!(matches!(
6780 &attachments[0],
6781 Attachment::File {
6782 path,
6783 display_name,
6784 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
6785 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
6786 ));
6787 assert!(matches!(
6788 &attachments[1],
6789 Attachment::Directory { path, display_name }
6790 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
6791 ));
6792 assert!(matches!(
6793 &attachments[2],
6794 Attachment::Selection {
6795 file_path,
6796 display_name,
6797 selection:
6798 AttachmentSelectionRange {
6799 start: AttachmentSelectionPosition { line: 1, character: 2 },
6800 end: AttachmentSelectionPosition { line: 3, character: 4 },
6801 },
6802 ..
6803 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
6804 ));
6805 assert!(matches!(
6806 &attachments[3],
6807 Attachment::Blob {
6808 data,
6809 mime_type,
6810 display_name,
6811 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
6812 ));
6813 assert!(matches!(
6814 &attachments[4],
6815 Attachment::GitHubReference {
6816 number: 42,
6817 title,
6818 reference_type: GitHubReferenceType::Issue,
6819 state,
6820 url,
6821 } if title == "Fix rendering"
6822 && state == "open"
6823 && url == "https://github.com/example/repo/issues/42"
6824 ));
6825 }
6826
6827 #[test]
6828 fn ensures_display_names_for_variants_that_support_them() {
6829 let mut attachments = vec![
6830 Attachment::File {
6831 path: PathBuf::from("/tmp/file.rs"),
6832 display_name: None,
6833 line_range: None,
6834 },
6835 Attachment::Selection {
6836 file_path: PathBuf::from("/tmp/src/lib.rs"),
6837 display_name: None,
6838 text: "fn main() {}".to_string(),
6839 selection: AttachmentSelectionRange {
6840 start: AttachmentSelectionPosition {
6841 line: 0,
6842 character: 0,
6843 },
6844 end: AttachmentSelectionPosition {
6845 line: 0,
6846 character: 10,
6847 },
6848 },
6849 },
6850 Attachment::Blob {
6851 data: "Zm9v".to_string(),
6852 mime_type: "image/png".to_string(),
6853 display_name: None,
6854 },
6855 Attachment::GitHubReference {
6856 number: 7,
6857 title: "Track regressions".to_string(),
6858 reference_type: GitHubReferenceType::Issue,
6859 state: "open".to_string(),
6860 url: "https://example.com/issues/7".to_string(),
6861 },
6862 ];
6863
6864 ensure_attachment_display_names(&mut attachments);
6865
6866 assert_eq!(attachments[0].display_name(), Some("file.rs"));
6867 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
6868 assert_eq!(attachments[2].display_name(), Some("attachment"));
6869 assert_eq!(attachments[3].display_name(), None);
6870 assert_eq!(
6871 attachments[3].label(),
6872 Some("Track regressions".to_string())
6873 );
6874 }
6875
6876 #[test]
6877 fn github_anchored_attachment_variants_round_trip() {
6878 let cases = vec![
6879 (
6880 "github_commit",
6881 json!({
6882 "type": "github_commit",
6883 "message": "Fix the thing",
6884 "oid": "abc123",
6885 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
6886 "url": "https://github.com/octocat/repo/commit/abc123"
6887 }),
6888 ),
6889 (
6890 "github_release",
6891 json!({
6892 "type": "github_release",
6893 "name": "v1.2.3",
6894 "repo": { "name": "repo", "owner": "octocat" },
6895 "tagName": "v1.2.3",
6896 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
6897 }),
6898 ),
6899 (
6900 "github_actions_job",
6901 json!({
6902 "type": "github_actions_job",
6903 "conclusion": "failure",
6904 "jobId": 99,
6905 "jobName": "build",
6906 "repo": { "name": "repo", "owner": "octocat" },
6907 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
6908 "workflowName": "CI"
6909 }),
6910 ),
6911 (
6912 "github_repository",
6913 json!({
6914 "type": "github_repository",
6915 "description": "An example repository",
6916 "ref": "main",
6917 "repo": { "name": "repo", "owner": "octocat" },
6918 "url": "https://github.com/octocat/repo"
6919 }),
6920 ),
6921 (
6922 "github_file_diff",
6923 json!({
6924 "type": "github_file_diff",
6925 "base": {
6926 "path": "src/lib.rs",
6927 "ref": "main",
6928 "repo": { "name": "repo", "owner": "octocat" }
6929 },
6930 "head": {
6931 "path": "src/lib.rs",
6932 "ref": "feature",
6933 "repo": { "name": "repo", "owner": "octocat" }
6934 },
6935 "url": "https://github.com/octocat/repo/compare/main...feature"
6936 }),
6937 ),
6938 (
6939 "github_tree_comparison",
6940 json!({
6941 "type": "github_tree_comparison",
6942 "base": {
6943 "repo": { "name": "repo", "owner": "octocat" },
6944 "revision": "main"
6945 },
6946 "head": {
6947 "repo": { "name": "repo", "owner": "octocat" },
6948 "revision": "feature"
6949 },
6950 "url": "https://github.com/octocat/repo/compare/main...feature"
6951 }),
6952 ),
6953 (
6954 "github_url",
6955 json!({
6956 "type": "github_url",
6957 "url": "https://github.com/octocat/repo/wiki"
6958 }),
6959 ),
6960 (
6961 "github_file",
6962 json!({
6963 "type": "github_file",
6964 "path": "src/main.rs",
6965 "ref": "main",
6966 "repo": { "name": "repo", "owner": "octocat" },
6967 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
6968 }),
6969 ),
6970 (
6971 "github_snippet",
6972 json!({
6973 "type": "github_snippet",
6974 "lineRange": { "start": 10, "end": 20 },
6975 "path": "src/main.rs",
6976 "ref": "main",
6977 "repo": { "name": "repo", "owner": "octocat" },
6978 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
6979 }),
6980 ),
6981 ];
6982
6983 for (expected_type, input) in cases {
6984 let attachment: Attachment = serde_json::from_value(input.clone())
6985 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
6986
6987 let serialized_string = serde_json::to_string(&attachment)
6992 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
6993
6994 assert_eq!(
6996 serialized_string.matches("\"type\":").count(),
6997 1,
6998 "{expected_type} must serialize a single `type` key"
6999 );
7000
7001 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
7002 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
7003 assert_eq!(
7004 serialized.get("type").and_then(|value| value.as_str()),
7005 Some(expected_type),
7006 "{expected_type} must serialize the correct discriminator"
7007 );
7008
7009 assert_eq!(
7011 serialized, input,
7012 "{expected_type} should round-trip without data loss"
7013 );
7014 let reparsed: Attachment = serde_json::from_value(serialized)
7015 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
7016 assert_eq!(
7017 reparsed, attachment,
7018 "{expected_type} should re-deserialize to the same value"
7019 );
7020 }
7021 }
7022}
7023
7024#[cfg(test)]
7025mod permission_builder_tests {
7026 use std::sync::Arc;
7027
7028 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
7029 use crate::permission;
7030 use crate::types::{
7031 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
7032 SessionId,
7033 };
7034
7035 fn data() -> PermissionRequestData {
7036 PermissionRequestData {
7037 extra: serde_json::json!({"tool": "shell"}),
7038 ..Default::default()
7039 }
7040 }
7041
7042 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7045 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7046 }
7047
7048 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7049 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7050 }
7051
7052 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
7053 handler
7054 .handle(SessionId::from("s1"), RequestId::new("1"), data())
7055 .await
7056 }
7057
7058 #[tokio::test]
7059 async fn approve_all_with_handler_present_approves() {
7060 let cfg = SessionConfig::default()
7061 .with_permission_handler(Arc::new(ApproveAllHandler))
7062 .approve_all_permissions();
7063 let h = resolve_create(cfg).expect("policy + handler yields handler");
7064 assert!(matches!(
7065 dispatch(&h).await,
7066 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7067 ));
7068 }
7069
7070 #[tokio::test]
7071 async fn approve_all_standalone_produces_handler() {
7072 let cfg = SessionConfig::default().approve_all_permissions();
7073 let h = resolve_create(cfg).expect("policy alone yields handler");
7074 assert!(matches!(
7075 dispatch(&h).await,
7076 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7077 ));
7078 }
7079
7080 #[tokio::test]
7083 async fn approve_all_is_order_independent() {
7084 let a = SessionConfig::default()
7085 .with_permission_handler(Arc::new(ApproveAllHandler))
7086 .approve_all_permissions();
7087 let b = SessionConfig::default()
7088 .approve_all_permissions()
7089 .with_permission_handler(Arc::new(ApproveAllHandler));
7090 let ha = resolve_create(a).unwrap();
7091 let hb = resolve_create(b).unwrap();
7092 assert!(matches!(
7093 dispatch(&ha).await,
7094 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7095 ));
7096 assert!(matches!(
7097 dispatch(&hb).await,
7098 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7099 ));
7100 }
7101
7102 #[tokio::test]
7103 async fn deny_all_is_order_independent() {
7104 let a = SessionConfig::default()
7105 .with_permission_handler(Arc::new(ApproveAllHandler))
7106 .deny_all_permissions();
7107 let b = SessionConfig::default()
7108 .deny_all_permissions()
7109 .with_permission_handler(Arc::new(ApproveAllHandler));
7110 let ha = resolve_create(a).unwrap();
7111 let hb = resolve_create(b).unwrap();
7112 assert!(matches!(
7113 dispatch(&ha).await,
7114 PermissionResult::Decision(PermissionDecision::Reject(_))
7115 ));
7116 assert!(matches!(
7117 dispatch(&hb).await,
7118 PermissionResult::Decision(PermissionDecision::Reject(_))
7119 ));
7120 }
7121
7122 #[tokio::test]
7123 async fn approve_permissions_if_consults_predicate() {
7124 let cfg = SessionConfig::default().approve_permissions_if(|d| {
7125 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7126 });
7127 let h = resolve_create(cfg).unwrap();
7128 assert!(matches!(
7129 dispatch(&h).await,
7130 PermissionResult::Decision(PermissionDecision::Reject(_))
7131 ));
7132 }
7133
7134 #[tokio::test]
7135 async fn approve_permissions_if_is_order_independent() {
7136 let predicate = |d: &PermissionRequestData| {
7137 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7138 };
7139 let a = SessionConfig::default()
7140 .with_permission_handler(Arc::new(ApproveAllHandler))
7141 .approve_permissions_if(predicate);
7142 let b = SessionConfig::default()
7143 .approve_permissions_if(predicate)
7144 .with_permission_handler(Arc::new(ApproveAllHandler));
7145 let ha = resolve_create(a).unwrap();
7146 let hb = resolve_create(b).unwrap();
7147 assert!(matches!(
7148 dispatch(&ha).await,
7149 PermissionResult::Decision(PermissionDecision::Reject(_))
7150 ));
7151 assert!(matches!(
7152 dispatch(&hb).await,
7153 PermissionResult::Decision(PermissionDecision::Reject(_))
7154 ));
7155 }
7156
7157 #[tokio::test]
7158 async fn resume_session_config_approve_all_works() {
7159 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
7160 .with_permission_handler(Arc::new(ApproveAllHandler))
7161 .approve_all_permissions();
7162 let h = resolve_resume(cfg).unwrap();
7163 assert!(matches!(
7164 dispatch(&h).await,
7165 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7166 ));
7167 }
7168
7169 #[tokio::test]
7170 async fn resume_session_config_approve_all_is_order_independent() {
7171 let a = ResumeSessionConfig::new(SessionId::from("s1"))
7172 .with_permission_handler(Arc::new(ApproveAllHandler))
7173 .approve_all_permissions();
7174 let b = ResumeSessionConfig::new(SessionId::from("s1"))
7175 .approve_all_permissions()
7176 .with_permission_handler(Arc::new(ApproveAllHandler));
7177 let ha = resolve_resume(a).unwrap();
7178 let hb = resolve_resume(b).unwrap();
7179 assert!(matches!(
7180 dispatch(&ha).await,
7181 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7182 ));
7183 assert!(matches!(
7184 dispatch(&hb).await,
7185 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7186 ));
7187 }
7188}