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