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>,
1791 pub skip_embedding_retrieval: Option<bool>,
1793 pub embedding_cache_storage: Option<String>,
1796 pub organization_custom_instructions: Option<String>,
1798 pub enable_on_demand_instruction_discovery: Option<bool>,
1800 pub enable_file_hooks: Option<bool>,
1802 pub enable_host_git_operations: Option<bool>,
1804 pub enable_session_store: Option<bool>,
1806 pub enable_skills: Option<bool>,
1808 pub enable_mcp_apps: Option<bool>,
1835 pub skill_directories: Option<Vec<PathBuf>>,
1837 pub instruction_directories: Option<Vec<PathBuf>>,
1840 pub plugin_directories: Option<Vec<PathBuf>>,
1842 pub large_output: Option<LargeToolOutputConfig>,
1844 pub tool_search: Option<ToolSearchConfig>,
1848 pub disabled_skills: Option<Vec<String>>,
1851 pub hooks: Option<bool>,
1855 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1857 pub default_agent: Option<DefaultAgentConfig>,
1861 pub agent: Option<String>,
1864 pub infinite_sessions: Option<InfiniteSessionConfig>,
1867 pub provider: Option<ProviderConfig>,
1871 pub capi: Option<CapiSessionOptions>,
1877 pub providers: Option<Vec<NamedProviderConfig>>,
1884 pub models: Option<Vec<ProviderModelConfig>>,
1890 pub enable_session_telemetry: Option<bool>,
1898 pub enable_citations: Option<bool>,
1900 pub session_limits: Option<SessionLimitsConfig>,
1902 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1905 pub memory: Option<MemoryConfiguration>,
1907 pub config_directory: Option<PathBuf>,
1910 pub working_directory: Option<PathBuf>,
1913 pub github_token: Option<String>,
1919 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
1925 pub cloud: Option<CloudSessionOptions>,
1928 pub include_sub_agent_streaming_events: Option<bool>,
1932 pub commands: Option<Vec<CommandDefinition>>,
1936 #[doc(hidden)]
1943 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
1944 pub enable_managed_settings: Option<bool>,
1951 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1956 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
1960 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
1963 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
1966 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
1970 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
1973 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
1976 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1980 pub(crate) permission_policy: Option<crate::permission::Policy>,
1984 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
1989 pub skip_custom_instructions: Option<bool>,
1993 pub custom_agents_local_only: Option<bool>,
1997 pub coauthor_enabled: Option<bool>,
2001 pub manage_schedule_enabled: Option<bool>,
2005}
2006
2007impl std::fmt::Debug for SessionConfig {
2008 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2009 f.debug_struct("SessionConfig")
2010 .field("session_id", &self.session_id)
2011 .field("model", &self.model)
2012 .field("client_name", &self.client_name)
2013 .field("reasoning_effort", &self.reasoning_effort)
2014 .field("reasoning_summary", &self.reasoning_summary)
2015 .field("context_tier", &self.context_tier)
2016 .field("streaming", &self.streaming)
2017 .field("system_message", &self.system_message)
2018 .field("tools", &self.tools)
2019 .field("canvases", &self.canvases)
2020 .field(
2021 "canvas_handler",
2022 &self.canvas_handler.as_ref().map(|_| "<set>"),
2023 )
2024 .field("request_canvas_renderer", &self.request_canvas_renderer)
2025 .field("request_extensions", &self.request_extensions)
2026 .field("extension_sdk_path", &self.extension_sdk_path)
2027 .field("extension_info", &self.extension_info)
2028 .field("canvas_provider", &self.canvas_provider)
2029 .field("available_tools", &self.available_tools)
2030 .field("excluded_tools", &self.excluded_tools)
2031 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2032 .field("mcp_servers", &self.mcp_servers)
2033 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2034 .field("embedding_cache_storage", &self.embedding_cache_storage)
2035 .field("enable_config_discovery", &self.enable_config_discovery)
2036 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2037 .field(
2038 "organization_custom_instructions",
2039 &self
2040 .organization_custom_instructions
2041 .as_ref()
2042 .map(|_| "<redacted>"),
2043 )
2044 .field(
2045 "enable_on_demand_instruction_discovery",
2046 &self.enable_on_demand_instruction_discovery,
2047 )
2048 .field("enable_file_hooks", &self.enable_file_hooks)
2049 .field(
2050 "enable_host_git_operations",
2051 &self.enable_host_git_operations,
2052 )
2053 .field("enable_session_store", &self.enable_session_store)
2054 .field("enable_skills", &self.enable_skills)
2055 .field("enable_mcp_apps", &self.enable_mcp_apps)
2056 .field("skill_directories", &self.skill_directories)
2057 .field("instruction_directories", &self.instruction_directories)
2058 .field("plugin_directories", &self.plugin_directories)
2059 .field("large_output", &self.large_output)
2060 .field("tool_search", &self.tool_search)
2061 .field("disabled_skills", &self.disabled_skills)
2062 .field("hooks", &self.hooks)
2063 .field("custom_agents", &self.custom_agents)
2064 .field("default_agent", &self.default_agent)
2065 .field("agent", &self.agent)
2066 .field("infinite_sessions", &self.infinite_sessions)
2067 .field("provider", &self.provider)
2068 .field("capi", &self.capi)
2069 .field("enable_session_telemetry", &self.enable_session_telemetry)
2070 .field("enable_citations", &self.enable_citations)
2071 .field("session_limits", &self.session_limits)
2072 .field("model_capabilities", &self.model_capabilities)
2073 .field("memory", &self.memory)
2074 .field("config_directory", &self.config_directory)
2075 .field("working_directory", &self.working_directory)
2076 .field(
2077 "github_token",
2078 &self.github_token.as_ref().map(|_| "<redacted>"),
2079 )
2080 .field("remote_session", &self.remote_session)
2081 .field("cloud", &self.cloud)
2082 .field(
2083 "include_sub_agent_streaming_events",
2084 &self.include_sub_agent_streaming_events,
2085 )
2086 .field("commands", &self.commands)
2087 .field("exp_assignments", &self.exp_assignments)
2088 .field("enable_managed_settings", &self.enable_managed_settings)
2089 .field(
2090 "session_fs_provider",
2091 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2092 )
2093 .field(
2094 "permission_handler",
2095 &self.permission_handler.as_ref().map(|_| "<set>"),
2096 )
2097 .field(
2098 "elicitation_handler",
2099 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2100 )
2101 .field(
2102 "mcp_auth_handler",
2103 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2104 )
2105 .field(
2106 "user_input_handler",
2107 &self.user_input_handler.as_ref().map(|_| "<set>"),
2108 )
2109 .field(
2110 "exit_plan_mode_handler",
2111 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2112 )
2113 .field(
2114 "auto_mode_switch_handler",
2115 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2116 )
2117 .field(
2118 "hooks_handler",
2119 &self.hooks_handler.as_ref().map(|_| "<set>"),
2120 )
2121 .field(
2122 "system_message_transform",
2123 &self.system_message_transform.as_ref().map(|_| "<set>"),
2124 )
2125 .finish()
2126 }
2127}
2128
2129impl Default for SessionConfig {
2130 fn default() -> Self {
2136 Self {
2137 session_id: None,
2138 model: None,
2139 client_name: None,
2140 reasoning_effort: None,
2141 reasoning_summary: None,
2142 context_tier: None,
2143 streaming: None,
2144 system_message: None,
2145 tools: None,
2146 canvases: None,
2147 canvas_handler: None,
2148 request_canvas_renderer: None,
2149 request_extensions: None,
2150 extension_sdk_path: None,
2151 extension_info: None,
2152 canvas_provider: None,
2153 available_tools: None,
2154 excluded_tools: None,
2155 excluded_builtin_agents: None,
2156 mcp_servers: None,
2157 mcp_oauth_token_storage: None,
2158 enable_config_discovery: None,
2159 skip_embedding_retrieval: None,
2160 organization_custom_instructions: None,
2161 enable_on_demand_instruction_discovery: None,
2162 enable_file_hooks: None,
2163 enable_host_git_operations: None,
2164 enable_session_store: None,
2165 enable_skills: None,
2166 embedding_cache_storage: None,
2167 enable_mcp_apps: None,
2168 skill_directories: None,
2169 instruction_directories: None,
2170 plugin_directories: None,
2171 large_output: None,
2172 tool_search: None,
2173 disabled_skills: None,
2174 hooks: None,
2175 custom_agents: None,
2176 default_agent: None,
2177 agent: None,
2178 infinite_sessions: None,
2179 provider: None,
2180 capi: None,
2181 providers: None,
2182 models: None,
2183 enable_session_telemetry: None,
2184 enable_citations: None,
2185 session_limits: None,
2186 model_capabilities: None,
2187 memory: None,
2188 config_directory: None,
2189 working_directory: None,
2190 github_token: None,
2191 remote_session: None,
2192 cloud: None,
2193 include_sub_agent_streaming_events: None,
2194 commands: None,
2195 exp_assignments: None,
2196 enable_managed_settings: None,
2197 session_fs_provider: None,
2198 permission_handler: None,
2199 elicitation_handler: None,
2200 mcp_auth_handler: None,
2201 user_input_handler: None,
2202 exit_plan_mode_handler: None,
2203 auto_mode_switch_handler: None,
2204 hooks_handler: None,
2205 permission_policy: None,
2206 system_message_transform: None,
2207 skip_custom_instructions: None,
2208 custom_agents_local_only: None,
2209 coauthor_enabled: None,
2210 manage_schedule_enabled: None,
2211 }
2212 }
2213}
2214
2215pub(crate) struct SessionConfigRuntime {
2221 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2222 pub permission_policy: Option<crate::permission::Policy>,
2223 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2224 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2225 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2226 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2227 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2228 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2229 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2230 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2231 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2232 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2233 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2234 pub commands: Option<Vec<CommandDefinition>>,
2235}
2236
2237impl SessionConfig {
2238 pub(crate) fn into_wire(
2250 mut self,
2251 session_id: Option<SessionId>,
2252 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2253 let permission_active =
2254 self.permission_handler.is_some() || self.permission_policy.is_some();
2255 let request_user_input = self.user_input_handler.is_some();
2256 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2257 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2258 let request_elicitation = self.elicitation_handler.is_some();
2259 let hooks_flag = self.hooks_handler.is_some();
2260
2261 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2262 if let Some(tools) = self.tools.as_mut() {
2263 for tool in tools.iter_mut() {
2264 if let Some(handler) = tool.handler.take()
2265 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2266 {
2267 return Err(crate::Error::with_message(
2268 crate::ErrorKind::InvalidConfig,
2269 format!("duplicate tool handler registered for name {:?}", tool.name),
2270 ));
2271 }
2272 }
2273 }
2274
2275 let wire_commands = self.commands.as_ref().map(|cmds| {
2276 cmds.iter()
2277 .map(|c| crate::wire::CommandWireDefinition {
2278 name: c.name.clone(),
2279 description: c.description.clone(),
2280 })
2281 .collect()
2282 });
2283 let wire_canvases = self.canvases.clone();
2284 let canvas_handler = self.canvas_handler.clone();
2285 let bearer_token_providers =
2286 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2287
2288 let wire = crate::wire::SessionCreateWire {
2289 session_id,
2290 model: self.model,
2291 client_name: self.client_name,
2292 reasoning_effort: self.reasoning_effort,
2293 reasoning_summary: self.reasoning_summary,
2294 context_tier: self.context_tier,
2295 streaming: self.streaming,
2296 system_message: self.system_message,
2297 tools: self.tools,
2298 canvases: wire_canvases,
2299 request_canvas_renderer: self.request_canvas_renderer,
2300 request_extensions: self.request_extensions,
2301 extension_sdk_path: self.extension_sdk_path,
2302 extension_info: self.extension_info,
2303 canvas_provider: self.canvas_provider,
2304 available_tools: self.available_tools,
2305 excluded_tools: self.excluded_tools,
2306 excluded_builtin_agents: self.excluded_builtin_agents,
2307 tool_filter_precedence: "excluded",
2308 mcp_servers: self.mcp_servers,
2309 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2310 embedding_cache_storage: self.embedding_cache_storage,
2311 env_value_mode: "direct",
2312 enable_config_discovery: self.enable_config_discovery,
2313 skip_embedding_retrieval: self.skip_embedding_retrieval,
2314 organization_custom_instructions: self.organization_custom_instructions,
2315 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2316 enable_file_hooks: self.enable_file_hooks,
2317 enable_host_git_operations: self.enable_host_git_operations,
2318 enable_session_store: self.enable_session_store,
2319 enable_skills: self.enable_skills,
2320 request_user_input,
2321 request_permission: permission_active,
2322 request_exit_plan_mode,
2323 request_auto_mode_switch,
2324 request_elicitation,
2325 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2326 hooks: hooks_flag,
2327 skill_directories: self.skill_directories,
2328 instruction_directories: self.instruction_directories,
2329 plugin_directories: self.plugin_directories,
2330 large_output: self.large_output,
2331 tool_search: self.tool_search,
2332 disabled_skills: self.disabled_skills,
2333 custom_agents: self.custom_agents,
2334 default_agent: self.default_agent,
2335 agent: self.agent,
2336 infinite_sessions: self.infinite_sessions,
2337 provider: self.provider,
2338 capi: self.capi,
2339 providers: self.providers,
2340 models: self.models,
2341 enable_session_telemetry: self.enable_session_telemetry,
2342 enable_citations: self.enable_citations,
2343 session_limits: self.session_limits,
2344 model_capabilities: self.model_capabilities,
2345 memory: self.memory,
2346 config_dir: self.config_directory,
2347 working_directory: self.working_directory,
2348 github_token: self.github_token,
2349 remote_session: self.remote_session,
2350 cloud: self.cloud,
2351 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2352 enable_github_telemetry_forwarding: None,
2353 commands: wire_commands,
2354 exp_assignments: self.exp_assignments,
2355 enable_managed_settings: self.enable_managed_settings,
2356 };
2357
2358 let runtime = SessionConfigRuntime {
2359 permission_handler: self.permission_handler,
2360 permission_policy: self.permission_policy,
2361 elicitation_handler: self.elicitation_handler,
2362 mcp_auth_handler: self.mcp_auth_handler,
2363 user_input_handler: self.user_input_handler,
2364 exit_plan_mode_handler: self.exit_plan_mode_handler,
2365 auto_mode_switch_handler: self.auto_mode_switch_handler,
2366 hooks_handler: self.hooks_handler,
2367 system_message_transform: self.system_message_transform,
2368 tool_handlers,
2369 canvas_handler,
2370 session_fs_provider: self.session_fs_provider,
2371 bearer_token_providers,
2372 commands: self.commands,
2373 };
2374
2375 Ok((wire, runtime))
2376 }
2377
2378 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2382 self.permission_handler = Some(handler);
2383 self
2384 }
2385
2386 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2389 self.elicitation_handler = Some(handler);
2390 self
2391 }
2392
2393 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2395 self.mcp_auth_handler = Some(handler);
2396 self
2397 }
2398
2399 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2402 self.user_input_handler = Some(handler);
2403 self
2404 }
2405
2406 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2408 self.exit_plan_mode_handler = Some(handler);
2409 self
2410 }
2411
2412 pub fn with_auto_mode_switch_handler(
2414 mut self,
2415 handler: Arc<dyn AutoModeSwitchHandler>,
2416 ) -> Self {
2417 self.auto_mode_switch_handler = Some(handler);
2418 self
2419 }
2420
2421 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2426 self.commands = Some(commands);
2427 self
2428 }
2429
2430 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2434 self.session_fs_provider = Some(provider);
2435 self
2436 }
2437
2438 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2441 self.hooks_handler = Some(hooks);
2442 self
2443 }
2444
2445 pub fn with_system_message_transform(
2449 mut self,
2450 transform: Arc<dyn SystemMessageTransform>,
2451 ) -> Self {
2452 self.system_message_transform = Some(transform);
2453 self
2454 }
2455
2456 pub fn approve_all_permissions(mut self) -> Self {
2462 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2463 self
2464 }
2465
2466 pub fn deny_all_permissions(mut self) -> Self {
2469 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2470 self
2471 }
2472
2473 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2478 where
2479 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2480 {
2481 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2482 self
2483 }
2484
2485 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2487 self.session_id = Some(id.into());
2488 self
2489 }
2490
2491 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2493 self.model = Some(model.into());
2494 self
2495 }
2496
2497 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2499 self.client_name = Some(name.into());
2500 self
2501 }
2502
2503 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2505 self.reasoning_effort = Some(effort.into());
2506 self
2507 }
2508
2509 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2511 self.reasoning_summary = Some(summary);
2512 self
2513 }
2514
2515 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2517 self.context_tier = Some(tier.into());
2518 self
2519 }
2520
2521 pub fn with_streaming(mut self, streaming: bool) -> Self {
2523 self.streaming = Some(streaming);
2524 self
2525 }
2526
2527 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2529 self.system_message = Some(system_message);
2530 self
2531 }
2532
2533 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2535 self.tools = Some(tools.into_iter().collect());
2536 self
2537 }
2538
2539 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2544 self.canvases = Some(canvases.into_iter().collect());
2545 self
2546 }
2547
2548 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2550 self.canvas_handler = Some(handler);
2551 self
2552 }
2553
2554 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2556 self.request_canvas_renderer = Some(request);
2557 self
2558 }
2559
2560 pub fn with_request_extensions(mut self, request: bool) -> Self {
2562 self.request_extensions = Some(request);
2563 self
2564 }
2565
2566 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2570 self.extension_sdk_path = Some(path.into());
2571 self
2572 }
2573
2574 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2576 self.extension_info = Some(extension_info);
2577 self
2578 }
2579
2580 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2583 self.canvas_provider = Some(canvas_provider);
2584 self
2585 }
2586
2587 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2589 where
2590 I: IntoIterator<Item = S>,
2591 S: Into<String>,
2592 {
2593 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2594 self
2595 }
2596
2597 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2599 where
2600 I: IntoIterator<Item = S>,
2601 S: Into<String>,
2602 {
2603 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2604 self
2605 }
2606
2607 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2609 where
2610 I: IntoIterator<Item = S>,
2611 S: Into<String>,
2612 {
2613 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2614 self
2615 }
2616
2617 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2619 self.mcp_servers = Some(servers);
2620 self
2621 }
2622
2623 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2631 self.mcp_oauth_token_storage = Some(mode.into());
2632 self
2633 }
2634
2635 pub fn with_embedding_cache_storage(
2637 mut self,
2638 embedding_cache_storage: impl Into<String>,
2639 ) -> Self {
2640 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2641 self
2642 }
2643
2644 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2646 self.enable_config_discovery = Some(enable);
2647 self
2648 }
2649
2650 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2652 self.skip_embedding_retrieval = Some(value);
2653 self
2654 }
2655
2656 pub fn with_organization_custom_instructions(
2658 mut self,
2659 instructions: impl Into<String>,
2660 ) -> Self {
2661 self.organization_custom_instructions = Some(instructions.into());
2662 self
2663 }
2664
2665 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2667 self.enable_on_demand_instruction_discovery = Some(value);
2668 self
2669 }
2670
2671 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2673 self.enable_file_hooks = Some(value);
2674 self
2675 }
2676
2677 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2679 self.enable_host_git_operations = Some(value);
2680 self
2681 }
2682
2683 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2685 self.enable_session_store = Some(value);
2686 self
2687 }
2688
2689 pub fn with_enable_skills(mut self, value: bool) -> Self {
2691 self.enable_skills = Some(value);
2692 self
2693 }
2694
2695 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2701 self.enable_mcp_apps = Some(enable);
2702 self
2703 }
2704
2705 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2707 where
2708 I: IntoIterator<Item = P>,
2709 P: Into<PathBuf>,
2710 {
2711 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2712 self
2713 }
2714
2715 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2719 where
2720 I: IntoIterator<Item = P>,
2721 P: Into<PathBuf>,
2722 {
2723 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2724 self
2725 }
2726
2727 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2729 where
2730 I: IntoIterator<Item = P>,
2731 P: Into<PathBuf>,
2732 {
2733 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2734 self
2735 }
2736
2737 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2739 self.large_output = Some(config);
2740 self
2741 }
2742
2743 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
2746 self.tool_search = Some(config);
2747 self
2748 }
2749
2750 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2752 where
2753 I: IntoIterator<Item = S>,
2754 S: Into<String>,
2755 {
2756 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2757 self
2758 }
2759
2760 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2762 mut self,
2763 agents: I,
2764 ) -> Self {
2765 self.custom_agents = Some(agents.into_iter().collect());
2766 self
2767 }
2768
2769 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2771 self.default_agent = Some(agent);
2772 self
2773 }
2774
2775 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2778 self.agent = Some(name.into());
2779 self
2780 }
2781
2782 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2785 self.infinite_sessions = Some(config);
2786 self
2787 }
2788
2789 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2791 self.provider = Some(provider);
2792 self
2793 }
2794
2795 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
2797 self.capi = Some(capi);
2798 self
2799 }
2800
2801 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
2807 self.providers = Some(providers);
2808 self
2809 }
2810
2811 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
2817 self.models = Some(models);
2818 self
2819 }
2820
2821 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2825 self.enable_session_telemetry = Some(enable);
2826 self
2827 }
2828
2829 pub fn with_enable_citations(mut self, enable: bool) -> Self {
2831 self.enable_citations = Some(enable);
2832 self
2833 }
2834
2835 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
2837 self.session_limits = Some(limits);
2838 self
2839 }
2840
2841 pub fn with_model_capabilities(
2843 mut self,
2844 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2845 ) -> Self {
2846 self.model_capabilities = Some(capabilities);
2847 self
2848 }
2849
2850 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
2852 self.memory = Some(memory);
2853 self
2854 }
2855
2856 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2858 self.config_directory = Some(dir.into());
2859 self
2860 }
2861
2862 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2865 self.working_directory = Some(dir.into());
2866 self
2867 }
2868
2869 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2874 self.github_token = Some(token.into());
2875 self
2876 }
2877
2878 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2881 self.include_sub_agent_streaming_events = Some(include);
2882 self
2883 }
2884
2885 pub fn with_remote_session(
2887 mut self,
2888 mode: crate::generated::api_types::RemoteSessionMode,
2889 ) -> Self {
2890 self.remote_session = Some(mode);
2891 self
2892 }
2893
2894 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
2896 self.cloud = Some(cloud);
2897 self
2898 }
2899
2900 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
2902 self.skip_custom_instructions = Some(value);
2903 self
2904 }
2905
2906 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
2908 self.custom_agents_local_only = Some(value);
2909 self
2910 }
2911
2912 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
2914 self.coauthor_enabled = Some(value);
2915 self
2916 }
2917
2918 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
2920 self.manage_schedule_enabled = Some(value);
2921 self
2922 }
2923
2924 #[doc(hidden)]
2932 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
2933 self.exp_assignments = Some(assignments);
2934 self
2935 }
2936
2937 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
2943 self.enable_managed_settings = Some(enabled);
2944 self
2945 }
2946}
2947#[derive(Clone)]
2954#[non_exhaustive]
2955pub struct ResumeSessionConfig {
2956 pub session_id: SessionId,
2958 pub model: Option<String>,
2961 pub client_name: Option<String>,
2963 pub reasoning_effort: Option<String>,
2965 pub reasoning_summary: Option<ReasoningSummary>,
2969 pub context_tier: Option<String>,
2972 pub streaming: Option<bool>,
2974 pub system_message: Option<SystemMessageConfig>,
2977 pub tools: Option<Vec<Tool>>,
2979 pub canvases: Option<Vec<CanvasDeclaration>>,
2981 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2984 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
2986 pub request_canvas_renderer: Option<bool>,
2988 pub request_extensions: Option<bool>,
2990 pub extension_sdk_path: Option<String>,
2994 pub extension_info: Option<ExtensionInfo>,
2996 pub canvas_provider: Option<CanvasProviderIdentity>,
2999 pub available_tools: Option<Vec<String>>,
3001 pub excluded_tools: Option<Vec<String>>,
3003 pub excluded_builtin_agents: Option<Vec<String>>,
3009 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3011 pub mcp_oauth_token_storage: Option<String>,
3014 pub enable_config_discovery: Option<bool>,
3016 pub skip_embedding_retrieval: Option<bool>,
3018 pub embedding_cache_storage: Option<String>,
3020 pub organization_custom_instructions: Option<String>,
3022 pub enable_on_demand_instruction_discovery: Option<bool>,
3024 pub enable_file_hooks: Option<bool>,
3026 pub enable_host_git_operations: Option<bool>,
3028 pub enable_session_store: Option<bool>,
3030 pub enable_skills: Option<bool>,
3032 pub enable_mcp_apps: Option<bool>,
3038 pub skill_directories: Option<Vec<PathBuf>>,
3040 pub instruction_directories: Option<Vec<PathBuf>>,
3043 pub plugin_directories: Option<Vec<PathBuf>>,
3045 pub large_output: Option<LargeToolOutputConfig>,
3047 pub tool_search: Option<ToolSearchConfig>,
3050 pub disabled_skills: Option<Vec<String>>,
3052 pub hooks: Option<bool>,
3054 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3056 pub default_agent: Option<DefaultAgentConfig>,
3058 pub agent: Option<String>,
3060 pub infinite_sessions: Option<InfiniteSessionConfig>,
3062 pub provider: Option<ProviderConfig>,
3064 pub capi: Option<CapiSessionOptions>,
3070 pub providers: Option<Vec<NamedProviderConfig>>,
3076 pub models: Option<Vec<ProviderModelConfig>>,
3082 pub enable_session_telemetry: Option<bool>,
3090 pub enable_citations: Option<bool>,
3092 pub session_limits: Option<SessionLimitsConfig>,
3094 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3096 pub memory: Option<MemoryConfiguration>,
3098 pub config_directory: Option<PathBuf>,
3100 pub working_directory: Option<PathBuf>,
3102 pub github_token: Option<String>,
3105 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3108 pub include_sub_agent_streaming_events: Option<bool>,
3110 pub commands: Option<Vec<CommandDefinition>>,
3114 #[doc(hidden)]
3119 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3120 pub enable_managed_settings: Option<bool>,
3126 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3131 pub suppress_resume_event: Option<bool>,
3134 pub continue_pending_work: Option<bool>,
3142 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3145 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3148 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3150 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3153 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3156 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3159 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3161 pub(crate) permission_policy: Option<crate::permission::Policy>,
3163 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3165 pub skip_custom_instructions: Option<bool>,
3167 pub custom_agents_local_only: Option<bool>,
3169 pub coauthor_enabled: Option<bool>,
3171 pub manage_schedule_enabled: Option<bool>,
3173}
3174
3175impl std::fmt::Debug for ResumeSessionConfig {
3176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3177 f.debug_struct("ResumeSessionConfig")
3178 .field("session_id", &self.session_id)
3179 .field("model", &self.model)
3180 .field("client_name", &self.client_name)
3181 .field("reasoning_effort", &self.reasoning_effort)
3182 .field("reasoning_summary", &self.reasoning_summary)
3183 .field("context_tier", &self.context_tier)
3184 .field("streaming", &self.streaming)
3185 .field("system_message", &self.system_message)
3186 .field("tools", &self.tools)
3187 .field("canvases", &self.canvases)
3188 .field(
3189 "canvas_handler",
3190 &self.canvas_handler.as_ref().map(|_| "<set>"),
3191 )
3192 .field("open_canvases", &self.open_canvases)
3193 .field("request_canvas_renderer", &self.request_canvas_renderer)
3194 .field("request_extensions", &self.request_extensions)
3195 .field("extension_sdk_path", &self.extension_sdk_path)
3196 .field("extension_info", &self.extension_info)
3197 .field("canvas_provider", &self.canvas_provider)
3198 .field("available_tools", &self.available_tools)
3199 .field("excluded_tools", &self.excluded_tools)
3200 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3201 .field("mcp_servers", &self.mcp_servers)
3202 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3203 .field("embedding_cache_storage", &self.embedding_cache_storage)
3204 .field("enable_config_discovery", &self.enable_config_discovery)
3205 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3206 .field(
3207 "organization_custom_instructions",
3208 &self
3209 .organization_custom_instructions
3210 .as_ref()
3211 .map(|_| "<redacted>"),
3212 )
3213 .field(
3214 "enable_on_demand_instruction_discovery",
3215 &self.enable_on_demand_instruction_discovery,
3216 )
3217 .field("enable_file_hooks", &self.enable_file_hooks)
3218 .field(
3219 "enable_host_git_operations",
3220 &self.enable_host_git_operations,
3221 )
3222 .field("enable_session_store", &self.enable_session_store)
3223 .field("enable_skills", &self.enable_skills)
3224 .field("enable_mcp_apps", &self.enable_mcp_apps)
3225 .field("skill_directories", &self.skill_directories)
3226 .field("instruction_directories", &self.instruction_directories)
3227 .field("plugin_directories", &self.plugin_directories)
3228 .field("large_output", &self.large_output)
3229 .field("tool_search", &self.tool_search)
3230 .field("disabled_skills", &self.disabled_skills)
3231 .field("hooks", &self.hooks)
3232 .field("custom_agents", &self.custom_agents)
3233 .field("default_agent", &self.default_agent)
3234 .field("agent", &self.agent)
3235 .field("infinite_sessions", &self.infinite_sessions)
3236 .field("provider", &self.provider)
3237 .field("capi", &self.capi)
3238 .field("enable_session_telemetry", &self.enable_session_telemetry)
3239 .field("enable_citations", &self.enable_citations)
3240 .field("session_limits", &self.session_limits)
3241 .field("model_capabilities", &self.model_capabilities)
3242 .field("memory", &self.memory)
3243 .field("config_directory", &self.config_directory)
3244 .field("working_directory", &self.working_directory)
3245 .field(
3246 "github_token",
3247 &self.github_token.as_ref().map(|_| "<redacted>"),
3248 )
3249 .field("remote_session", &self.remote_session)
3250 .field(
3251 "include_sub_agent_streaming_events",
3252 &self.include_sub_agent_streaming_events,
3253 )
3254 .field("commands", &self.commands)
3255 .field("exp_assignments", &self.exp_assignments)
3256 .field("enable_managed_settings", &self.enable_managed_settings)
3257 .field(
3258 "session_fs_provider",
3259 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3260 )
3261 .field(
3262 "permission_handler",
3263 &self.permission_handler.as_ref().map(|_| "<set>"),
3264 )
3265 .field(
3266 "elicitation_handler",
3267 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3268 )
3269 .field(
3270 "user_input_handler",
3271 &self.user_input_handler.as_ref().map(|_| "<set>"),
3272 )
3273 .field(
3274 "exit_plan_mode_handler",
3275 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3276 )
3277 .field(
3278 "auto_mode_switch_handler",
3279 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3280 )
3281 .field(
3282 "hooks_handler",
3283 &self.hooks_handler.as_ref().map(|_| "<set>"),
3284 )
3285 .field(
3286 "system_message_transform",
3287 &self.system_message_transform.as_ref().map(|_| "<set>"),
3288 )
3289 .field("suppress_resume_event", &self.suppress_resume_event)
3290 .field("continue_pending_work", &self.continue_pending_work)
3291 .finish()
3292 }
3293}
3294
3295impl ResumeSessionConfig {
3296 pub(crate) fn into_wire(
3304 mut self,
3305 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3306 let permission_active =
3307 self.permission_handler.is_some() || self.permission_policy.is_some();
3308 let request_user_input = self.user_input_handler.is_some();
3309 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3310 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3311 let request_elicitation = self.elicitation_handler.is_some();
3312 let hooks_flag = self.hooks_handler.is_some();
3313
3314 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3315 if let Some(tools) = self.tools.as_mut() {
3316 for tool in tools.iter_mut() {
3317 if let Some(handler) = tool.handler.take()
3318 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3319 {
3320 return Err(crate::Error::with_message(
3321 crate::ErrorKind::InvalidConfig,
3322 format!("duplicate tool handler registered for name {:?}", tool.name),
3323 ));
3324 }
3325 }
3326 }
3327
3328 let wire_commands = self.commands.as_ref().map(|cmds| {
3329 cmds.iter()
3330 .map(|c| crate::wire::CommandWireDefinition {
3331 name: c.name.clone(),
3332 description: c.description.clone(),
3333 })
3334 .collect()
3335 });
3336 let wire_canvases = self.canvases.clone();
3337 let canvas_handler = self.canvas_handler.clone();
3338 let bearer_token_providers =
3339 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3340
3341 let wire = crate::wire::SessionResumeWire {
3342 session_id: self.session_id,
3343 model: self.model,
3344 client_name: self.client_name,
3345 reasoning_effort: self.reasoning_effort,
3346 reasoning_summary: self.reasoning_summary,
3347 context_tier: self.context_tier,
3348 streaming: self.streaming,
3349 system_message: self.system_message,
3350 tools: self.tools,
3351 canvases: wire_canvases,
3352 open_canvases: self.open_canvases,
3353 request_canvas_renderer: self.request_canvas_renderer,
3354 request_extensions: self.request_extensions,
3355 extension_sdk_path: self.extension_sdk_path,
3356 extension_info: self.extension_info,
3357 canvas_provider: self.canvas_provider,
3358 available_tools: self.available_tools,
3359 excluded_tools: self.excluded_tools,
3360 excluded_builtin_agents: self.excluded_builtin_agents,
3361 tool_filter_precedence: "excluded",
3362 mcp_servers: self.mcp_servers,
3363 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3364 embedding_cache_storage: self.embedding_cache_storage,
3365 env_value_mode: "direct",
3366 enable_config_discovery: self.enable_config_discovery,
3367 skip_embedding_retrieval: self.skip_embedding_retrieval,
3368 organization_custom_instructions: self.organization_custom_instructions,
3369 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3370 enable_file_hooks: self.enable_file_hooks,
3371 enable_host_git_operations: self.enable_host_git_operations,
3372 enable_session_store: self.enable_session_store,
3373 enable_skills: self.enable_skills,
3374 request_user_input,
3375 request_permission: permission_active,
3376 request_exit_plan_mode,
3377 request_auto_mode_switch,
3378 request_elicitation,
3379 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3380 hooks: hooks_flag,
3381 skill_directories: self.skill_directories,
3382 instruction_directories: self.instruction_directories,
3383 plugin_directories: self.plugin_directories,
3384 large_output: self.large_output,
3385 tool_search: self.tool_search,
3386 disabled_skills: self.disabled_skills,
3387 custom_agents: self.custom_agents,
3388 default_agent: self.default_agent,
3389 agent: self.agent,
3390 infinite_sessions: self.infinite_sessions,
3391 provider: self.provider,
3392 capi: self.capi,
3393 providers: self.providers,
3394 models: self.models,
3395 enable_session_telemetry: self.enable_session_telemetry,
3396 enable_citations: self.enable_citations,
3397 session_limits: self.session_limits,
3398 model_capabilities: self.model_capabilities,
3399 memory: self.memory,
3400 config_dir: self.config_directory,
3401 working_directory: self.working_directory,
3402 github_token: self.github_token,
3403 remote_session: self.remote_session,
3404 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3405 enable_github_telemetry_forwarding: None,
3406 commands: wire_commands,
3407 exp_assignments: self.exp_assignments,
3408 enable_managed_settings: self.enable_managed_settings,
3409 suppress_resume_event: self.suppress_resume_event,
3410 continue_pending_work: self.continue_pending_work,
3411 };
3412
3413 let runtime = SessionConfigRuntime {
3414 permission_handler: self.permission_handler,
3415 permission_policy: self.permission_policy,
3416 elicitation_handler: self.elicitation_handler,
3417 mcp_auth_handler: self.mcp_auth_handler,
3418 user_input_handler: self.user_input_handler,
3419 exit_plan_mode_handler: self.exit_plan_mode_handler,
3420 auto_mode_switch_handler: self.auto_mode_switch_handler,
3421 hooks_handler: self.hooks_handler,
3422 system_message_transform: self.system_message_transform,
3423 tool_handlers,
3424 canvas_handler,
3425 session_fs_provider: self.session_fs_provider,
3426 bearer_token_providers,
3427 commands: self.commands,
3428 };
3429
3430 Ok((wire, runtime))
3431 }
3432
3433 pub fn new(session_id: SessionId) -> Self {
3438 Self {
3439 session_id,
3440 model: None,
3441 client_name: None,
3442 reasoning_effort: None,
3443 reasoning_summary: None,
3444 context_tier: None,
3445 streaming: None,
3446 system_message: None,
3447 tools: None,
3448 canvases: None,
3449 canvas_handler: None,
3450 open_canvases: None,
3451 request_canvas_renderer: None,
3452 request_extensions: None,
3453 extension_sdk_path: None,
3454 extension_info: None,
3455 canvas_provider: None,
3456 available_tools: None,
3457 excluded_tools: None,
3458 excluded_builtin_agents: None,
3459 mcp_servers: None,
3460 mcp_oauth_token_storage: None,
3461 enable_config_discovery: None,
3462 skip_embedding_retrieval: None,
3463 organization_custom_instructions: None,
3464 enable_on_demand_instruction_discovery: None,
3465 enable_file_hooks: None,
3466 enable_host_git_operations: None,
3467 enable_session_store: None,
3468 enable_skills: None,
3469 embedding_cache_storage: None,
3470 enable_mcp_apps: None,
3471 skill_directories: None,
3472 instruction_directories: None,
3473 plugin_directories: None,
3474 large_output: None,
3475 tool_search: None,
3476 disabled_skills: None,
3477 hooks: None,
3478 custom_agents: None,
3479 default_agent: None,
3480 agent: None,
3481 infinite_sessions: None,
3482 provider: None,
3483 capi: None,
3484 providers: None,
3485 models: None,
3486 enable_session_telemetry: None,
3487 enable_citations: None,
3488 session_limits: None,
3489 model_capabilities: None,
3490 memory: None,
3491 config_directory: None,
3492 working_directory: None,
3493 github_token: None,
3494 remote_session: None,
3495 include_sub_agent_streaming_events: None,
3496 commands: None,
3497 exp_assignments: None,
3498 enable_managed_settings: None,
3499 session_fs_provider: None,
3500 suppress_resume_event: None,
3501 continue_pending_work: None,
3502 permission_handler: None,
3503 elicitation_handler: None,
3504 mcp_auth_handler: None,
3505 user_input_handler: None,
3506 exit_plan_mode_handler: None,
3507 auto_mode_switch_handler: None,
3508 hooks_handler: None,
3509 permission_policy: None,
3510 system_message_transform: None,
3511 skip_custom_instructions: None,
3512 custom_agents_local_only: None,
3513 coauthor_enabled: None,
3514 manage_schedule_enabled: None,
3515 }
3516 }
3517
3518 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3520 self.permission_handler = Some(handler);
3521 self
3522 }
3523
3524 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3526 self.elicitation_handler = Some(handler);
3527 self
3528 }
3529
3530 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3532 self.mcp_auth_handler = Some(handler);
3533 self
3534 }
3535
3536 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3538 self.user_input_handler = Some(handler);
3539 self
3540 }
3541
3542 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3544 self.exit_plan_mode_handler = Some(handler);
3545 self
3546 }
3547
3548 pub fn with_auto_mode_switch_handler(
3550 mut self,
3551 handler: Arc<dyn AutoModeSwitchHandler>,
3552 ) -> Self {
3553 self.auto_mode_switch_handler = Some(handler);
3554 self
3555 }
3556
3557 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3560 self.hooks_handler = Some(hooks);
3561 self
3562 }
3563
3564 pub fn with_system_message_transform(
3566 mut self,
3567 transform: Arc<dyn SystemMessageTransform>,
3568 ) -> Self {
3569 self.system_message_transform = Some(transform);
3570 self
3571 }
3572
3573 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3577 self.commands = Some(commands);
3578 self
3579 }
3580
3581 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3584 self.session_fs_provider = Some(provider);
3585 self
3586 }
3587
3588 pub fn approve_all_permissions(mut self) -> Self {
3591 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3592 self
3593 }
3594
3595 pub fn deny_all_permissions(mut self) -> Self {
3598 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3599 self
3600 }
3601
3602 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3605 where
3606 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3607 {
3608 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3609 self
3610 }
3611
3612 pub fn with_model(mut self, model: impl Into<String>) -> Self {
3614 self.model = Some(model.into());
3615 self
3616 }
3617
3618 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3620 self.client_name = Some(name.into());
3621 self
3622 }
3623
3624 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3626 self.reasoning_effort = Some(effort.into());
3627 self
3628 }
3629
3630 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3632 self.reasoning_summary = Some(summary);
3633 self
3634 }
3635
3636 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3639 self.context_tier = Some(tier.into());
3640 self
3641 }
3642
3643 pub fn with_streaming(mut self, streaming: bool) -> Self {
3645 self.streaming = Some(streaming);
3646 self
3647 }
3648
3649 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3652 self.system_message = Some(system_message);
3653 self
3654 }
3655
3656 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3658 self.tools = Some(tools.into_iter().collect());
3659 self
3660 }
3661
3662 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3664 self.canvases = Some(canvases.into_iter().collect());
3665 self
3666 }
3667
3668 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
3670 self.canvas_handler = Some(handler);
3671 self
3672 }
3673
3674 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
3676 mut self,
3677 open_canvases: I,
3678 ) -> Self {
3679 self.open_canvases = Some(open_canvases.into_iter().collect());
3680 self
3681 }
3682
3683 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
3685 self.request_canvas_renderer = Some(request);
3686 self
3687 }
3688
3689 pub fn with_request_extensions(mut self, request: bool) -> Self {
3691 self.request_extensions = Some(request);
3692 self
3693 }
3694
3695 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
3699 self.extension_sdk_path = Some(path.into());
3700 self
3701 }
3702
3703 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
3705 self.extension_info = Some(extension_info);
3706 self
3707 }
3708
3709 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
3712 self.canvas_provider = Some(canvas_provider);
3713 self
3714 }
3715
3716 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
3718 where
3719 I: IntoIterator<Item = S>,
3720 S: Into<String>,
3721 {
3722 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
3723 self
3724 }
3725
3726 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
3728 where
3729 I: IntoIterator<Item = S>,
3730 S: Into<String>,
3731 {
3732 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
3733 self
3734 }
3735
3736 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
3738 where
3739 I: IntoIterator<Item = S>,
3740 S: Into<String>,
3741 {
3742 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
3743 self
3744 }
3745
3746 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
3748 self.mcp_servers = Some(servers);
3749 self
3750 }
3751
3752 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3755 self.mcp_oauth_token_storage = Some(mode.into());
3756 self
3757 }
3758
3759 pub fn with_embedding_cache_storage(
3761 mut self,
3762 embedding_cache_storage: impl Into<String>,
3763 ) -> Self {
3764 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3765 self
3766 }
3767
3768 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3770 self.enable_config_discovery = Some(enable);
3771 self
3772 }
3773
3774 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3776 self.skip_embedding_retrieval = Some(value);
3777 self
3778 }
3779
3780 pub fn with_organization_custom_instructions(
3782 mut self,
3783 instructions: impl Into<String>,
3784 ) -> Self {
3785 self.organization_custom_instructions = Some(instructions.into());
3786 self
3787 }
3788
3789 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3791 self.enable_on_demand_instruction_discovery = Some(value);
3792 self
3793 }
3794
3795 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3797 self.enable_file_hooks = Some(value);
3798 self
3799 }
3800
3801 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3803 self.enable_host_git_operations = Some(value);
3804 self
3805 }
3806
3807 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3809 self.enable_session_store = Some(value);
3810 self
3811 }
3812
3813 pub fn with_enable_skills(mut self, value: bool) -> Self {
3815 self.enable_skills = Some(value);
3816 self
3817 }
3818
3819 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3825 self.enable_mcp_apps = Some(enable);
3826 self
3827 }
3828
3829 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3831 where
3832 I: IntoIterator<Item = P>,
3833 P: Into<PathBuf>,
3834 {
3835 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3836 self
3837 }
3838
3839 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3843 where
3844 I: IntoIterator<Item = P>,
3845 P: Into<PathBuf>,
3846 {
3847 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3848 self
3849 }
3850
3851 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3853 where
3854 I: IntoIterator<Item = P>,
3855 P: Into<PathBuf>,
3856 {
3857 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3858 self
3859 }
3860
3861 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3863 self.large_output = Some(config);
3864 self
3865 }
3866
3867 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
3870 self.tool_search = Some(config);
3871 self
3872 }
3873
3874 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3876 where
3877 I: IntoIterator<Item = S>,
3878 S: Into<String>,
3879 {
3880 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3881 self
3882 }
3883
3884 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3886 mut self,
3887 agents: I,
3888 ) -> Self {
3889 self.custom_agents = Some(agents.into_iter().collect());
3890 self
3891 }
3892
3893 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3895 self.default_agent = Some(agent);
3896 self
3897 }
3898
3899 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3901 self.agent = Some(name.into());
3902 self
3903 }
3904
3905 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3907 self.infinite_sessions = Some(config);
3908 self
3909 }
3910
3911 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3913 self.provider = Some(provider);
3914 self
3915 }
3916
3917 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3919 self.capi = Some(capi);
3920 self
3921 }
3922
3923 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3929 self.providers = Some(providers);
3930 self
3931 }
3932
3933 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3939 self.models = Some(models);
3940 self
3941 }
3942
3943 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3947 self.enable_session_telemetry = Some(enable);
3948 self
3949 }
3950
3951 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3953 self.enable_citations = Some(enable);
3954 self
3955 }
3956
3957 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3959 self.session_limits = Some(limits);
3960 self
3961 }
3962
3963 pub fn with_model_capabilities(
3965 mut self,
3966 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3967 ) -> Self {
3968 self.model_capabilities = Some(capabilities);
3969 self
3970 }
3971
3972 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3974 self.memory = Some(memory);
3975 self
3976 }
3977
3978 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3980 self.config_directory = Some(dir.into());
3981 self
3982 }
3983
3984 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3986 self.working_directory = Some(dir.into());
3987 self
3988 }
3989
3990 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3994 self.github_token = Some(token.into());
3995 self
3996 }
3997
3998 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4000 self.include_sub_agent_streaming_events = Some(include);
4001 self
4002 }
4003
4004 pub fn with_remote_session(
4006 mut self,
4007 mode: crate::generated::api_types::RemoteSessionMode,
4008 ) -> Self {
4009 self.remote_session = Some(mode);
4010 self
4011 }
4012
4013 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4016 self.suppress_resume_event = Some(suppress);
4017 self
4018 }
4019
4020 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4026 self.continue_pending_work = Some(continue_pending);
4027 self
4028 }
4029
4030 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4032 self.skip_custom_instructions = Some(value);
4033 self
4034 }
4035
4036 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4038 self.custom_agents_local_only = Some(value);
4039 self
4040 }
4041
4042 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4044 self.coauthor_enabled = Some(value);
4045 self
4046 }
4047
4048 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4050 self.manage_schedule_enabled = Some(value);
4051 self
4052 }
4053
4054 #[doc(hidden)]
4058 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4059 self.exp_assignments = Some(assignments);
4060 self
4061 }
4062
4063 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4066 self.enable_managed_settings = Some(enabled);
4067 self
4068 }
4069}
4070
4071#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4077#[serde(rename_all = "camelCase")]
4078#[non_exhaustive]
4079pub struct SystemMessageConfig {
4080 #[serde(skip_serializing_if = "Option::is_none")]
4082 pub mode: Option<String>,
4083 #[serde(skip_serializing_if = "Option::is_none")]
4085 pub content: Option<String>,
4086 #[serde(skip_serializing_if = "Option::is_none")]
4088 pub sections: Option<HashMap<String, SectionOverride>>,
4089}
4090
4091impl SystemMessageConfig {
4092 pub fn new() -> Self {
4095 Self::default()
4096 }
4097
4098 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4101 self.mode = Some(mode.into());
4102 self
4103 }
4104
4105 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4108 self.content = Some(content.into());
4109 self
4110 }
4111
4112 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4114 self.sections = Some(sections);
4115 self
4116 }
4117}
4118
4119#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4125#[serde(rename_all = "camelCase")]
4126pub struct SectionOverride {
4127 #[serde(skip_serializing_if = "Option::is_none")]
4130 pub action: Option<String>,
4131 #[serde(skip_serializing_if = "Option::is_none")]
4133 pub content: Option<String>,
4134}
4135
4136#[derive(Debug, Clone, Serialize, Deserialize)]
4138#[serde(rename_all = "camelCase")]
4139pub struct CreateSessionResult {
4140 pub session_id: SessionId,
4142 #[serde(skip_serializing_if = "Option::is_none")]
4144 pub workspace_path: Option<PathBuf>,
4145 #[serde(default, alias = "remote_url")]
4147 pub remote_url: Option<String>,
4148 #[serde(skip_serializing_if = "Option::is_none")]
4150 pub capabilities: Option<SessionCapabilities>,
4151}
4152
4153#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4155#[serde(rename_all = "camelCase")]
4156pub(crate) struct ResumeSessionResult {
4157 #[serde(default)]
4159 pub session_id: Option<SessionId>,
4160 #[serde(default, skip_serializing_if = "Option::is_none")]
4162 pub workspace_path: Option<PathBuf>,
4163 #[serde(default, alias = "remote_url")]
4165 pub remote_url: Option<String>,
4166 #[serde(default, skip_serializing_if = "Option::is_none")]
4168 pub capabilities: Option<SessionCapabilities>,
4169 #[serde(
4171 default,
4172 alias = "openCanvasInstances",
4173 skip_serializing_if = "Option::is_none"
4174 )]
4175 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4176}
4177
4178#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4180#[serde(rename_all = "lowercase")]
4181pub enum LogLevel {
4182 #[default]
4184 Info,
4185 Warning,
4187 Error,
4189}
4190
4191#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4196#[serde(rename_all = "camelCase")]
4197pub struct LogOptions {
4198 #[serde(skip_serializing_if = "Option::is_none")]
4200 pub level: Option<LogLevel>,
4201 #[serde(skip_serializing_if = "Option::is_none")]
4204 pub ephemeral: Option<bool>,
4205}
4206
4207impl LogOptions {
4208 pub fn with_level(mut self, level: LogLevel) -> Self {
4210 self.level = Some(level);
4211 self
4212 }
4213
4214 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4216 self.ephemeral = Some(ephemeral);
4217 self
4218 }
4219}
4220
4221#[derive(Debug, Clone, Default)]
4225pub struct SetModelOptions {
4226 pub reasoning_effort: Option<String>,
4229 pub reasoning_summary: Option<ReasoningSummary>,
4233 pub context_tier: Option<ContextTier>,
4236 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4240}
4241
4242impl SetModelOptions {
4243 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4245 self.reasoning_effort = Some(effort.into());
4246 self
4247 }
4248
4249 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4251 self.reasoning_summary = Some(summary);
4252 self
4253 }
4254
4255 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4257 self.context_tier = Some(tier);
4258 self
4259 }
4260
4261 pub fn with_model_capabilities(
4263 mut self,
4264 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4265 ) -> Self {
4266 self.model_capabilities = Some(caps);
4267 self
4268 }
4269}
4270
4271#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4278#[serde(rename_all = "camelCase")]
4279pub struct PingResponse {
4280 #[serde(default)]
4282 pub message: String,
4283 #[serde(default)]
4285 pub timestamp: String,
4286 #[serde(skip_serializing_if = "Option::is_none")]
4288 pub protocol_version: Option<u32>,
4289}
4290
4291#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4293#[serde(rename_all = "camelCase")]
4294pub struct AttachmentLineRange {
4295 pub start: u32,
4297 pub end: u32,
4299}
4300
4301#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4303#[serde(rename_all = "camelCase")]
4304pub struct AttachmentSelectionPosition {
4305 pub line: u32,
4307 pub character: u32,
4309}
4310
4311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4313#[serde(rename_all = "camelCase")]
4314pub struct AttachmentSelectionRange {
4315 pub start: AttachmentSelectionPosition,
4317 pub end: AttachmentSelectionPosition,
4319}
4320
4321#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4323#[serde(rename_all = "snake_case")]
4324#[non_exhaustive]
4325pub enum GitHubReferenceType {
4326 Issue,
4328 Pr,
4330 Discussion,
4332}
4333
4334#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4340#[serde(rename_all = "camelCase")]
4341pub struct GitHubRepoPointer {
4342 #[serde(skip_serializing_if = "Option::is_none")]
4344 pub id: Option<i64>,
4345 pub name: String,
4347 pub owner: String,
4349}
4350
4351#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4353#[serde(rename_all = "camelCase")]
4354pub struct GitHubFileDiffSide {
4355 pub path: String,
4357 pub r#ref: String,
4359 pub repo: GitHubRepoPointer,
4361}
4362
4363#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4365#[serde(rename_all = "camelCase")]
4366pub struct GitHubTreeComparisonSide {
4367 pub repo: GitHubRepoPointer,
4369 pub revision: String,
4371}
4372
4373#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4375#[serde(rename_all = "camelCase")]
4376pub struct GitHubSnippetLineRange {
4377 pub start: i64,
4379 pub end: i64,
4381}
4382
4383#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4385#[serde(
4386 tag = "type",
4387 rename_all = "camelCase",
4388 rename_all_fields = "camelCase"
4389)]
4390#[non_exhaustive]
4391pub enum Attachment {
4392 File {
4394 path: PathBuf,
4396 #[serde(skip_serializing_if = "Option::is_none")]
4398 display_name: Option<String>,
4399 #[serde(skip_serializing_if = "Option::is_none")]
4401 line_range: Option<AttachmentLineRange>,
4402 },
4403 Directory {
4405 path: PathBuf,
4407 #[serde(skip_serializing_if = "Option::is_none")]
4409 display_name: Option<String>,
4410 },
4411 Selection {
4413 file_path: PathBuf,
4415 text: String,
4417 #[serde(skip_serializing_if = "Option::is_none")]
4419 display_name: Option<String>,
4420 selection: AttachmentSelectionRange,
4422 },
4423 Blob {
4425 data: String,
4427 mime_type: String,
4429 #[serde(skip_serializing_if = "Option::is_none")]
4431 display_name: Option<String>,
4432 },
4433 #[serde(rename = "github_reference")]
4435 GitHubReference {
4436 number: u64,
4438 title: String,
4440 reference_type: GitHubReferenceType,
4442 state: String,
4444 url: String,
4446 },
4447 #[serde(rename = "github_commit")]
4449 GitHubCommit {
4450 message: String,
4452 oid: String,
4454 repo: GitHubRepoPointer,
4456 url: String,
4458 },
4459 #[serde(rename = "github_release")]
4461 GitHubRelease {
4462 name: String,
4464 repo: GitHubRepoPointer,
4466 tag_name: String,
4468 url: String,
4470 },
4471 #[serde(rename = "github_actions_job")]
4473 GitHubActionsJob {
4474 #[serde(skip_serializing_if = "Option::is_none")]
4477 conclusion: Option<String>,
4478 job_id: i64,
4480 job_name: String,
4482 repo: GitHubRepoPointer,
4484 url: String,
4486 workflow_name: String,
4488 },
4489 #[serde(rename = "github_repository")]
4491 GitHubRepository {
4492 #[serde(skip_serializing_if = "Option::is_none")]
4494 description: Option<String>,
4495 #[serde(skip_serializing_if = "Option::is_none")]
4498 r#ref: Option<String>,
4499 repo: GitHubRepoPointer,
4501 url: String,
4503 },
4504 #[serde(rename = "github_file_diff")]
4506 GitHubFileDiff {
4507 #[serde(skip_serializing_if = "Option::is_none")]
4509 base: Option<GitHubFileDiffSide>,
4510 #[serde(skip_serializing_if = "Option::is_none")]
4512 head: Option<GitHubFileDiffSide>,
4513 url: String,
4515 },
4516 #[serde(rename = "github_tree_comparison")]
4518 GitHubTreeComparison {
4519 base: GitHubTreeComparisonSide,
4521 head: GitHubTreeComparisonSide,
4523 url: String,
4525 },
4526 #[serde(rename = "github_url")]
4528 GitHubUrl {
4529 url: String,
4531 },
4532 #[serde(rename = "github_file")]
4534 GitHubFile {
4535 path: String,
4537 r#ref: String,
4539 repo: GitHubRepoPointer,
4541 url: String,
4543 },
4544 #[serde(rename = "github_snippet")]
4546 GitHubSnippet {
4547 line_range: GitHubSnippetLineRange,
4549 path: String,
4551 r#ref: String,
4553 repo: GitHubRepoPointer,
4555 url: String,
4557 },
4558}
4559
4560impl Attachment {
4561 pub fn display_name(&self) -> Option<&str> {
4563 match self {
4564 Self::File { display_name, .. }
4565 | Self::Directory { display_name, .. }
4566 | Self::Selection { display_name, .. }
4567 | Self::Blob { display_name, .. } => display_name.as_deref(),
4568 Self::GitHubReference { .. }
4569 | Self::GitHubCommit { .. }
4570 | Self::GitHubRelease { .. }
4571 | Self::GitHubActionsJob { .. }
4572 | Self::GitHubRepository { .. }
4573 | Self::GitHubFileDiff { .. }
4574 | Self::GitHubTreeComparison { .. }
4575 | Self::GitHubUrl { .. }
4576 | Self::GitHubFile { .. }
4577 | Self::GitHubSnippet { .. } => None,
4578 }
4579 }
4580
4581 pub fn label(&self) -> Option<String> {
4583 if let Some(display_name) = self
4584 .display_name()
4585 .map(str::trim)
4586 .filter(|name| !name.is_empty())
4587 {
4588 return Some(display_name.to_string());
4589 }
4590
4591 match self {
4592 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4593 format!("#{}", number)
4594 } else {
4595 title.trim().to_string()
4596 }),
4597 _ => self.derived_display_name(),
4598 }
4599 }
4600
4601 pub fn ensure_display_name(&mut self) {
4603 if self
4604 .display_name()
4605 .map(str::trim)
4606 .is_some_and(|name| !name.is_empty())
4607 {
4608 return;
4609 }
4610
4611 let Some(derived_display_name) = self.derived_display_name() else {
4612 return;
4613 };
4614
4615 match self {
4616 Self::File { display_name, .. }
4617 | Self::Directory { display_name, .. }
4618 | Self::Selection { display_name, .. }
4619 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4620 Self::GitHubReference { .. }
4621 | Self::GitHubCommit { .. }
4622 | Self::GitHubRelease { .. }
4623 | Self::GitHubActionsJob { .. }
4624 | Self::GitHubRepository { .. }
4625 | Self::GitHubFileDiff { .. }
4626 | Self::GitHubTreeComparison { .. }
4627 | Self::GitHubUrl { .. }
4628 | Self::GitHubFile { .. }
4629 | Self::GitHubSnippet { .. } => {}
4630 }
4631 }
4632
4633 fn derived_display_name(&self) -> Option<String> {
4634 match self {
4635 Self::File { path, .. } | Self::Directory { path, .. } => {
4636 Some(attachment_name_from_path(path))
4637 }
4638 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4639 Self::Blob { .. } => Some("attachment".to_string()),
4640 Self::GitHubReference { .. }
4641 | Self::GitHubCommit { .. }
4642 | Self::GitHubRelease { .. }
4643 | Self::GitHubActionsJob { .. }
4644 | Self::GitHubRepository { .. }
4645 | Self::GitHubFileDiff { .. }
4646 | Self::GitHubTreeComparison { .. }
4647 | Self::GitHubUrl { .. }
4648 | Self::GitHubFile { .. }
4649 | Self::GitHubSnippet { .. } => None,
4650 }
4651 }
4652}
4653
4654fn attachment_name_from_path(path: &Path) -> String {
4655 path.file_name()
4656 .map(|name| name.to_string_lossy().into_owned())
4657 .filter(|name| !name.is_empty())
4658 .unwrap_or_else(|| {
4659 let full = path.to_string_lossy();
4660 if full.is_empty() {
4661 "attachment".to_string()
4662 } else {
4663 full.into_owned()
4664 }
4665 })
4666}
4667
4668pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
4670 for attachment in attachments {
4671 attachment.ensure_display_name();
4672 }
4673}
4674
4675#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4680#[serde(rename_all = "lowercase")]
4681#[non_exhaustive]
4682pub enum DeliveryMode {
4683 Enqueue,
4685 Immediate,
4687}
4688
4689#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4694#[serde(rename_all = "lowercase")]
4695#[non_exhaustive]
4696pub enum AgentMode {
4697 Interactive,
4699 Plan,
4701 Autopilot,
4703 Shell,
4705}
4706
4707#[derive(Debug, Clone)]
4736#[non_exhaustive]
4737pub struct MessageOptions {
4738 pub prompt: String,
4740 pub mode: Option<DeliveryMode>,
4746 pub agent_mode: Option<AgentMode>,
4750 pub attachments: Option<Vec<Attachment>>,
4752 pub wait_timeout: Option<Duration>,
4755 pub request_headers: Option<HashMap<String, String>>,
4759 pub traceparent: Option<String>,
4766 pub tracestate: Option<String>,
4770 pub display_prompt: Option<String>,
4772}
4773
4774impl MessageOptions {
4775 pub fn new(prompt: impl Into<String>) -> Self {
4777 Self {
4778 prompt: prompt.into(),
4779 mode: None,
4780 agent_mode: None,
4781 attachments: None,
4782 wait_timeout: None,
4783 request_headers: None,
4784 traceparent: None,
4785 tracestate: None,
4786 display_prompt: None,
4787 }
4788 }
4789
4790 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
4796 self.mode = Some(mode);
4797 self
4798 }
4799
4800 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
4804 self.agent_mode = Some(agent_mode);
4805 self
4806 }
4807
4808 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
4810 self.attachments = Some(attachments);
4811 self
4812 }
4813
4814 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
4816 self.wait_timeout = Some(timeout);
4817 self
4818 }
4819
4820 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
4822 self.request_headers = Some(headers);
4823 self
4824 }
4825
4826 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
4831 self.traceparent = ctx.traceparent;
4832 self.tracestate = ctx.tracestate;
4833 self
4834 }
4835
4836 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
4838 self.traceparent = Some(traceparent.into());
4839 self
4840 }
4841
4842 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
4844 self.tracestate = Some(tracestate.into());
4845 self
4846 }
4847
4848 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
4850 self.display_prompt = Some(display_prompt.into());
4851 self
4852 }
4853}
4854
4855impl From<&str> for MessageOptions {
4856 fn from(prompt: &str) -> Self {
4857 Self::new(prompt)
4858 }
4859}
4860
4861impl From<String> for MessageOptions {
4862 fn from(prompt: String) -> Self {
4863 Self::new(prompt)
4864 }
4865}
4866
4867impl From<&String> for MessageOptions {
4868 fn from(prompt: &String) -> Self {
4869 Self::new(prompt.clone())
4870 }
4871}
4872
4873#[derive(Debug, Clone, Serialize, Deserialize)]
4875#[serde(rename_all = "camelCase")]
4876#[non_exhaustive]
4877pub struct GetStatusResponse {
4878 pub version: String,
4880 pub protocol_version: u32,
4882}
4883
4884#[derive(Debug, Clone, Serialize, Deserialize)]
4886#[serde(rename_all = "camelCase")]
4887#[non_exhaustive]
4888pub struct GetAuthStatusResponse {
4889 pub is_authenticated: bool,
4891 #[serde(skip_serializing_if = "Option::is_none")]
4894 pub auth_type: Option<String>,
4895 #[serde(skip_serializing_if = "Option::is_none")]
4897 pub host: Option<String>,
4898 #[serde(skip_serializing_if = "Option::is_none")]
4900 pub login: Option<String>,
4901 #[serde(skip_serializing_if = "Option::is_none")]
4903 pub status_message: Option<String>,
4904}
4905
4906#[derive(Debug, Clone, Serialize, Deserialize)]
4910#[serde(rename_all = "camelCase")]
4911pub struct SessionEventNotification {
4912 pub session_id: SessionId,
4914 pub event: SessionEvent,
4916}
4917
4918#[derive(Debug, Clone, Serialize, Deserialize)]
4925#[serde(rename_all = "camelCase")]
4926pub struct SessionEvent {
4927 pub id: String,
4929 pub timestamp: String,
4931 pub parent_id: Option<String>,
4933 #[serde(skip_serializing_if = "Option::is_none")]
4935 pub ephemeral: Option<bool>,
4936 #[serde(skip_serializing_if = "Option::is_none")]
4939 pub agent_id: Option<String>,
4940 #[serde(skip_serializing_if = "Option::is_none")]
4942 pub debug_cli_received_at_ms: Option<i64>,
4943 #[serde(skip_serializing_if = "Option::is_none")]
4945 pub debug_ws_forwarded_at_ms: Option<i64>,
4946 #[serde(rename = "type")]
4948 pub event_type: String,
4949 pub data: Value,
4951}
4952
4953impl SessionEvent {
4954 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
4959 use serde::de::IntoDeserializer;
4960 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
4961 self.event_type.as_str().into_deserializer();
4962 crate::generated::SessionEventType::deserialize(deserializer)
4963 .unwrap_or(crate::generated::SessionEventType::Unknown)
4964 }
4965
4966 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
4972 serde_json::from_value(self.data.clone()).ok()
4973 }
4974
4975 pub fn is_transient_error(&self) -> bool {
4979 self.event_type == "session.error"
4980 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
4981 }
4982}
4983
4984#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4989#[serde(rename_all = "camelCase")]
4990#[non_exhaustive]
4991pub struct ToolInvocation {
4992 pub session_id: SessionId,
4994 pub tool_call_id: String,
4996 pub tool_name: String,
4998 pub arguments: Value,
5000 #[serde(skip)]
5008 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5009 #[serde(default, skip_serializing_if = "Option::is_none")]
5014 pub traceparent: Option<String>,
5015 #[serde(default, skip_serializing_if = "Option::is_none")]
5018 pub tracestate: Option<String>,
5019}
5020
5021impl ToolInvocation {
5022 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5043 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5044 }
5045
5046 pub fn trace_context(&self) -> TraceContext {
5049 TraceContext {
5050 traceparent: self.traceparent.clone(),
5051 tracestate: self.tracestate.clone(),
5052 }
5053 }
5054}
5055
5056#[derive(Debug, Clone, Serialize, Deserialize)]
5058#[serde(rename_all = "camelCase")]
5059pub struct ToolBinaryResult {
5060 pub data: String,
5062 pub mime_type: String,
5064 pub r#type: String,
5066 #[serde(default, skip_serializing_if = "Option::is_none")]
5068 pub description: Option<String>,
5069}
5070
5071#[derive(Debug, Clone, Serialize, Deserialize)]
5078#[serde(rename_all = "camelCase")]
5079#[non_exhaustive]
5080pub struct ToolResultExpanded {
5081 pub text_result_for_llm: String,
5083 pub result_type: String,
5085 #[serde(default, skip_serializing_if = "Option::is_none")]
5087 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5088 #[serde(skip_serializing_if = "Option::is_none")]
5090 pub session_log: Option<String>,
5091 #[serde(skip_serializing_if = "Option::is_none")]
5093 pub error: Option<String>,
5094 #[serde(default, skip_serializing_if = "Option::is_none")]
5096 pub tool_telemetry: Option<HashMap<String, Value>>,
5097 #[serde(default, skip_serializing_if = "Option::is_none")]
5099 pub tool_references: Option<Vec<String>>,
5100}
5101
5102impl ToolResultExpanded {
5103 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5107 Self {
5108 text_result_for_llm: text_result_for_llm.into(),
5109 result_type: result_type.into(),
5110 binary_results_for_llm: None,
5111 session_log: None,
5112 error: None,
5113 tool_telemetry: None,
5114 tool_references: None,
5115 }
5116 }
5117
5118 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5120 self.binary_results_for_llm = Some(results);
5121 self
5122 }
5123
5124 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5126 self.session_log = Some(session_log.into());
5127 self
5128 }
5129
5130 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5132 self.error = Some(error.into());
5133 self
5134 }
5135
5136 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5138 self.tool_telemetry = Some(telemetry);
5139 self
5140 }
5141
5142 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5144 where
5145 I: IntoIterator<Item = S>,
5146 S: Into<String>,
5147 {
5148 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5149 self
5150 }
5151}
5152
5153#[derive(Debug, Clone, Serialize, Deserialize)]
5155#[serde(untagged)]
5156#[non_exhaustive]
5157pub enum ToolResult {
5158 Text(String),
5160 Expanded(ToolResultExpanded),
5162}
5163
5164#[derive(Debug, Clone, Serialize, Deserialize)]
5166#[serde(rename_all = "camelCase")]
5167pub struct ToolResultResponse {
5168 pub result: ToolResult,
5170}
5171
5172#[derive(Debug, Clone, Serialize, Deserialize)]
5174#[serde(rename_all = "camelCase")]
5175pub struct SessionMetadata {
5176 pub session_id: SessionId,
5178 pub start_time: String,
5180 pub modified_time: String,
5182 #[serde(skip_serializing_if = "Option::is_none")]
5184 pub summary: Option<String>,
5185 pub is_remote: bool,
5187}
5188
5189#[derive(Debug, Clone, Serialize, Deserialize)]
5191#[serde(rename_all = "camelCase")]
5192pub struct ListSessionsResponse {
5193 pub sessions: Vec<SessionMetadata>,
5195}
5196
5197#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5201#[serde(rename_all = "camelCase")]
5202pub struct SessionListFilter {
5203 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5205 pub working_directory: Option<String>,
5206 #[serde(default, skip_serializing_if = "Option::is_none")]
5208 pub git_root: Option<String>,
5209 #[serde(default, skip_serializing_if = "Option::is_none")]
5211 pub repository: Option<String>,
5212 #[serde(default, skip_serializing_if = "Option::is_none")]
5214 pub branch: Option<String>,
5215}
5216
5217#[derive(Debug, Clone, Serialize, Deserialize)]
5219#[serde(rename_all = "camelCase")]
5220pub struct GetSessionMetadataResponse {
5221 #[serde(skip_serializing_if = "Option::is_none")]
5223 pub session: Option<SessionMetadata>,
5224}
5225
5226#[derive(Debug, Clone, Serialize, Deserialize)]
5228#[serde(rename_all = "camelCase")]
5229pub struct GetLastSessionIdResponse {
5230 #[serde(skip_serializing_if = "Option::is_none")]
5232 pub session_id: Option<SessionId>,
5233}
5234
5235#[derive(Debug, Clone, Serialize, Deserialize)]
5237#[serde(rename_all = "camelCase")]
5238pub struct GetForegroundSessionResponse {
5239 #[serde(skip_serializing_if = "Option::is_none")]
5241 pub session_id: Option<SessionId>,
5242}
5243
5244#[derive(Debug, Clone, Serialize, Deserialize)]
5246#[serde(rename_all = "camelCase")]
5247pub struct GetMessagesResponse {
5248 pub events: Vec<SessionEvent>,
5250}
5251
5252#[derive(Debug, Clone, Serialize, Deserialize)]
5254#[serde(rename_all = "camelCase")]
5255pub struct ElicitationResult {
5256 pub action: String,
5258 #[serde(skip_serializing_if = "Option::is_none")]
5260 pub content: Option<Value>,
5261}
5262
5263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5269#[serde(rename_all = "camelCase")]
5270#[non_exhaustive]
5271pub enum ElicitationMode {
5272 Form,
5274 Url,
5276 #[serde(other)]
5278 Unknown,
5279}
5280
5281#[derive(Debug, Clone, Serialize, Deserialize)]
5288#[serde(rename_all = "camelCase")]
5289pub struct ElicitationRequest {
5290 pub message: String,
5292 #[serde(skip_serializing_if = "Option::is_none")]
5294 pub requested_schema: Option<Value>,
5295 #[serde(skip_serializing_if = "Option::is_none")]
5297 pub mode: Option<ElicitationMode>,
5298 #[serde(skip_serializing_if = "Option::is_none")]
5300 pub elicitation_source: Option<String>,
5301 #[serde(skip_serializing_if = "Option::is_none")]
5303 pub url: Option<String>,
5304}
5305
5306#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5311#[serde(rename_all = "camelCase")]
5312pub struct SessionCapabilities {
5313 #[serde(skip_serializing_if = "Option::is_none")]
5315 pub ui: Option<UiCapabilities>,
5316}
5317
5318#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5320#[serde(rename_all = "camelCase")]
5321pub struct UiCapabilities {
5322 #[serde(skip_serializing_if = "Option::is_none")]
5324 pub elicitation: Option<bool>,
5325 #[serde(skip_serializing_if = "Option::is_none")]
5336 pub mcp_apps: Option<bool>,
5337 #[serde(skip_serializing_if = "Option::is_none")]
5339 pub canvases: Option<bool>,
5340}
5341
5342#[derive(Debug, Clone, Default)]
5344pub struct UiInputOptions<'a> {
5345 pub title: Option<&'a str>,
5347 pub description: Option<&'a str>,
5349 pub min_length: Option<u64>,
5351 pub max_length: Option<u64>,
5353 pub format: Option<InputFormat>,
5355 pub default: Option<&'a str>,
5357}
5358
5359#[derive(Debug, Clone, Copy)]
5361#[non_exhaustive]
5362pub enum InputFormat {
5363 Email,
5365 Uri,
5367 Date,
5369 DateTime,
5371}
5372
5373impl InputFormat {
5374 pub fn as_str(&self) -> &'static str {
5376 match self {
5377 Self::Email => "email",
5378 Self::Uri => "uri",
5379 Self::Date => "date",
5380 Self::DateTime => "date-time",
5381 }
5382 }
5383}
5384
5385pub use crate::generated::api_types::{
5390 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5391 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5392 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5393 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
5394};
5395
5396#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5402#[serde(rename_all = "kebab-case")]
5403#[non_exhaustive]
5404pub enum PermissionRequestKind {
5405 Shell,
5407 Write,
5409 Read,
5411 Url,
5413 Mcp,
5415 CustomTool,
5417 Memory,
5419 Hook,
5421 #[serde(other)]
5424 Unknown,
5425}
5426
5427#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5433#[serde(rename_all = "camelCase")]
5434pub struct PermissionRequestData {
5435 #[serde(default, skip_serializing_if = "Option::is_none")]
5439 pub kind: Option<PermissionRequestKind>,
5440 #[serde(default, skip_serializing_if = "Option::is_none")]
5443 pub tool_call_id: Option<String>,
5444 #[serde(flatten)]
5447 pub extra: Value,
5448}
5449
5450#[derive(Debug, Clone, Serialize, Deserialize)]
5452#[serde(rename_all = "camelCase")]
5453pub struct ExitPlanModeData {
5454 #[serde(default)]
5456 pub summary: String,
5457 #[serde(default, skip_serializing_if = "Option::is_none")]
5459 pub plan_content: Option<String>,
5460 #[serde(default)]
5462 pub actions: Vec<String>,
5463 #[serde(default = "default_recommended_action")]
5465 pub recommended_action: String,
5466}
5467
5468fn default_recommended_action() -> String {
5469 "autopilot".to_string()
5470}
5471
5472impl Default for ExitPlanModeData {
5473 fn default() -> Self {
5474 Self {
5475 summary: String::new(),
5476 plan_content: None,
5477 actions: Vec::new(),
5478 recommended_action: default_recommended_action(),
5479 }
5480 }
5481}
5482
5483#[cfg(test)]
5484mod tests {
5485 use std::collections::HashMap;
5486 use std::path::PathBuf;
5487
5488 use serde_json::json;
5489
5490 use super::{
5491 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5492 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5493 CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry,
5494 ExpFlagValue, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig,
5495 LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration,
5496 NamedProviderConfig, ProviderConfig, ProviderModelConfig, ReasoningSummary,
5497 ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, SystemMessageConfig, Tool,
5498 ToolBinaryResult, ToolResult, ToolResultExpanded, ToolResultResponse,
5499 ensure_attachment_display_names,
5500 };
5501 use crate::generated::session_events::TypedSessionEvent;
5502
5503 #[test]
5504 fn tool_builder_composes() {
5505 let tool = Tool::new("greet")
5506 .with_description("Say hello")
5507 .with_namespaced_name("hello/greet")
5508 .with_instructions("Pass the user's name")
5509 .with_parameters(json!({
5510 "type": "object",
5511 "properties": { "name": { "type": "string" } },
5512 "required": ["name"]
5513 }))
5514 .with_overrides_built_in_tool(true)
5515 .with_skip_permission(true);
5516 assert_eq!(tool.name, "greet");
5517 assert_eq!(tool.description, "Say hello");
5518 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5519 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5520 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5521 assert!(tool.overrides_built_in_tool);
5522 assert!(tool.skip_permission);
5523 }
5524
5525 #[test]
5526 fn tool_defer_serialization() {
5527 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5528 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5529 let value = serde_json::to_value(&tool).unwrap();
5530 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5531
5532 let plain = Tool::new("plain");
5533 let value = serde_json::to_value(&plain).unwrap();
5534 assert!(value.get("defer").is_none());
5535 }
5536
5537 #[test]
5538 fn tool_metadata_serialization() {
5539 use indexmap::IndexMap;
5540
5541 let mut metadata = IndexMap::new();
5542 metadata.insert(
5543 "github.com/copilot:safeForTelemetry".to_string(),
5544 json!({ "name": true, "inputsNames": false }),
5545 );
5546 let tool = Tool::new("lookup").with_metadata(metadata);
5547 let value = serde_json::to_value(&tool).unwrap();
5548 assert_eq!(
5549 value
5550 .get("metadata")
5551 .unwrap()
5552 .get("github.com/copilot:safeForTelemetry")
5553 .unwrap(),
5554 &json!({ "name": true, "inputsNames": false })
5555 );
5556
5557 let plain = Tool::new("plain");
5559 let value = serde_json::to_value(&plain).unwrap();
5560 assert!(value.get("metadata").is_none());
5561 }
5562
5563 #[test]
5564 fn custom_agent_config_builder_with_model() {
5565 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5566 .with_model("claude-haiku-4.5")
5567 .with_display_name("My Agent");
5568 assert_eq!(agent.name, "my-agent");
5569 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5570 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5571 }
5572
5573 #[test]
5574 fn custom_agent_config_serializes_model() {
5575 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5576 let wire = serde_json::to_value(&agent).unwrap();
5577 assert_eq!(wire["model"], "claude-haiku-4.5");
5578 assert_eq!(wire["name"], "model-agent");
5579 }
5580
5581 #[test]
5582 fn custom_agent_config_omits_model_when_none() {
5583 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5584 let wire = serde_json::to_value(&agent).unwrap();
5585 assert!(wire.get("model").is_none());
5586 }
5587
5588 #[test]
5589 fn custom_agent_config_builder_with_reasoning_effort() {
5590 let agent =
5591 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5592 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
5593 }
5594
5595 #[test]
5596 fn custom_agent_config_serializes_reasoning_effort() {
5597 let agent =
5598 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5599 let wire = serde_json::to_value(&agent).unwrap();
5600 assert_eq!(wire["reasoningEffort"], "high");
5601 }
5602
5603 #[test]
5604 fn custom_agent_config_omits_reasoning_effort_when_none() {
5605 let agent = CustomAgentConfig::new("default-agent", "prompt");
5606 let wire = serde_json::to_value(&agent).unwrap();
5607 assert!(wire.get("reasoningEffort").is_none());
5608 }
5609
5610 #[test]
5611 #[should_panic(expected = "tool parameter schema must be a JSON object")]
5612 fn tool_with_parameters_panics_on_non_object_value() {
5613 let _ = Tool::new("noop").with_parameters(json!(null));
5614 }
5615
5616 #[test]
5617 fn tool_result_expanded_serializes_binary_results_for_llm() {
5618 let response = ToolResultResponse {
5619 result: ToolResult::Expanded(ToolResultExpanded {
5620 text_result_for_llm: "rendered chart".to_string(),
5621 result_type: "success".to_string(),
5622 binary_results_for_llm: Some(vec![ToolBinaryResult {
5623 data: "aW1n".to_string(),
5624 mime_type: "image/png".to_string(),
5625 r#type: "image".to_string(),
5626 description: Some("chart preview".to_string()),
5627 }]),
5628 session_log: None,
5629 error: None,
5630 tool_telemetry: None,
5631 tool_references: None,
5632 }),
5633 };
5634
5635 let wire = serde_json::to_value(&response).unwrap();
5636
5637 assert_eq!(
5638 wire,
5639 json!({
5640 "result": {
5641 "textResultForLlm": "rendered chart",
5642 "resultType": "success",
5643 "binaryResultsForLlm": [
5644 {
5645 "data": "aW1n",
5646 "mimeType": "image/png",
5647 "type": "image",
5648 "description": "chart preview"
5649 }
5650 ]
5651 }
5652 })
5653 );
5654 }
5655
5656 #[test]
5657 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
5658 let response = ToolResultResponse {
5659 result: ToolResult::Expanded(ToolResultExpanded {
5660 text_result_for_llm: "ok".to_string(),
5661 result_type: "success".to_string(),
5662 binary_results_for_llm: None,
5663 session_log: None,
5664 error: None,
5665 tool_telemetry: None,
5666 tool_references: None,
5667 }),
5668 };
5669
5670 let wire = serde_json::to_value(&response).unwrap();
5671
5672 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5673 assert!(wire["result"].get("binaryResultsForLlm").is_none());
5674 }
5675
5676 #[test]
5677 fn tool_result_expanded_serializes_tool_references() {
5678 let response = ToolResultResponse {
5679 result: ToolResult::Expanded(
5680 ToolResultExpanded::new("found 2 tools", "success")
5681 .with_tool_references(["get_weather", "check_status"]),
5682 ),
5683 };
5684
5685 let wire = serde_json::to_value(&response).unwrap();
5686
5687 assert_eq!(
5688 wire,
5689 json!({
5690 "result": {
5691 "textResultForLlm": "found 2 tools",
5692 "resultType": "success",
5693 "toolReferences": ["get_weather", "check_status"]
5694 }
5695 })
5696 );
5697 }
5698
5699 #[test]
5700 fn tool_result_expanded_omits_tool_references_when_none() {
5701 let response = ToolResultResponse {
5702 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
5703 };
5704
5705 let wire = serde_json::to_value(&response).unwrap();
5706
5707 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5708 assert!(wire["result"].get("toolReferences").is_none());
5709 }
5710
5711 #[test]
5712 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
5713 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
5716 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
5717
5718 assert_eq!(
5719 expanded.tool_references.as_deref(),
5720 Some(["alpha".to_string(), "beta".to_string()].as_slice())
5721 );
5722 }
5723
5724 #[test]
5725 fn tool_result_expanded_deserializes_tool_references() {
5726 let wire = json!({
5727 "textResultForLlm": "found tools",
5728 "resultType": "success",
5729 "toolReferences": ["alpha", "beta"]
5730 });
5731
5732 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
5733
5734 assert_eq!(
5735 expanded.tool_references.as_deref(),
5736 Some(["alpha".to_string(), "beta".to_string()].as_slice())
5737 );
5738 }
5739
5740 #[test]
5741 fn session_config_default_wire_flags_off_without_handlers() {
5742 let cfg = SessionConfig::default();
5743 assert_eq!(cfg.mcp_oauth_token_storage, None);
5744 let (wire, _runtime) = cfg
5748 .into_wire(Some(SessionId::from("default-flags")))
5749 .expect("default config has no duplicate handlers");
5750 assert!(!wire.request_user_input);
5751 assert!(!wire.request_permission);
5752 assert!(!wire.request_elicitation);
5753 assert!(!wire.request_exit_plan_mode);
5754 assert!(!wire.request_auto_mode_switch);
5755 assert!(!wire.hooks);
5756 assert!(!wire.request_mcp_apps);
5757 }
5758
5759 #[test]
5760 fn resume_session_config_new_wire_flags_off_without_handlers() {
5761 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
5762 assert_eq!(cfg.mcp_oauth_token_storage, None);
5763 let (wire, _runtime) = cfg
5764 .into_wire()
5765 .expect("default resume config has no duplicate handlers");
5766 assert!(!wire.request_user_input);
5767 assert!(!wire.request_permission);
5768 assert!(!wire.request_elicitation);
5769 assert!(!wire.request_exit_plan_mode);
5770 assert!(!wire.request_auto_mode_switch);
5771 assert!(!wire.hooks);
5772 assert!(!wire.request_mcp_apps);
5773 }
5774
5775 #[test]
5776 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5777 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
5778 assert_eq!(cfg.enable_mcp_apps, Some(true));
5779
5780 let (wire, _runtime) = cfg
5781 .into_wire(Some(SessionId::from("enable-mcp-apps")))
5782 .expect("enable_mcp_apps config has no duplicate handlers");
5783 assert!(wire.request_mcp_apps);
5784
5785 let json = serde_json::to_value(&wire).unwrap();
5786 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5787 }
5788
5789 #[test]
5790 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5791 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
5792 .with_enable_mcp_apps(true);
5793 assert_eq!(cfg.enable_mcp_apps, Some(true));
5794
5795 let (wire, _runtime) = cfg
5796 .into_wire()
5797 .expect("resume enable_mcp_apps config has no duplicate handlers");
5798 assert!(wire.request_mcp_apps);
5799
5800 let json = serde_json::to_value(&wire).unwrap();
5801 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5802 }
5803
5804 #[test]
5805 fn memory_configuration_constructors_and_serde() {
5806 assert!(MemoryConfiguration::enabled().enabled);
5807 assert!(!MemoryConfiguration::disabled().enabled);
5808 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
5809
5810 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
5811 assert_eq!(json, serde_json::json!({ "enabled": true }));
5812 }
5813
5814 #[test]
5815 fn session_config_with_memory_serializes() {
5816 let (wire, _runtime) = SessionConfig::default()
5817 .with_memory(MemoryConfiguration::enabled())
5818 .into_wire(Some(SessionId::from("memory-on")))
5819 .expect("no duplicate handlers");
5820 let json = serde_json::to_value(&wire).unwrap();
5821 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5822
5823 let (wire_off, _) = SessionConfig::default()
5824 .with_memory(MemoryConfiguration::disabled())
5825 .into_wire(Some(SessionId::from("memory-off")))
5826 .expect("no duplicate handlers");
5827 let json_off = serde_json::to_value(&wire_off).unwrap();
5828 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
5829
5830 let (empty_wire, _) = SessionConfig::default()
5832 .into_wire(Some(SessionId::from("memory-unset")))
5833 .expect("no duplicate handlers");
5834 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5835 assert!(empty_json.get("memory").is_none());
5836 }
5837
5838 #[test]
5839 fn resume_session_config_with_memory_serializes() {
5840 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
5841 .with_memory(MemoryConfiguration::enabled())
5842 .into_wire()
5843 .expect("no duplicate handlers");
5844 let json = serde_json::to_value(&wire).unwrap();
5845 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5846
5847 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
5849 .into_wire()
5850 .expect("no duplicate handlers");
5851 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5852 assert!(empty_json.get("memory").is_none());
5853 }
5854
5855 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
5856 CopilotExpAssignmentResponse {
5857 features: vec!["copilot_exp_flag".to_string()],
5858 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
5859 configs: vec![ExpConfigEntry {
5860 id: "cfg-1".to_string(),
5861 parameters: HashMap::from([
5862 ("threshold".to_string(), ExpFlagValue::Integer(5)),
5863 ("enabled".to_string(), ExpFlagValue::Bool(true)),
5864 ]),
5865 }],
5866 assignment_context: context.to_string(),
5867 ..Default::default()
5868 }
5869 }
5870
5871 #[test]
5872 fn exp_flag_value_round_trips_all_variants() {
5873 let values = serde_json::json!({
5874 "s": "text",
5875 "i": 7,
5876 "f": 1.5,
5877 "b": true,
5878 "n": null,
5879 });
5880 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
5881 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
5882 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
5883 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
5884 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
5885 assert_eq!(parsed["n"], ExpFlagValue::Null);
5886 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
5887 }
5888
5889 #[test]
5890 fn session_config_with_exp_assignments_serializes() {
5891 let assignments = sample_exp_assignments("ctx-123");
5892 let expected = serde_json::to_value(&assignments).unwrap();
5893 let (wire, _runtime) = SessionConfig::default()
5894 .with_exp_assignments(assignments)
5895 .into_wire(Some(SessionId::from("exp-on")))
5896 .expect("no duplicate handlers");
5897 let json = serde_json::to_value(&wire).unwrap();
5898 assert_eq!(json["expAssignments"], expected);
5899 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
5900 assert_eq!(
5901 json["expAssignments"]["Flights"]["copilot_exp_flag"],
5902 "treatment"
5903 );
5904
5905 let (empty_wire, _) = SessionConfig::default()
5907 .into_wire(Some(SessionId::from("exp-unset")))
5908 .expect("no duplicate handlers");
5909 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5910 assert!(empty_json.get("expAssignments").is_none());
5911 }
5912
5913 #[test]
5914 fn resume_session_config_with_exp_assignments_serializes() {
5915 let assignments = sample_exp_assignments("ctx-456");
5916 let expected = serde_json::to_value(&assignments).unwrap();
5917 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
5918 .with_exp_assignments(assignments)
5919 .into_wire()
5920 .expect("no duplicate handlers");
5921 let json = serde_json::to_value(&wire).unwrap();
5922 assert_eq!(json["expAssignments"], expected);
5923
5924 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
5926 .into_wire()
5927 .expect("no duplicate handlers");
5928 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5929 assert!(empty_json.get("expAssignments").is_none());
5930 }
5931
5932 #[test]
5933 fn session_config_clone_preserves_exp_assignments() {
5934 let assignments = sample_exp_assignments("ctx-clone");
5935 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
5936 let cloned = config.clone();
5937
5938 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5939
5940 let (wire, _runtime) = cloned
5941 .into_wire(Some(SessionId::from("exp-clone")))
5942 .expect("no duplicate handlers");
5943 let json = serde_json::to_value(&wire).unwrap();
5944 assert_eq!(
5945 json["expAssignments"],
5946 serde_json::to_value(&assignments).unwrap()
5947 );
5948 }
5949
5950 #[test]
5951 fn resume_session_config_clone_preserves_exp_assignments() {
5952 let assignments = sample_exp_assignments("ctx-clone-resume");
5953 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
5954 .with_exp_assignments(assignments.clone());
5955 let cloned = config.clone();
5956
5957 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5958
5959 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
5960 let json = serde_json::to_value(&wire).unwrap();
5961 assert_eq!(
5962 json["expAssignments"],
5963 serde_json::to_value(&assignments).unwrap()
5964 );
5965 }
5966
5967 #[test]
5968 #[allow(clippy::field_reassign_with_default)]
5969 fn session_config_into_wire_serializes_bucket_b_fields() {
5970 use std::path::PathBuf;
5971
5972 use super::{CloudSessionOptions, CloudSessionRepository};
5973
5974 let mut cfg = SessionConfig::default();
5975 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5976 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5977 cfg.github_token = Some("ghs_secret".to_string());
5978 cfg.include_sub_agent_streaming_events = Some(false);
5979 cfg.enable_session_telemetry = Some(false);
5980 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
5981 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
5982 cfg.enable_on_demand_instruction_discovery = Some(false);
5983 cfg.cloud = Some(CloudSessionOptions::with_repository(
5984 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
5985 ));
5986
5987 let (wire, _runtime) = cfg
5988 .into_wire(Some(SessionId::from("custom-id")))
5989 .expect("no duplicate handlers");
5990 let wire_json = serde_json::to_value(&wire).unwrap();
5991 assert_eq!(wire_json["sessionId"], "custom-id");
5992 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5993 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5994 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5995 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
5996 assert_eq!(wire_json["enableSessionTelemetry"], false);
5997 assert_eq!(wire_json["reasoningSummary"], "concise");
5998 assert_eq!(wire_json["remoteSession"], "export");
5999 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6000 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6001 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6002 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6003
6004 let (empty_wire, _) = SessionConfig::default()
6006 .into_wire(Some(SessionId::from("empty")))
6007 .expect("default has no duplicate handlers");
6008 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6009 assert!(empty_json.get("gitHubToken").is_none());
6010 assert!(empty_json.get("enableSessionTelemetry").is_none());
6011 assert!(empty_json.get("reasoningSummary").is_none());
6012 assert!(empty_json.get("remoteSession").is_none());
6013 assert!(
6014 empty_json
6015 .get("enableOnDemandInstructionDiscovery")
6016 .is_none()
6017 );
6018 assert!(empty_json.get("cloud").is_none());
6019 }
6020
6021 #[test]
6022 fn session_config_into_wire_serializes_named_providers_and_models() {
6023 let cfg = SessionConfig::default()
6024 .with_providers(vec![
6025 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6026 .with_provider_type("openai")
6027 .with_wire_api("responses")
6028 .with_api_key("sk-test"),
6029 ])
6030 .with_models(vec![
6031 ProviderModelConfig::new("gpt-x", "my-openai")
6032 .with_wire_model("gpt-x-2025")
6033 .with_max_output_tokens(2048),
6034 ]);
6035
6036 let (wire, _) = cfg
6037 .into_wire(Some(SessionId::from("sess-providers")))
6038 .expect("no duplicate handlers");
6039 let wire_json = serde_json::to_value(&wire).unwrap();
6040 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6041 assert_eq!(
6042 wire_json["providers"][0]["baseUrl"],
6043 "https://api.example.com/v1"
6044 );
6045 assert_eq!(wire_json["providers"][0]["type"], "openai");
6046 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6047 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6048 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
6049 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
6050 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
6051 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
6052
6053 let (empty_wire, _) = SessionConfig::default()
6054 .into_wire(Some(SessionId::from("empty")))
6055 .expect("default has no duplicate handlers");
6056 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6057 assert!(empty_json.get("providers").is_none());
6058 assert!(empty_json.get("models").is_none());
6059 }
6060
6061 #[test]
6062 fn resume_config_into_wire_serializes_named_providers_and_models() {
6063 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
6064 .with_providers(vec![
6065 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
6066 .with_provider_type("azure")
6067 .with_azure(AzureProviderOptions {
6068 api_version: Some("2024-10-21".to_string()),
6069 }),
6070 ])
6071 .with_models(vec![
6072 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
6073 ]);
6074
6075 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6076 let wire_json = serde_json::to_value(&wire).unwrap();
6077 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
6078 assert_eq!(wire_json["providers"][0]["type"], "azure");
6079 assert_eq!(
6080 wire_json["providers"][0]["azure"]["apiVersion"],
6081 "2024-10-21"
6082 );
6083 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
6084 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
6085 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
6086
6087 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
6088 .into_wire()
6089 .expect("default has no duplicate handlers");
6090 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6091 assert!(empty_json.get("providers").is_none());
6092 assert!(empty_json.get("models").is_none());
6093 }
6094
6095 #[test]
6096 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
6097 use std::path::PathBuf;
6098
6099 let cfg = SessionConfig {
6100 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
6101 large_output: Some(
6102 LargeToolOutputConfig::new()
6103 .with_enabled(true)
6104 .with_max_size_bytes(1024)
6105 .with_output_directory(PathBuf::from("/tmp/large-output")),
6106 ),
6107 ..Default::default()
6108 };
6109
6110 let (wire, _) = cfg
6111 .into_wire(Some(SessionId::from("sess-1")))
6112 .expect("no duplicate handlers");
6113 let wire_json = serde_json::to_value(&wire).unwrap();
6114 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
6115 assert_eq!(wire_json["largeOutput"]["enabled"], true);
6116 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
6117 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
6118
6119 let (empty_wire, _) = SessionConfig::default()
6120 .into_wire(Some(SessionId::from("empty")))
6121 .expect("default has no duplicate handlers");
6122 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6123 assert!(empty_json.get("pluginDirectories").is_none());
6124 assert!(empty_json.get("largeOutput").is_none());
6125 }
6126
6127 #[test]
6128 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
6129 use std::path::PathBuf;
6130
6131 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6132 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6133 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6134 cfg.github_token = Some("ghs_secret".to_string());
6135 cfg.include_sub_agent_streaming_events = Some(true);
6136 cfg.enable_session_telemetry = Some(false);
6137 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
6138 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
6139 cfg.enable_on_demand_instruction_discovery = Some(false);
6140
6141 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6142 let wire_json = serde_json::to_value(&wire).unwrap();
6143 assert_eq!(wire_json["sessionId"], "sess-1");
6144 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6145 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6146 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6147 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
6148 assert_eq!(wire_json["enableSessionTelemetry"], false);
6149 assert_eq!(wire_json["reasoningSummary"], "detailed");
6150 assert_eq!(wire_json["remoteSession"], "on");
6151 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6152
6153 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6155 .into_wire()
6156 .expect("default resume has no duplicate handlers");
6157 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6158 assert!(empty_json.get("reasoningSummary").is_none());
6159 assert!(empty_json.get("remoteSession").is_none());
6160 assert!(
6161 empty_json
6162 .get("enableOnDemandInstructionDiscovery")
6163 .is_none()
6164 );
6165 }
6166
6167 #[test]
6168 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
6169 use std::path::PathBuf;
6170
6171 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6172 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6173 cfg.large_output = Some(
6174 LargeToolOutputConfig::new()
6175 .with_enabled(false)
6176 .with_max_size_bytes(2048)
6177 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6178 );
6179
6180 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6181 let wire_json = serde_json::to_value(&wire).unwrap();
6182 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6183 assert_eq!(wire_json["largeOutput"]["enabled"], false);
6184 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6185 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6186
6187 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6188 .into_wire()
6189 .expect("default resume has no duplicate handlers");
6190 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6191 assert!(empty_json.get("pluginDirectories").is_none());
6192 assert!(empty_json.get("largeOutput").is_none());
6193 }
6194
6195 #[test]
6196 fn session_config_builder_composes() {
6197 use indexmap::IndexMap;
6198
6199 let cfg = SessionConfig::default()
6200 .with_session_id(SessionId::from("sess-1"))
6201 .with_model("claude-sonnet-4")
6202 .with_client_name("test-app")
6203 .with_reasoning_effort("medium")
6204 .with_reasoning_summary(ReasoningSummary::Concise)
6205 .with_context_tier("long_context")
6206 .with_streaming(true)
6207 .with_tools([Tool::new("greet")])
6208 .with_available_tools(["bash", "view"])
6209 .with_excluded_tools(["dangerous"])
6210 .with_mcp_servers(IndexMap::new())
6211 .with_mcp_oauth_token_storage("persistent")
6212 .with_enable_config_discovery(true)
6213 .with_enable_on_demand_instruction_discovery(true)
6214 .with_skill_directories([PathBuf::from("/tmp/skills")])
6215 .with_disabled_skills(["broken-skill"])
6216 .with_agent("researcher")
6217 .with_config_directory(PathBuf::from("/tmp/config"))
6218 .with_working_directory(PathBuf::from("/tmp/work"))
6219 .with_github_token("ghp_test")
6220 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6221 .with_enable_session_telemetry(false)
6222 .with_include_sub_agent_streaming_events(false)
6223 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6224
6225 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
6226 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
6227 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6228 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
6229 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
6230 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
6231 assert_eq!(cfg.streaming, Some(true));
6232 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6233 assert_eq!(
6234 cfg.available_tools.as_deref(),
6235 Some(&["bash".to_string(), "view".to_string()][..])
6236 );
6237 assert_eq!(
6238 cfg.excluded_tools.as_deref(),
6239 Some(&["dangerous".to_string()][..])
6240 );
6241 assert!(cfg.mcp_servers.is_some());
6242 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6243 assert_eq!(cfg.enable_config_discovery, Some(true));
6244 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
6245 assert_eq!(
6246 cfg.skill_directories.as_deref(),
6247 Some(&[PathBuf::from("/tmp/skills")][..])
6248 );
6249 assert_eq!(
6250 cfg.disabled_skills.as_deref(),
6251 Some(&["broken-skill".to_string()][..])
6252 );
6253 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6254 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6255 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6256 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6257 assert_eq!(
6258 cfg.capi,
6259 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6260 );
6261 assert_eq!(cfg.enable_session_telemetry, Some(false));
6262 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
6263 assert_eq!(
6264 cfg.extension_info,
6265 Some(ExtensionInfo::new("github-app", "counter"))
6266 );
6267 }
6268
6269 #[test]
6270 fn resume_session_config_builder_composes() {
6271 use indexmap::IndexMap;
6272
6273 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
6274 .with_client_name("test-app")
6275 .with_reasoning_summary(ReasoningSummary::None)
6276 .with_context_tier("default")
6277 .with_streaming(true)
6278 .with_tools([Tool::new("greet")])
6279 .with_available_tools(["bash", "view"])
6280 .with_excluded_tools(["dangerous"])
6281 .with_mcp_servers(IndexMap::new())
6282 .with_mcp_oauth_token_storage("persistent")
6283 .with_enable_config_discovery(true)
6284 .with_enable_on_demand_instruction_discovery(false)
6285 .with_skill_directories([PathBuf::from("/tmp/skills")])
6286 .with_disabled_skills(["broken-skill"])
6287 .with_agent("researcher")
6288 .with_config_directory(PathBuf::from("/tmp/config"))
6289 .with_working_directory(PathBuf::from("/tmp/work"))
6290 .with_github_token("ghp_test")
6291 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6292 .with_enable_session_telemetry(false)
6293 .with_include_sub_agent_streaming_events(true)
6294 .with_suppress_resume_event(true)
6295 .with_continue_pending_work(true)
6296 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6297
6298 assert_eq!(cfg.session_id.as_str(), "sess-2");
6299 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6300 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
6301 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
6302 assert_eq!(cfg.streaming, Some(true));
6303 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6304 assert_eq!(
6305 cfg.available_tools.as_deref(),
6306 Some(&["bash".to_string(), "view".to_string()][..])
6307 );
6308 assert_eq!(
6309 cfg.excluded_tools.as_deref(),
6310 Some(&["dangerous".to_string()][..])
6311 );
6312 assert!(cfg.mcp_servers.is_some());
6313 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6314 assert_eq!(cfg.enable_config_discovery, Some(true));
6315 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
6316 assert_eq!(
6317 cfg.skill_directories.as_deref(),
6318 Some(&[PathBuf::from("/tmp/skills")][..])
6319 );
6320 assert_eq!(
6321 cfg.disabled_skills.as_deref(),
6322 Some(&["broken-skill".to_string()][..])
6323 );
6324 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6325 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6326 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6327 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6328 assert_eq!(
6329 cfg.capi,
6330 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6331 );
6332 assert_eq!(cfg.enable_session_telemetry, Some(false));
6333 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
6334 assert_eq!(cfg.suppress_resume_event, Some(true));
6335 assert_eq!(cfg.continue_pending_work, Some(true));
6336 assert_eq!(
6337 cfg.extension_info,
6338 Some(ExtensionInfo::new("github-app", "counter"))
6339 );
6340 }
6341
6342 #[test]
6346 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
6347 let cfg =
6348 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
6349 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6350 let json = serde_json::to_value(&wire).unwrap();
6351 assert_eq!(json["continuePendingWork"], true);
6352
6353 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6355 .into_wire()
6356 .expect("no duplicate handlers");
6357 let json = serde_json::to_value(&wire).unwrap();
6358 assert!(json.get("continuePendingWork").is_none());
6359 }
6360
6361 #[test]
6365 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
6366 let cfg =
6367 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
6368 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6369 let json = serde_json::to_value(&wire).unwrap();
6370 assert_eq!(json["disableResume"], true);
6371 assert!(json.get("suppressResumeEvent").is_none());
6372 }
6373
6374 #[test]
6377 fn session_config_serializes_instruction_directories_to_camel_case() {
6378 let cfg =
6379 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
6380 let (wire, _) = cfg
6381 .into_wire(Some(SessionId::from("instr-on")))
6382 .expect("no duplicate handlers");
6383 let json = serde_json::to_value(&wire).unwrap();
6384 assert_eq!(
6385 json["instructionDirectories"],
6386 serde_json::json!(["/tmp/instr"])
6387 );
6388
6389 let (wire, _) = SessionConfig::default()
6391 .into_wire(Some(SessionId::from("instr-off")))
6392 .expect("no duplicate handlers");
6393 let json = serde_json::to_value(&wire).unwrap();
6394 assert!(json.get("instructionDirectories").is_none());
6395 }
6396
6397 #[test]
6400 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
6401 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
6402 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
6403 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6404 let json = serde_json::to_value(&wire).unwrap();
6405 assert_eq!(
6406 json["instructionDirectories"],
6407 serde_json::json!(["/tmp/instr"])
6408 );
6409
6410 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6411 .into_wire()
6412 .expect("no duplicate handlers");
6413 let json = serde_json::to_value(&wire).unwrap();
6414 assert!(json.get("instructionDirectories").is_none());
6415 }
6416
6417 #[test]
6418 fn custom_agent_config_builder_composes() {
6419 use indexmap::IndexMap;
6420
6421 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
6422 .with_display_name("Research Assistant")
6423 .with_description("Investigates technical questions.")
6424 .with_tools(["bash", "view"])
6425 .with_mcp_servers(IndexMap::new())
6426 .with_infer(true)
6427 .with_skills(["rust-coding-skill"]);
6428
6429 assert_eq!(cfg.name, "researcher");
6430 assert_eq!(cfg.prompt, "You are a research assistant.");
6431 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
6432 assert_eq!(
6433 cfg.description.as_deref(),
6434 Some("Investigates technical questions.")
6435 );
6436 assert_eq!(
6437 cfg.tools.as_deref(),
6438 Some(&["bash".to_string(), "view".to_string()][..])
6439 );
6440 assert!(cfg.mcp_servers.is_some());
6441 assert_eq!(cfg.infer, Some(true));
6442 assert_eq!(
6443 cfg.skills.as_deref(),
6444 Some(&["rust-coding-skill".to_string()][..])
6445 );
6446 }
6447
6448 #[test]
6449 fn mcp_servers_serialize_in_insertion_order() {
6450 use indexmap::IndexMap;
6451
6452 let order = [
6458 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
6459 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
6460 ];
6461 let mut servers = IndexMap::new();
6462 for name in order {
6463 servers.insert(
6464 name.to_string(),
6465 McpServerConfig::Stdio(McpStdioServerConfig {
6466 command: "run".to_string(),
6467 ..Default::default()
6468 }),
6469 );
6470 }
6471
6472 let (wire, _runtime) = SessionConfig::default()
6473 .with_mcp_servers(servers)
6474 .into_wire(None)
6475 .expect("into_wire should succeed");
6476 let json = serde_json::to_string(&wire).expect("serialize wire");
6477
6478 let positions: Vec<usize> = order
6479 .iter()
6480 .map(|name| {
6481 json.find(&format!("\"{name}\""))
6482 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
6483 })
6484 .collect();
6485 let mut ascending = positions.clone();
6486 ascending.sort_unstable();
6487 assert_eq!(
6488 positions, ascending,
6489 "mcp server keys must serialize in insertion order: {json}"
6490 );
6491 }
6492
6493 #[test]
6494 fn infinite_session_config_builder_composes() {
6495 let cfg = InfiniteSessionConfig::new()
6496 .with_enabled(true)
6497 .with_background_compaction_threshold(0.75)
6498 .with_buffer_exhaustion_threshold(0.92);
6499
6500 assert_eq!(cfg.enabled, Some(true));
6501 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
6502 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
6503 }
6504
6505 #[test]
6506 fn provider_config_builder_composes() {
6507 use std::collections::HashMap;
6508
6509 let mut headers = HashMap::new();
6510 headers.insert("X-Custom".to_string(), "value".to_string());
6511
6512 let cfg = ProviderConfig::new("https://api.example.com")
6513 .with_provider_type("openai")
6514 .with_wire_api("completions")
6515 .with_transport("websockets")
6516 .with_api_key("sk-test")
6517 .with_bearer_token("bearer-test")
6518 .with_headers(headers)
6519 .with_model_id("gpt-4")
6520 .with_wire_model("azure-gpt-4-deployment")
6521 .with_max_prompt_tokens(8192)
6522 .with_max_output_tokens(2048);
6523
6524 assert_eq!(cfg.base_url, "https://api.example.com");
6525 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
6526 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
6527 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
6528 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
6529 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
6530 assert_eq!(
6531 cfg.headers
6532 .as_ref()
6533 .and_then(|h| h.get("X-Custom"))
6534 .map(String::as_str),
6535 Some("value"),
6536 );
6537 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
6538 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
6539 assert_eq!(cfg.max_prompt_tokens, Some(8192));
6540 assert_eq!(cfg.max_output_tokens, Some(2048));
6541
6542 let wire = serde_json::to_value(&cfg).unwrap();
6544 assert_eq!(wire["modelId"], "gpt-4");
6545 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
6546 assert_eq!(wire["maxPromptTokens"], 8192);
6547 assert_eq!(wire["maxOutputTokens"], 2048);
6548
6549 let unset = ProviderConfig::new("https://api.example.com");
6550 let wire_unset = serde_json::to_value(&unset).unwrap();
6551 assert!(wire_unset.get("modelId").is_none());
6552 assert!(wire_unset.get("wireModel").is_none());
6553 assert!(wire_unset.get("maxPromptTokens").is_none());
6554 assert!(wire_unset.get("maxOutputTokens").is_none());
6555 }
6556
6557 #[test]
6558 fn capi_session_options_builder_composes_and_serializes() {
6559 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
6560
6561 assert_eq!(cfg.enable_web_socket_responses, Some(false));
6562
6563 let wire = serde_json::to_value(&cfg).unwrap();
6564 assert_eq!(
6565 wire,
6566 serde_json::json!({ "enableWebSocketResponses": false })
6567 );
6568
6569 let unset = CapiSessionOptions::new();
6570 let wire_unset = serde_json::to_value(&unset).unwrap();
6571 assert!(wire_unset.get("enableWebSocketResponses").is_none());
6572 }
6573
6574 #[test]
6575 fn session_config_with_capi_serializes() {
6576 let (wire, _) = SessionConfig::default()
6577 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6578 .into_wire(Some(SessionId::from("capi-create")))
6579 .expect("no duplicate handlers");
6580 let json = serde_json::to_value(&wire).unwrap();
6581 assert_eq!(
6582 json["capi"],
6583 serde_json::json!({ "enableWebSocketResponses": false })
6584 );
6585
6586 let (empty_wire, _) = SessionConfig::default()
6587 .into_wire(Some(SessionId::from("capi-create-unset")))
6588 .expect("no duplicate handlers");
6589 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6590 assert!(empty_json.get("capi").is_none());
6591 }
6592
6593 #[test]
6594 fn resume_session_config_with_capi_serializes() {
6595 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
6596 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6597 .into_wire()
6598 .expect("no duplicate handlers");
6599 let json = serde_json::to_value(&wire).unwrap();
6600 assert_eq!(
6601 json["capi"],
6602 serde_json::json!({ "enableWebSocketResponses": false })
6603 );
6604
6605 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
6606 .into_wire()
6607 .expect("no duplicate handlers");
6608 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6609 assert!(empty_json.get("capi").is_none());
6610 }
6611
6612 #[test]
6613 fn system_message_config_builder_composes() {
6614 use std::collections::HashMap;
6615
6616 let cfg = SystemMessageConfig::new()
6617 .with_mode("replace")
6618 .with_content("Custom system message.")
6619 .with_sections(HashMap::new());
6620
6621 assert_eq!(cfg.mode.as_deref(), Some("replace"));
6622 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
6623 assert!(cfg.sections.is_some());
6624 }
6625
6626 #[test]
6627 fn delivery_mode_serializes_to_kebab_case_strings() {
6628 assert_eq!(
6629 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
6630 "\"enqueue\""
6631 );
6632 assert_eq!(
6633 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
6634 "\"immediate\""
6635 );
6636 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
6637 assert_eq!(parsed, DeliveryMode::Immediate);
6638 }
6639
6640 #[test]
6641 fn agent_mode_serializes_to_kebab_case_strings() {
6642 assert_eq!(
6643 serde_json::to_string(&AgentMode::Interactive).unwrap(),
6644 "\"interactive\""
6645 );
6646 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
6647 assert_eq!(
6648 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
6649 "\"autopilot\""
6650 );
6651 assert_eq!(
6652 serde_json::to_string(&AgentMode::Shell).unwrap(),
6653 "\"shell\""
6654 );
6655 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
6656 assert_eq!(parsed, AgentMode::Plan);
6657 }
6658
6659 #[test]
6660 fn connection_state_distinguishes_variants() {
6661 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
6664 }
6665
6666 #[test]
6672 fn session_event_round_trips_agent_id_on_envelope() {
6673 let wire = json!({
6674 "id": "evt-1",
6675 "timestamp": "2026-04-30T12:00:00Z",
6676 "parentId": null,
6677 "agentId": "sub-agent-42",
6678 "type": "assistant.message",
6679 "data": { "message": "hi" }
6680 });
6681
6682 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
6683 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6684
6685 let roundtripped = serde_json::to_value(&event).unwrap();
6687 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6688
6689 let main_agent_event: SessionEvent = serde_json::from_value(json!({
6691 "id": "evt-2",
6692 "timestamp": "2026-04-30T12:00:01Z",
6693 "parentId": null,
6694 "type": "session.idle",
6695 "data": {}
6696 }))
6697 .unwrap();
6698 assert!(main_agent_event.agent_id.is_none());
6699 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
6700 assert!(roundtripped.get("agentId").is_none());
6701 }
6702
6703 #[test]
6705 fn typed_session_event_round_trips_agent_id_on_envelope() {
6706 let wire = json!({
6707 "id": "evt-1",
6708 "timestamp": "2026-04-30T12:00:00Z",
6709 "parentId": null,
6710 "agentId": "sub-agent-42",
6711 "type": "session.idle",
6712 "data": {}
6713 });
6714
6715 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
6716 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6717
6718 let roundtripped = serde_json::to_value(&event).unwrap();
6719 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6720 }
6721
6722 #[test]
6723 fn connection_state_variants_compile() {
6724 let _ = ConnectionState::Disconnected;
6728 let _ = ConnectionState::Connecting;
6729 let _ = ConnectionState::Connected;
6730 let _ = ConnectionState::Error;
6731 }
6732
6733 #[test]
6734 fn deserializes_runtime_attachment_variants() {
6735 let attachments: Vec<Attachment> = serde_json::from_value(json!([
6736 {
6737 "type": "file",
6738 "path": "/tmp/file.rs",
6739 "displayName": "file.rs",
6740 "lineRange": { "start": 7, "end": 12 }
6741 },
6742 {
6743 "type": "directory",
6744 "path": "/tmp/project",
6745 "displayName": "project"
6746 },
6747 {
6748 "type": "selection",
6749 "filePath": "/tmp/lib.rs",
6750 "displayName": "lib.rs",
6751 "text": "fn main() {}",
6752 "selection": {
6753 "start": { "line": 1, "character": 2 },
6754 "end": { "line": 3, "character": 4 }
6755 }
6756 },
6757 {
6758 "type": "blob",
6759 "data": "Zm9v",
6760 "mimeType": "image/png",
6761 "displayName": "image.png"
6762 },
6763 {
6764 "type": "github_reference",
6765 "number": 42,
6766 "title": "Fix rendering",
6767 "referenceType": "issue",
6768 "state": "open",
6769 "url": "https://github.com/example/repo/issues/42"
6770 }
6771 ]))
6772 .expect("attachments should deserialize");
6773
6774 assert_eq!(attachments.len(), 5);
6775 assert!(matches!(
6776 &attachments[0],
6777 Attachment::File {
6778 path,
6779 display_name,
6780 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
6781 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
6782 ));
6783 assert!(matches!(
6784 &attachments[1],
6785 Attachment::Directory { path, display_name }
6786 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
6787 ));
6788 assert!(matches!(
6789 &attachments[2],
6790 Attachment::Selection {
6791 file_path,
6792 display_name,
6793 selection:
6794 AttachmentSelectionRange {
6795 start: AttachmentSelectionPosition { line: 1, character: 2 },
6796 end: AttachmentSelectionPosition { line: 3, character: 4 },
6797 },
6798 ..
6799 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
6800 ));
6801 assert!(matches!(
6802 &attachments[3],
6803 Attachment::Blob {
6804 data,
6805 mime_type,
6806 display_name,
6807 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
6808 ));
6809 assert!(matches!(
6810 &attachments[4],
6811 Attachment::GitHubReference {
6812 number: 42,
6813 title,
6814 reference_type: GitHubReferenceType::Issue,
6815 state,
6816 url,
6817 } if title == "Fix rendering"
6818 && state == "open"
6819 && url == "https://github.com/example/repo/issues/42"
6820 ));
6821 }
6822
6823 #[test]
6824 fn ensures_display_names_for_variants_that_support_them() {
6825 let mut attachments = vec![
6826 Attachment::File {
6827 path: PathBuf::from("/tmp/file.rs"),
6828 display_name: None,
6829 line_range: None,
6830 },
6831 Attachment::Selection {
6832 file_path: PathBuf::from("/tmp/src/lib.rs"),
6833 display_name: None,
6834 text: "fn main() {}".to_string(),
6835 selection: AttachmentSelectionRange {
6836 start: AttachmentSelectionPosition {
6837 line: 0,
6838 character: 0,
6839 },
6840 end: AttachmentSelectionPosition {
6841 line: 0,
6842 character: 10,
6843 },
6844 },
6845 },
6846 Attachment::Blob {
6847 data: "Zm9v".to_string(),
6848 mime_type: "image/png".to_string(),
6849 display_name: None,
6850 },
6851 Attachment::GitHubReference {
6852 number: 7,
6853 title: "Track regressions".to_string(),
6854 reference_type: GitHubReferenceType::Issue,
6855 state: "open".to_string(),
6856 url: "https://example.com/issues/7".to_string(),
6857 },
6858 ];
6859
6860 ensure_attachment_display_names(&mut attachments);
6861
6862 assert_eq!(attachments[0].display_name(), Some("file.rs"));
6863 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
6864 assert_eq!(attachments[2].display_name(), Some("attachment"));
6865 assert_eq!(attachments[3].display_name(), None);
6866 assert_eq!(
6867 attachments[3].label(),
6868 Some("Track regressions".to_string())
6869 );
6870 }
6871
6872 #[test]
6873 fn github_anchored_attachment_variants_round_trip() {
6874 let cases = vec![
6875 (
6876 "github_commit",
6877 json!({
6878 "type": "github_commit",
6879 "message": "Fix the thing",
6880 "oid": "abc123",
6881 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
6882 "url": "https://github.com/octocat/repo/commit/abc123"
6883 }),
6884 ),
6885 (
6886 "github_release",
6887 json!({
6888 "type": "github_release",
6889 "name": "v1.2.3",
6890 "repo": { "name": "repo", "owner": "octocat" },
6891 "tagName": "v1.2.3",
6892 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
6893 }),
6894 ),
6895 (
6896 "github_actions_job",
6897 json!({
6898 "type": "github_actions_job",
6899 "conclusion": "failure",
6900 "jobId": 99,
6901 "jobName": "build",
6902 "repo": { "name": "repo", "owner": "octocat" },
6903 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
6904 "workflowName": "CI"
6905 }),
6906 ),
6907 (
6908 "github_repository",
6909 json!({
6910 "type": "github_repository",
6911 "description": "An example repository",
6912 "ref": "main",
6913 "repo": { "name": "repo", "owner": "octocat" },
6914 "url": "https://github.com/octocat/repo"
6915 }),
6916 ),
6917 (
6918 "github_file_diff",
6919 json!({
6920 "type": "github_file_diff",
6921 "base": {
6922 "path": "src/lib.rs",
6923 "ref": "main",
6924 "repo": { "name": "repo", "owner": "octocat" }
6925 },
6926 "head": {
6927 "path": "src/lib.rs",
6928 "ref": "feature",
6929 "repo": { "name": "repo", "owner": "octocat" }
6930 },
6931 "url": "https://github.com/octocat/repo/compare/main...feature"
6932 }),
6933 ),
6934 (
6935 "github_tree_comparison",
6936 json!({
6937 "type": "github_tree_comparison",
6938 "base": {
6939 "repo": { "name": "repo", "owner": "octocat" },
6940 "revision": "main"
6941 },
6942 "head": {
6943 "repo": { "name": "repo", "owner": "octocat" },
6944 "revision": "feature"
6945 },
6946 "url": "https://github.com/octocat/repo/compare/main...feature"
6947 }),
6948 ),
6949 (
6950 "github_url",
6951 json!({
6952 "type": "github_url",
6953 "url": "https://github.com/octocat/repo/wiki"
6954 }),
6955 ),
6956 (
6957 "github_file",
6958 json!({
6959 "type": "github_file",
6960 "path": "src/main.rs",
6961 "ref": "main",
6962 "repo": { "name": "repo", "owner": "octocat" },
6963 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
6964 }),
6965 ),
6966 (
6967 "github_snippet",
6968 json!({
6969 "type": "github_snippet",
6970 "lineRange": { "start": 10, "end": 20 },
6971 "path": "src/main.rs",
6972 "ref": "main",
6973 "repo": { "name": "repo", "owner": "octocat" },
6974 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
6975 }),
6976 ),
6977 ];
6978
6979 for (expected_type, input) in cases {
6980 let attachment: Attachment = serde_json::from_value(input.clone())
6981 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
6982
6983 let serialized_string = serde_json::to_string(&attachment)
6988 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
6989
6990 assert_eq!(
6992 serialized_string.matches("\"type\":").count(),
6993 1,
6994 "{expected_type} must serialize a single `type` key"
6995 );
6996
6997 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
6998 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
6999 assert_eq!(
7000 serialized.get("type").and_then(|value| value.as_str()),
7001 Some(expected_type),
7002 "{expected_type} must serialize the correct discriminator"
7003 );
7004
7005 assert_eq!(
7007 serialized, input,
7008 "{expected_type} should round-trip without data loss"
7009 );
7010 let reparsed: Attachment = serde_json::from_value(serialized)
7011 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
7012 assert_eq!(
7013 reparsed, attachment,
7014 "{expected_type} should re-deserialize to the same value"
7015 );
7016 }
7017 }
7018}
7019
7020#[cfg(test)]
7021mod permission_builder_tests {
7022 use std::sync::Arc;
7023
7024 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
7025 use crate::permission;
7026 use crate::types::{
7027 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
7028 SessionId,
7029 };
7030
7031 fn data() -> PermissionRequestData {
7032 PermissionRequestData {
7033 extra: serde_json::json!({"tool": "shell"}),
7034 ..Default::default()
7035 }
7036 }
7037
7038 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7041 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7042 }
7043
7044 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7045 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7046 }
7047
7048 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
7049 handler
7050 .handle(SessionId::from("s1"), RequestId::new("1"), data())
7051 .await
7052 }
7053
7054 #[tokio::test]
7055 async fn approve_all_with_handler_present_approves() {
7056 let cfg = SessionConfig::default()
7057 .with_permission_handler(Arc::new(ApproveAllHandler))
7058 .approve_all_permissions();
7059 let h = resolve_create(cfg).expect("policy + handler yields handler");
7060 assert!(matches!(
7061 dispatch(&h).await,
7062 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7063 ));
7064 }
7065
7066 #[tokio::test]
7067 async fn approve_all_standalone_produces_handler() {
7068 let cfg = SessionConfig::default().approve_all_permissions();
7069 let h = resolve_create(cfg).expect("policy alone yields handler");
7070 assert!(matches!(
7071 dispatch(&h).await,
7072 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7073 ));
7074 }
7075
7076 #[tokio::test]
7079 async fn approve_all_is_order_independent() {
7080 let a = SessionConfig::default()
7081 .with_permission_handler(Arc::new(ApproveAllHandler))
7082 .approve_all_permissions();
7083 let b = SessionConfig::default()
7084 .approve_all_permissions()
7085 .with_permission_handler(Arc::new(ApproveAllHandler));
7086 let ha = resolve_create(a).unwrap();
7087 let hb = resolve_create(b).unwrap();
7088 assert!(matches!(
7089 dispatch(&ha).await,
7090 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7091 ));
7092 assert!(matches!(
7093 dispatch(&hb).await,
7094 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7095 ));
7096 }
7097
7098 #[tokio::test]
7099 async fn deny_all_is_order_independent() {
7100 let a = SessionConfig::default()
7101 .with_permission_handler(Arc::new(ApproveAllHandler))
7102 .deny_all_permissions();
7103 let b = SessionConfig::default()
7104 .deny_all_permissions()
7105 .with_permission_handler(Arc::new(ApproveAllHandler));
7106 let ha = resolve_create(a).unwrap();
7107 let hb = resolve_create(b).unwrap();
7108 assert!(matches!(
7109 dispatch(&ha).await,
7110 PermissionResult::Decision(PermissionDecision::Reject(_))
7111 ));
7112 assert!(matches!(
7113 dispatch(&hb).await,
7114 PermissionResult::Decision(PermissionDecision::Reject(_))
7115 ));
7116 }
7117
7118 #[tokio::test]
7119 async fn approve_permissions_if_consults_predicate() {
7120 let cfg = SessionConfig::default().approve_permissions_if(|d| {
7121 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7122 });
7123 let h = resolve_create(cfg).unwrap();
7124 assert!(matches!(
7125 dispatch(&h).await,
7126 PermissionResult::Decision(PermissionDecision::Reject(_))
7127 ));
7128 }
7129
7130 #[tokio::test]
7131 async fn approve_permissions_if_is_order_independent() {
7132 let predicate = |d: &PermissionRequestData| {
7133 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7134 };
7135 let a = SessionConfig::default()
7136 .with_permission_handler(Arc::new(ApproveAllHandler))
7137 .approve_permissions_if(predicate);
7138 let b = SessionConfig::default()
7139 .approve_permissions_if(predicate)
7140 .with_permission_handler(Arc::new(ApproveAllHandler));
7141 let ha = resolve_create(a).unwrap();
7142 let hb = resolve_create(b).unwrap();
7143 assert!(matches!(
7144 dispatch(&ha).await,
7145 PermissionResult::Decision(PermissionDecision::Reject(_))
7146 ));
7147 assert!(matches!(
7148 dispatch(&hb).await,
7149 PermissionResult::Decision(PermissionDecision::Reject(_))
7150 ));
7151 }
7152
7153 #[tokio::test]
7154 async fn resume_session_config_approve_all_works() {
7155 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
7156 .with_permission_handler(Arc::new(ApproveAllHandler))
7157 .approve_all_permissions();
7158 let h = resolve_resume(cfg).unwrap();
7159 assert!(matches!(
7160 dispatch(&h).await,
7161 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7162 ));
7163 }
7164
7165 #[tokio::test]
7166 async fn resume_session_config_approve_all_is_order_independent() {
7167 let a = ResumeSessionConfig::new(SessionId::from("s1"))
7168 .with_permission_handler(Arc::new(ApproveAllHandler))
7169 .approve_all_permissions();
7170 let b = ResumeSessionConfig::new(SessionId::from("s1"))
7171 .approve_all_permissions()
7172 .with_permission_handler(Arc::new(ApproveAllHandler));
7173 let ha = resolve_resume(a).unwrap();
7174 let hb = resolve_resume(b).unwrap();
7175 assert!(matches!(
7176 dispatch(&ha).await,
7177 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7178 ));
7179 assert!(matches!(
7180 dispatch(&hb).await,
7181 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7182 ));
7183 }
7184}