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 = "is_false")]
354 pub is_terminal: bool,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
361 pub defer: Option<DeferMode>,
362 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
367 pub metadata: IndexMap<String, Value>,
368 #[serde(skip)]
380 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
381}
382
383#[inline]
384fn is_false(b: &bool) -> bool {
385 !*b
386}
387
388#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
391#[serde(rename_all = "lowercase")]
392pub enum DeferMode {
393 Auto,
395 Never,
397}
398
399impl Tool {
400 pub fn new(name: impl Into<String>) -> Self {
420 Self {
421 name: name.into(),
422 ..Default::default()
423 }
424 }
425
426 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
429 self.namespaced_name = Some(namespaced_name.into());
430 self
431 }
432
433 pub fn with_description(mut self, description: impl Into<String>) -> Self {
435 self.description = description.into();
436 self
437 }
438
439 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
441 self.instructions = Some(instructions.into());
442 self
443 }
444
445 pub fn with_parameters(mut self, parameters: Value) -> Self {
459 self.parameters = crate::tool::tool_parameters(parameters);
460 self
461 }
462
463 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
467 self.overrides_built_in_tool = overrides;
468 self
469 }
470
471 pub fn with_skip_permission(mut self, skip: bool) -> Self {
475 self.skip_permission = skip;
476 self
477 }
478
479 #[must_use]
486 pub fn with_is_terminal(mut self, is_terminal: bool) -> Self {
487 self.is_terminal = is_terminal;
488 self
489 }
490
491 pub fn with_defer(mut self, defer: DeferMode) -> Self {
495 self.defer = Some(defer);
496 self
497 }
498
499 pub fn with_metadata(mut self, metadata: IndexMap<String, Value>) -> Self {
502 self.metadata = metadata;
503 self
504 }
505
506 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
510 self.handler = Some(handler);
511 self
512 }
513
514 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
519 self.handler.as_ref()
520 }
521}
522
523impl std::fmt::Debug for Tool {
524 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525 f.debug_struct("Tool")
526 .field("name", &self.name)
527 .field("namespaced_name", &self.namespaced_name)
528 .field("description", &self.description)
529 .field("instructions", &self.instructions)
530 .field("parameters", &self.parameters)
531 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
532 .field("skip_permission", &self.skip_permission)
533 .field("is_terminal", &self.is_terminal)
534 .field("defer", &self.defer)
535 .field("metadata", &self.metadata)
536 .field(
537 "handler",
538 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
539 )
540 .finish()
541 }
542}
543
544#[non_exhaustive]
547#[derive(Debug, Clone)]
548pub struct CommandContext {
549 pub session_id: SessionId,
551 pub command: String,
553 pub command_name: String,
555 pub args: String,
557}
558
559#[async_trait::async_trait]
565pub trait CommandHandler: Send + Sync {
566 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
568}
569
570#[non_exhaustive]
576#[derive(Clone)]
577pub struct CommandDefinition {
578 pub name: String,
580 pub description: Option<String>,
582 pub handler: Arc<dyn CommandHandler>,
584}
585
586impl CommandDefinition {
587 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
590 Self {
591 name: name.into(),
592 description: None,
593 handler,
594 }
595 }
596
597 pub fn with_description(mut self, description: impl Into<String>) -> Self {
599 self.description = Some(description.into());
600 self
601 }
602}
603
604impl std::fmt::Debug for CommandDefinition {
605 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
606 f.debug_struct("CommandDefinition")
607 .field("name", &self.name)
608 .field("description", &self.description)
609 .field("handler", &"<set>")
610 .finish()
611 }
612}
613
614impl Serialize for CommandDefinition {
615 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
616 use serde::ser::SerializeStruct;
617 let len = if self.description.is_some() { 2 } else { 1 };
618 let mut state = serializer.serialize_struct("CommandDefinition", len)?;
619 state.serialize_field("name", &self.name)?;
620 if let Some(description) = &self.description {
621 state.serialize_field("description", description)?;
622 }
623 state.end()
624 }
625}
626
627#[derive(Debug, Clone, Default, Serialize, Deserialize)]
634#[serde(rename_all = "camelCase")]
635#[non_exhaustive]
636pub struct CustomAgentConfig {
637 pub name: String,
639 #[serde(default, skip_serializing_if = "Option::is_none")]
641 pub display_name: Option<String>,
642 #[serde(default, skip_serializing_if = "Option::is_none")]
644 pub description: Option<String>,
645 #[serde(default, skip_serializing_if = "Option::is_none")]
647 pub tools: Option<Vec<String>>,
648 pub prompt: String,
650 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub infer: Option<bool>,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub skills: Option<Vec<String>>,
659 #[serde(default, skip_serializing_if = "Option::is_none")]
664 pub model: Option<String>,
665 #[serde(default, skip_serializing_if = "Option::is_none")]
670 pub reasoning_effort: Option<String>,
671}
672
673impl CustomAgentConfig {
674 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
681 Self {
682 name: name.into(),
683 prompt: prompt.into(),
684 ..Self::default()
685 }
686 }
687
688 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
690 self.display_name = Some(display_name.into());
691 self
692 }
693
694 pub fn with_description(mut self, description: impl Into<String>) -> Self {
696 self.description = Some(description.into());
697 self
698 }
699
700 pub fn with_tools<I, S>(mut self, tools: I) -> Self
703 where
704 I: IntoIterator<Item = S>,
705 S: Into<String>,
706 {
707 self.tools = Some(tools.into_iter().map(Into::into).collect());
708 self
709 }
710
711 pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
713 self.mcp_servers = Some(mcp_servers);
714 self
715 }
716
717 pub fn with_infer(mut self, infer: bool) -> Self {
719 self.infer = Some(infer);
720 self
721 }
722
723 pub fn with_skills<I, S>(mut self, skills: I) -> Self
725 where
726 I: IntoIterator<Item = S>,
727 S: Into<String>,
728 {
729 self.skills = Some(skills.into_iter().map(Into::into).collect());
730 self
731 }
732
733 pub fn with_model(mut self, model: impl Into<String>) -> Self {
735 self.model = Some(model.into());
736 self
737 }
738
739 pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into<String>) -> Self {
741 self.reasoning_effort = Some(reasoning_effort.into());
742 self
743 }
744}
745
746#[derive(Debug, Clone, Default, Serialize, Deserialize)]
753#[serde(rename_all = "camelCase")]
754pub struct DefaultAgentConfig {
755 #[serde(default, skip_serializing_if = "Option::is_none")]
757 pub excluded_tools: Option<Vec<String>>,
758}
759
760#[derive(Debug, Clone, Default, Serialize, Deserialize)]
766#[serde(rename_all = "camelCase")]
767#[non_exhaustive]
768pub struct LargeToolOutputConfig {
769 #[serde(default, skip_serializing_if = "Option::is_none")]
771 pub enabled: Option<bool>,
772 #[serde(default, skip_serializing_if = "Option::is_none")]
775 pub max_size_bytes: Option<u64>,
776 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
779 pub output_directory: Option<PathBuf>,
780}
781
782impl LargeToolOutputConfig {
783 pub fn new() -> Self {
786 Self::default()
787 }
788
789 pub fn with_enabled(mut self, enabled: bool) -> Self {
791 self.enabled = Some(enabled);
792 self
793 }
794
795 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
797 self.max_size_bytes = Some(max_size_bytes);
798 self
799 }
800
801 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
803 self.output_directory = Some(output_directory.into());
804 self
805 }
806}
807
808#[derive(Debug, Clone, Default, Serialize, Deserialize)]
814#[serde(rename_all = "camelCase")]
815#[non_exhaustive]
816pub struct ToolSearchConfig {
817 #[serde(default, skip_serializing_if = "Option::is_none")]
819 pub enabled: Option<bool>,
820 #[serde(default, skip_serializing_if = "Option::is_none")]
823 pub defer_threshold: Option<u32>,
824}
825
826impl ToolSearchConfig {
827 pub fn new() -> Self {
830 Self::default()
831 }
832
833 pub fn with_enabled(mut self, enabled: bool) -> Self {
835 self.enabled = Some(enabled);
836 self
837 }
838
839 pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
842 self.defer_threshold = Some(defer_threshold);
843 self
844 }
845}
846
847#[derive(Debug, Clone, Default, Serialize, Deserialize)]
852#[serde(rename_all = "camelCase")]
853#[non_exhaustive]
854pub struct GitHubMcpToolConfig {
855 #[serde(default, skip_serializing_if = "Option::is_none")]
857 pub enable_all_tools: Option<bool>,
858 #[serde(default, skip_serializing_if = "Option::is_none")]
860 pub additional_toolsets: Option<Vec<String>>,
861 #[serde(default, skip_serializing_if = "Option::is_none")]
863 pub additional_tools: Option<Vec<String>>,
864 #[serde(default, skip_serializing_if = "Option::is_none")]
866 pub enable_insiders_mode: Option<bool>,
867 #[serde(default, skip_serializing_if = "Option::is_none")]
871 pub disable_form_deferral: Option<bool>,
872}
873
874impl GitHubMcpToolConfig {
875 pub fn new() -> Self {
877 Self::default()
878 }
879
880 pub fn with_enable_all_tools(mut self, value: bool) -> Self {
882 self.enable_all_tools = Some(value);
883 self
884 }
885
886 pub fn with_additional_toolsets<I, S>(mut self, values: I) -> Self
888 where
889 I: IntoIterator<Item = S>,
890 S: Into<String>,
891 {
892 self.additional_toolsets = Some(values.into_iter().map(Into::into).collect());
893 self
894 }
895
896 pub fn with_additional_tools<I, S>(mut self, values: I) -> Self
898 where
899 I: IntoIterator<Item = S>,
900 S: Into<String>,
901 {
902 self.additional_tools = Some(values.into_iter().map(Into::into).collect());
903 self
904 }
905
906 pub fn with_enable_insiders_mode(mut self, value: bool) -> Self {
908 self.enable_insiders_mode = Some(value);
909 self
910 }
911
912 pub fn with_disable_form_deferral(mut self, value: bool) -> Self {
916 self.disable_form_deferral = Some(value);
917 self
918 }
919}
920
921#[derive(Debug, Clone, Default, Serialize, Deserialize)]
928#[serde(rename_all = "camelCase")]
929#[non_exhaustive]
930pub struct InfiniteSessionConfig {
931 #[serde(default, skip_serializing_if = "Option::is_none")]
933 pub enabled: Option<bool>,
934 #[serde(default, skip_serializing_if = "Option::is_none")]
937 pub background_compaction_threshold: Option<f64>,
938 #[serde(default, skip_serializing_if = "Option::is_none")]
941 pub buffer_exhaustion_threshold: Option<f64>,
942}
943
944impl InfiniteSessionConfig {
945 pub fn new() -> Self {
948 Self::default()
949 }
950
951 pub fn with_enabled(mut self, enabled: bool) -> Self {
954 self.enabled = Some(enabled);
955 self
956 }
957
958 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
961 self.background_compaction_threshold = Some(threshold);
962 self
963 }
964
965 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
968 self.buffer_exhaustion_threshold = Some(threshold);
969 self
970 }
971}
972
973#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
984#[serde(rename_all = "camelCase")]
985#[non_exhaustive]
986pub struct MemoryConfiguration {
987 pub enabled: bool,
989}
990
991impl MemoryConfiguration {
992 pub fn enabled() -> Self {
994 Self { enabled: true }
995 }
996
997 pub fn disabled() -> Self {
999 Self { enabled: false }
1000 }
1001
1002 pub fn with_enabled(mut self, enabled: bool) -> Self {
1004 self.enabled = enabled;
1005 self
1006 }
1007}
1008
1009#[derive(Debug, Clone, Serialize, Deserialize)]
1011#[serde(rename_all = "camelCase")]
1012#[non_exhaustive]
1013pub struct CloudSessionRepository {
1014 pub owner: String,
1016 pub name: String,
1018 #[serde(skip_serializing_if = "Option::is_none")]
1020 pub branch: Option<String>,
1021}
1022
1023impl CloudSessionRepository {
1024 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
1026 Self {
1027 owner: owner.into(),
1028 name: name.into(),
1029 branch: None,
1030 }
1031 }
1032
1033 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
1035 self.branch = Some(branch.into());
1036 self
1037 }
1038}
1039
1040#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1042#[serde(rename_all = "camelCase")]
1043#[non_exhaustive]
1044pub struct CloudSessionOptions {
1045 #[serde(skip_serializing_if = "Option::is_none")]
1047 pub repository: Option<CloudSessionRepository>,
1048}
1049
1050impl CloudSessionOptions {
1051 pub fn with_repository(repository: CloudSessionRepository) -> Self {
1053 Self {
1054 repository: Some(repository),
1055 }
1056 }
1057}
1058
1059#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1061#[serde(rename_all = "camelCase")]
1062pub struct ExtensionInfo {
1063 pub source: String,
1065 pub name: String,
1067}
1068
1069impl ExtensionInfo {
1070 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
1072 Self {
1073 source: source.into(),
1074 name: name.into(),
1075 }
1076 }
1077}
1078
1079#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1090#[serde(rename_all = "camelCase")]
1091pub struct CanvasProviderIdentity {
1092 pub id: String,
1094 #[serde(skip_serializing_if = "Option::is_none")]
1096 pub name: Option<String>,
1097}
1098
1099impl CanvasProviderIdentity {
1100 pub fn new(id: impl Into<String>) -> Self {
1102 Self {
1103 id: id.into(),
1104 name: None,
1105 }
1106 }
1107
1108 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1110 self.name = Some(name.into());
1111 self
1112 }
1113}
1114
1115#[derive(Debug, Clone, Serialize, Deserialize)]
1149#[serde(tag = "type", rename_all = "lowercase")]
1150#[non_exhaustive]
1151pub enum McpServerConfig {
1152 #[serde(alias = "local")]
1156 Stdio(McpStdioServerConfig),
1157 Http(McpHttpServerConfig),
1159 Sse(McpHttpServerConfig),
1161}
1162
1163#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1167#[serde(rename_all = "camelCase")]
1168pub struct McpStdioServerConfig {
1169 #[serde(default, skip_serializing_if = "Option::is_none")]
1175 pub tools: Option<Vec<String>>,
1176 #[serde(default, skip_serializing_if = "Option::is_none")]
1178 pub timeout: Option<i64>,
1179 pub command: String,
1181 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1183 pub args: Vec<String>,
1184 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1187 pub env: HashMap<String, String>,
1188 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1190 pub working_directory: Option<String>,
1191}
1192
1193#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1197#[serde(rename_all = "camelCase")]
1198pub struct McpHttpServerConfig {
1199 #[serde(default, skip_serializing_if = "Option::is_none")]
1205 pub tools: Option<Vec<String>>,
1206 #[serde(default, skip_serializing_if = "Option::is_none")]
1208 pub timeout: Option<i64>,
1209 pub url: String,
1211 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1213 pub headers: HashMap<String, String>,
1214}
1215
1216#[derive(Clone, Default, Serialize, Deserialize)]
1222#[serde(rename_all = "camelCase")]
1223#[non_exhaustive]
1224pub struct ProviderConfig {
1225 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1228 pub provider_type: Option<String>,
1229 #[serde(default, skip_serializing_if = "Option::is_none")]
1232 pub wire_api: Option<String>,
1233 #[serde(default, skip_serializing_if = "Option::is_none")]
1238 pub transport: Option<String>,
1239 pub base_url: String,
1241 #[serde(default, skip_serializing_if = "Option::is_none")]
1243 pub api_key: Option<String>,
1244 #[serde(default, skip_serializing_if = "Option::is_none")]
1248 pub bearer_token: Option<String>,
1249 #[serde(skip)]
1252 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1253 #[serde(default, skip_serializing_if = "Option::is_none")]
1254 pub(crate) has_bearer_token_provider: Option<bool>,
1255 #[serde(default, skip_serializing_if = "Option::is_none")]
1257 pub azure: Option<AzureProviderOptions>,
1258 #[serde(default, skip_serializing_if = "Option::is_none")]
1260 pub headers: Option<HashMap<String, String>>,
1261 #[serde(default, skip_serializing_if = "Option::is_none")]
1265 pub model_id: Option<String>,
1266 #[serde(default, skip_serializing_if = "Option::is_none")]
1273 pub wire_model: Option<String>,
1274 #[serde(default, skip_serializing_if = "Option::is_none")]
1279 pub max_prompt_tokens: Option<i64>,
1280 #[serde(default, skip_serializing_if = "Option::is_none")]
1283 pub max_output_tokens: Option<i64>,
1284}
1285
1286impl std::fmt::Debug for ProviderConfig {
1287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1288 f.debug_struct("ProviderConfig")
1289 .field("provider_type", &self.provider_type)
1290 .field("wire_api", &self.wire_api)
1291 .field("transport", &self.transport)
1292 .field("base_url", &self.base_url)
1293 .field("api_key", &self.api_key)
1294 .field("bearer_token", &self.bearer_token)
1295 .field(
1296 "bearer_token_provider",
1297 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1298 )
1299 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1300 .field("azure", &self.azure)
1301 .field("headers", &self.headers)
1302 .field("model_id", &self.model_id)
1303 .field("wire_model", &self.wire_model)
1304 .field("max_prompt_tokens", &self.max_prompt_tokens)
1305 .field("max_output_tokens", &self.max_output_tokens)
1306 .finish()
1307 }
1308}
1309
1310impl ProviderConfig {
1311 pub fn new(base_url: impl Into<String>) -> Self {
1314 Self {
1315 base_url: base_url.into(),
1316 ..Self::default()
1317 }
1318 }
1319
1320 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1322 self.provider_type = Some(provider_type.into());
1323 self
1324 }
1325
1326 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1328 self.wire_api = Some(wire_api.into());
1329 self
1330 }
1331
1332 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1335 self.transport = Some(transport.into());
1336 self
1337 }
1338
1339 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1341 self.api_key = Some(api_key.into());
1342 self
1343 }
1344
1345 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1348 self.bearer_token = Some(bearer_token.into());
1349 self
1350 }
1351
1352 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1358 self.bearer_token_provider = Some(provider);
1359 self
1360 }
1361
1362 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1364 self.azure = Some(azure);
1365 self
1366 }
1367
1368 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1370 self.headers = Some(headers);
1371 self
1372 }
1373
1374 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1377 self.model_id = Some(model_id.into());
1378 self
1379 }
1380
1381 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1386 self.wire_model = Some(wire_model.into());
1387 self
1388 }
1389
1390 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1394 self.max_prompt_tokens = Some(max);
1395 self
1396 }
1397
1398 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1401 self.max_output_tokens = Some(max);
1402 self
1403 }
1404}
1405
1406#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1419#[serde(rename_all = "camelCase")]
1420#[non_exhaustive]
1421pub struct CapiSessionOptions {
1422 #[serde(default, skip_serializing_if = "Option::is_none")]
1428 pub enable_web_socket_responses: Option<bool>,
1429}
1430
1431impl CapiSessionOptions {
1432 pub fn new() -> Self {
1434 Self::default()
1435 }
1436
1437 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1439 self.enable_web_socket_responses = Some(enable);
1440 self
1441 }
1442}
1443
1444#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1446#[serde(rename_all = "camelCase")]
1447pub struct AzureProviderOptions {
1448 #[serde(default, skip_serializing_if = "Option::is_none")]
1450 pub api_version: Option<String>,
1451}
1452
1453#[derive(Clone, Default, Serialize, Deserialize)]
1464#[serde(rename_all = "camelCase")]
1465#[non_exhaustive]
1466pub struct NamedProviderConfig {
1467 pub name: String,
1470 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1473 pub provider_type: Option<String>,
1474 #[serde(default, skip_serializing_if = "Option::is_none")]
1477 pub wire_api: Option<String>,
1478 pub base_url: String,
1480 #[serde(default, skip_serializing_if = "Option::is_none")]
1482 pub api_key: Option<String>,
1483 #[serde(default, skip_serializing_if = "Option::is_none")]
1486 pub bearer_token: Option<String>,
1487 #[serde(skip)]
1490 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1491 #[serde(default, skip_serializing_if = "Option::is_none")]
1492 pub(crate) has_bearer_token_provider: Option<bool>,
1493 #[serde(default, skip_serializing_if = "Option::is_none")]
1495 pub azure: Option<AzureProviderOptions>,
1496 #[serde(default, skip_serializing_if = "Option::is_none")]
1498 pub headers: Option<HashMap<String, String>>,
1499}
1500
1501impl std::fmt::Debug for NamedProviderConfig {
1502 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1503 f.debug_struct("NamedProviderConfig")
1504 .field("name", &self.name)
1505 .field("provider_type", &self.provider_type)
1506 .field("wire_api", &self.wire_api)
1507 .field("base_url", &self.base_url)
1508 .field("api_key", &self.api_key)
1509 .field("bearer_token", &self.bearer_token)
1510 .field(
1511 "bearer_token_provider",
1512 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1513 )
1514 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1515 .field("azure", &self.azure)
1516 .field("headers", &self.headers)
1517 .finish()
1518 }
1519}
1520
1521impl NamedProviderConfig {
1522 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1525 Self {
1526 name: name.into(),
1527 base_url: base_url.into(),
1528 ..Self::default()
1529 }
1530 }
1531
1532 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1534 self.provider_type = Some(provider_type.into());
1535 self
1536 }
1537
1538 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1540 self.wire_api = Some(wire_api.into());
1541 self
1542 }
1543
1544 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1546 self.api_key = Some(api_key.into());
1547 self
1548 }
1549
1550 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1553 self.bearer_token = Some(bearer_token.into());
1554 self
1555 }
1556
1557 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1563 self.bearer_token_provider = Some(provider);
1564 self
1565 }
1566
1567 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1569 self.azure = Some(azure);
1570 self
1571 }
1572
1573 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1575 self.headers = Some(headers);
1576 self
1577 }
1578}
1579
1580fn prepare_bearer_token_providers(
1581 provider: &mut Option<ProviderConfig>,
1582 providers: &mut Option<Vec<NamedProviderConfig>>,
1583) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1584 let mut bearer_token_providers = HashMap::new();
1585
1586 if let Some(provider) = provider.as_mut()
1587 && let Some(token_provider) = provider.bearer_token_provider.take()
1588 {
1589 provider.has_bearer_token_provider = Some(true);
1590 bearer_token_providers.insert("default".to_string(), token_provider);
1591 }
1592
1593 if let Some(providers) = providers.as_mut() {
1594 for provider in providers {
1595 if let Some(token_provider) = provider.bearer_token_provider.take() {
1596 provider.has_bearer_token_provider = Some(true);
1597 bearer_token_providers.insert(provider.name.clone(), token_provider);
1598 }
1599 }
1600 }
1601
1602 bearer_token_providers
1603}
1604
1605#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1613#[serde(rename_all = "camelCase")]
1614#[non_exhaustive]
1615pub struct ProviderModelConfig {
1616 pub id: String,
1619 pub provider: String,
1621 #[serde(default, skip_serializing_if = "Option::is_none")]
1624 pub wire_model: Option<String>,
1625 #[serde(default, skip_serializing_if = "Option::is_none")]
1628 pub model_id: Option<String>,
1629 #[serde(default, skip_serializing_if = "Option::is_none")]
1631 pub name: Option<String>,
1632 #[serde(default, skip_serializing_if = "Option::is_none")]
1634 pub max_prompt_tokens: Option<i64>,
1635 #[serde(default, skip_serializing_if = "Option::is_none")]
1637 pub max_context_window_tokens: Option<i64>,
1638 #[serde(default, skip_serializing_if = "Option::is_none")]
1640 pub max_output_tokens: Option<i64>,
1641 #[serde(default, skip_serializing_if = "Option::is_none")]
1644 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1645}
1646
1647impl ProviderModelConfig {
1648 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1651 Self {
1652 id: id.into(),
1653 provider: provider.into(),
1654 ..Self::default()
1655 }
1656 }
1657
1658 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1660 self.wire_model = Some(wire_model.into());
1661 self
1662 }
1663
1664 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1667 self.model_id = Some(model_id.into());
1668 self
1669 }
1670
1671 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1673 self.name = Some(name.into());
1674 self
1675 }
1676
1677 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1679 self.max_prompt_tokens = Some(max);
1680 self
1681 }
1682
1683 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1685 self.max_context_window_tokens = Some(max);
1686 self
1687 }
1688
1689 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1691 self.max_output_tokens = Some(max);
1692 self
1693 }
1694
1695 pub fn with_capabilities(
1697 mut self,
1698 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1699 ) -> Self {
1700 self.capabilities = Some(capabilities);
1701 self
1702 }
1703}
1704
1705#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1709#[serde(untagged)]
1710pub enum ExpFlagValue {
1711 Bool(bool),
1713 Integer(i64),
1715 Float(f64),
1717 String(String),
1719 Null,
1721}
1722
1723#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1727#[serde(rename_all = "PascalCase")]
1728pub struct ExpConfigEntry {
1729 pub id: String,
1731 pub parameters: HashMap<String, ExpFlagValue>,
1733}
1734
1735#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1741#[serde(rename_all = "PascalCase")]
1742pub struct CopilotExpAssignmentResponse {
1743 #[serde(default)]
1745 pub features: Vec<String>,
1746 #[serde(default)]
1748 pub flights: HashMap<String, String>,
1749 #[serde(default)]
1751 pub configs: Vec<ExpConfigEntry>,
1752 #[serde(default, skip_serializing_if = "Option::is_none")]
1754 pub parameter_groups: Option<Value>,
1755 #[serde(default, skip_serializing_if = "Option::is_none")]
1757 pub flighting_version: Option<i64>,
1758 #[serde(default, skip_serializing_if = "Option::is_none")]
1760 pub impression_id: Option<String>,
1761 #[serde(default)]
1763 pub assignment_context: String,
1764}
1765
1766#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1768#[serde(rename_all = "lowercase")]
1769#[non_exhaustive]
1770pub enum DisableBypassPermissionsMode {
1771 Disable,
1773}
1774
1775#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1785#[serde(rename_all = "camelCase")]
1786#[non_exhaustive]
1787pub struct ManagedSettingsPermissions {
1788 #[serde(default, skip_serializing_if = "Option::is_none")]
1792 pub disable_bypass_permissions_mode: Option<DisableBypassPermissionsMode>,
1793 #[serde(default, skip_serializing_if = "Option::is_none")]
1795 pub deny: Option<Vec<String>>,
1796 #[serde(default, skip_serializing_if = "Option::is_none")]
1798 pub ask: Option<Vec<String>>,
1799 #[serde(default, skip_serializing_if = "Option::is_none")]
1801 pub allow: Option<Vec<String>>,
1802}
1803
1804impl ManagedSettingsPermissions {
1805 pub fn with_disable_bypass_permissions_mode(
1807 mut self,
1808 value: DisableBypassPermissionsMode,
1809 ) -> Self {
1810 self.disable_bypass_permissions_mode = Some(value);
1811 self
1812 }
1813
1814 pub fn with_deny(mut self, rules: Vec<String>) -> Self {
1816 self.deny = Some(rules);
1817 self
1818 }
1819
1820 pub fn with_ask(mut self, rules: Vec<String>) -> Self {
1822 self.ask = Some(rules);
1823 self
1824 }
1825
1826 pub fn with_allow(mut self, rules: Vec<String>) -> Self {
1828 self.allow = Some(rules);
1829 self
1830 }
1831}
1832
1833#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1843#[serde(rename_all = "camelCase")]
1844#[non_exhaustive]
1845pub struct ManagedSettings {
1846 #[serde(default, skip_serializing_if = "Option::is_none")]
1848 pub permissions: Option<ManagedSettingsPermissions>,
1849}
1850
1851impl ManagedSettings {
1852 pub fn with_permissions(mut self, permissions: ManagedSettingsPermissions) -> Self {
1854 self.permissions = Some(permissions);
1855 self
1856 }
1857}
1858
1859#[derive(Clone)]
1911#[non_exhaustive]
1912pub struct SessionConfig {
1913 pub session_id: Option<SessionId>,
1915 pub model: Option<String>,
1917 pub client_name: Option<String>,
1919 pub reasoning_effort: Option<String>,
1921 pub reasoning_summary: Option<ReasoningSummary>,
1925 pub context_tier: Option<String>,
1928 pub streaming: Option<bool>,
1930 pub system_message: Option<SystemMessageConfig>,
1932 pub tools: Option<Vec<Tool>>,
1934 pub canvases: Option<Vec<CanvasDeclaration>>,
1936 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1941 pub request_canvas_renderer: Option<bool>,
1943 pub request_extensions: Option<bool>,
1945 pub extension_sdk_path: Option<String>,
1949 pub extension_info: Option<ExtensionInfo>,
1951 pub canvas_provider: Option<CanvasProviderIdentity>,
1954 pub available_tools: Option<Vec<String>>,
1956 pub excluded_tools: Option<Vec<String>>,
1958 pub excluded_builtin_agents: Option<Vec<String>>,
1964 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
1966 pub mcp_oauth_token_storage: Option<String>,
1975 pub enable_config_discovery: Option<bool>,
1978 pub skip_embedding_retrieval: Option<bool>,
1980 pub embedding_cache_storage: Option<String>,
1983 pub organization_custom_instructions: Option<String>,
1985 pub enable_on_demand_instruction_discovery: Option<bool>,
1987 pub enable_file_hooks: Option<bool>,
1989 pub enable_host_git_operations: Option<bool>,
1991 pub enable_session_store: Option<bool>,
1993 pub enable_skills: Option<bool>,
1995 pub enable_mcp_apps: Option<bool>,
2022 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
2027 pub skill_directories: Option<Vec<PathBuf>>,
2029 pub instruction_directories: Option<Vec<PathBuf>>,
2032 pub plugin_directories: Option<Vec<PathBuf>>,
2034 pub large_output: Option<LargeToolOutputConfig>,
2036 pub tool_search: Option<ToolSearchConfig>,
2040 pub disabled_skills: Option<Vec<String>>,
2043 pub disabled_mcp_servers: Option<Vec<String>>,
2047 pub hooks: Option<bool>,
2051 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2053 pub default_agent: Option<DefaultAgentConfig>,
2057 pub agent: Option<String>,
2060 pub infinite_sessions: Option<InfiniteSessionConfig>,
2063 pub provider: Option<ProviderConfig>,
2067 pub capi: Option<CapiSessionOptions>,
2073 pub providers: Option<Vec<NamedProviderConfig>>,
2080 pub models: Option<Vec<ProviderModelConfig>>,
2086 pub enable_session_telemetry: Option<bool>,
2094 pub enable_citations: Option<bool>,
2096 pub session_limits: Option<SessionLimitsConfig>,
2098 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2101 pub memory: Option<MemoryConfiguration>,
2103 pub config_directory: Option<PathBuf>,
2106 pub working_directory: Option<PathBuf>,
2109 pub additional_directories: Option<Vec<PathBuf>>,
2113 pub github_token: Option<String>,
2119 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2125 pub cloud: Option<CloudSessionOptions>,
2128 pub include_sub_agent_streaming_events: Option<bool>,
2132 pub commands: Option<Vec<CommandDefinition>>,
2136 #[doc(hidden)]
2143 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
2144 pub enable_managed_settings: Option<bool>,
2151 pub managed_settings: Option<ManagedSettings>,
2160 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2165 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2169 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2172 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2175 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2179 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2182 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2185 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2189 pub(crate) permission_policy: Option<crate::permission::Policy>,
2193 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2198 pub skip_custom_instructions: Option<bool>,
2202 pub custom_agents_local_only: Option<bool>,
2206 pub enable_experimental_mode: Option<bool>,
2211 pub coauthor_enabled: Option<bool>,
2215 pub manage_schedule_enabled: Option<bool>,
2219}
2220
2221impl std::fmt::Debug for SessionConfig {
2222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2223 f.debug_struct("SessionConfig")
2224 .field("session_id", &self.session_id)
2225 .field("model", &self.model)
2226 .field("client_name", &self.client_name)
2227 .field("reasoning_effort", &self.reasoning_effort)
2228 .field("reasoning_summary", &self.reasoning_summary)
2229 .field("context_tier", &self.context_tier)
2230 .field("streaming", &self.streaming)
2231 .field("system_message", &self.system_message)
2232 .field("tools", &self.tools)
2233 .field("canvases", &self.canvases)
2234 .field(
2235 "canvas_handler",
2236 &self.canvas_handler.as_ref().map(|_| "<set>"),
2237 )
2238 .field("request_canvas_renderer", &self.request_canvas_renderer)
2239 .field("request_extensions", &self.request_extensions)
2240 .field("extension_sdk_path", &self.extension_sdk_path)
2241 .field("extension_info", &self.extension_info)
2242 .field("canvas_provider", &self.canvas_provider)
2243 .field("available_tools", &self.available_tools)
2244 .field("excluded_tools", &self.excluded_tools)
2245 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2246 .field("mcp_servers", &self.mcp_servers)
2247 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2248 .field("embedding_cache_storage", &self.embedding_cache_storage)
2249 .field("enable_config_discovery", &self.enable_config_discovery)
2250 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2251 .field(
2252 "organization_custom_instructions",
2253 &self
2254 .organization_custom_instructions
2255 .as_ref()
2256 .map(|_| "<redacted>"),
2257 )
2258 .field(
2259 "enable_on_demand_instruction_discovery",
2260 &self.enable_on_demand_instruction_discovery,
2261 )
2262 .field("enable_file_hooks", &self.enable_file_hooks)
2263 .field(
2264 "enable_host_git_operations",
2265 &self.enable_host_git_operations,
2266 )
2267 .field("enable_session_store", &self.enable_session_store)
2268 .field("enable_skills", &self.enable_skills)
2269 .field("enable_mcp_apps", &self.enable_mcp_apps)
2270 .field("skill_directories", &self.skill_directories)
2271 .field("instruction_directories", &self.instruction_directories)
2272 .field("plugin_directories", &self.plugin_directories)
2273 .field("large_output", &self.large_output)
2274 .field("tool_search", &self.tool_search)
2275 .field("disabled_skills", &self.disabled_skills)
2276 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
2277 .field("hooks", &self.hooks)
2278 .field("custom_agents", &self.custom_agents)
2279 .field("default_agent", &self.default_agent)
2280 .field("agent", &self.agent)
2281 .field("infinite_sessions", &self.infinite_sessions)
2282 .field("provider", &self.provider)
2283 .field("capi", &self.capi)
2284 .field("enable_session_telemetry", &self.enable_session_telemetry)
2285 .field("enable_citations", &self.enable_citations)
2286 .field("session_limits", &self.session_limits)
2287 .field("model_capabilities", &self.model_capabilities)
2288 .field("memory", &self.memory)
2289 .field("config_directory", &self.config_directory)
2290 .field("working_directory", &self.working_directory)
2291 .field("additional_directories", &self.additional_directories)
2292 .field(
2293 "github_token",
2294 &self.github_token.as_ref().map(|_| "<redacted>"),
2295 )
2296 .field("remote_session", &self.remote_session)
2297 .field("cloud", &self.cloud)
2298 .field(
2299 "include_sub_agent_streaming_events",
2300 &self.include_sub_agent_streaming_events,
2301 )
2302 .field("commands", &self.commands)
2303 .field("exp_assignments", &self.exp_assignments)
2304 .field("enable_managed_settings", &self.enable_managed_settings)
2305 .field("enable_experimental_mode", &self.enable_experimental_mode)
2306 .field("managed_settings", &self.managed_settings)
2307 .field(
2308 "session_fs_provider",
2309 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2310 )
2311 .field(
2312 "permission_handler",
2313 &self.permission_handler.as_ref().map(|_| "<set>"),
2314 )
2315 .field(
2316 "elicitation_handler",
2317 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2318 )
2319 .field(
2320 "mcp_auth_handler",
2321 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2322 )
2323 .field(
2324 "user_input_handler",
2325 &self.user_input_handler.as_ref().map(|_| "<set>"),
2326 )
2327 .field(
2328 "exit_plan_mode_handler",
2329 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2330 )
2331 .field(
2332 "auto_mode_switch_handler",
2333 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2334 )
2335 .field(
2336 "hooks_handler",
2337 &self.hooks_handler.as_ref().map(|_| "<set>"),
2338 )
2339 .field(
2340 "system_message_transform",
2341 &self.system_message_transform.as_ref().map(|_| "<set>"),
2342 )
2343 .finish()
2344 }
2345}
2346
2347impl Default for SessionConfig {
2348 fn default() -> Self {
2354 Self {
2355 session_id: None,
2356 model: None,
2357 client_name: None,
2358 reasoning_effort: None,
2359 reasoning_summary: None,
2360 context_tier: None,
2361 streaming: None,
2362 system_message: None,
2363 tools: None,
2364 canvases: None,
2365 canvas_handler: None,
2366 request_canvas_renderer: None,
2367 request_extensions: None,
2368 extension_sdk_path: None,
2369 extension_info: None,
2370 canvas_provider: None,
2371 available_tools: None,
2372 excluded_tools: None,
2373 excluded_builtin_agents: None,
2374 mcp_servers: None,
2375 mcp_oauth_token_storage: None,
2376 enable_config_discovery: None,
2377 skip_embedding_retrieval: None,
2378 organization_custom_instructions: None,
2379 enable_on_demand_instruction_discovery: None,
2380 enable_file_hooks: None,
2381 enable_host_git_operations: None,
2382 enable_session_store: None,
2383 enable_skills: None,
2384 embedding_cache_storage: None,
2385 enable_mcp_apps: None,
2386 github_mcp_tool_config: None,
2387 skill_directories: None,
2388 instruction_directories: None,
2389 plugin_directories: None,
2390 large_output: None,
2391 tool_search: None,
2392 disabled_skills: None,
2393 disabled_mcp_servers: None,
2394 hooks: None,
2395 custom_agents: None,
2396 default_agent: None,
2397 agent: None,
2398 infinite_sessions: None,
2399 provider: None,
2400 capi: None,
2401 providers: None,
2402 models: None,
2403 enable_session_telemetry: None,
2404 enable_citations: None,
2405 session_limits: None,
2406 model_capabilities: None,
2407 memory: None,
2408 config_directory: None,
2409 working_directory: None,
2410 additional_directories: None,
2411 github_token: None,
2412 remote_session: None,
2413 cloud: None,
2414 include_sub_agent_streaming_events: None,
2415 commands: None,
2416 exp_assignments: None,
2417 enable_managed_settings: None,
2418 managed_settings: None,
2419 session_fs_provider: None,
2420 permission_handler: None,
2421 elicitation_handler: None,
2422 mcp_auth_handler: None,
2423 user_input_handler: None,
2424 exit_plan_mode_handler: None,
2425 auto_mode_switch_handler: None,
2426 hooks_handler: None,
2427 permission_policy: None,
2428 system_message_transform: None,
2429 skip_custom_instructions: None,
2430 custom_agents_local_only: None,
2431 enable_experimental_mode: None,
2432 coauthor_enabled: None,
2433 manage_schedule_enabled: None,
2434 }
2435 }
2436}
2437
2438pub(crate) struct SessionConfigRuntime {
2444 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2445 pub permission_policy: Option<crate::permission::Policy>,
2446 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2447 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2448 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2449 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2450 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2451 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2452 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2453 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2454 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2455 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2456 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2457 pub commands: Option<Vec<CommandDefinition>>,
2458}
2459
2460impl SessionConfig {
2461 pub(crate) fn into_wire(
2473 mut self,
2474 session_id: Option<SessionId>,
2475 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2476 let permission_active =
2477 self.permission_handler.is_some() || self.permission_policy.is_some();
2478 let request_user_input = self.user_input_handler.is_some();
2479 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2480 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2481 let request_elicitation = self.elicitation_handler.is_some();
2482 let hooks_flag = self.hooks_handler.is_some();
2483
2484 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2485 if let Some(tools) = self.tools.as_mut() {
2486 for tool in tools.iter_mut() {
2487 if let Some(handler) = tool.handler.take()
2488 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2489 {
2490 return Err(crate::Error::with_message(
2491 crate::ErrorKind::InvalidConfig,
2492 format!("duplicate tool handler registered for name {:?}", tool.name),
2493 ));
2494 }
2495 }
2496 }
2497
2498 let wire_commands = self.commands.as_ref().map(|cmds| {
2499 cmds.iter()
2500 .map(|c| crate::wire::CommandWireDefinition {
2501 name: c.name.clone(),
2502 description: c.description.clone(),
2503 })
2504 .collect()
2505 });
2506 let wire_canvases = self.canvases.clone();
2507 let canvas_handler = self.canvas_handler.clone();
2508 let bearer_token_providers =
2509 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2510
2511 let wire = crate::wire::SessionCreateWire {
2512 session_id,
2513 model: self.model,
2514 client_name: self.client_name,
2515 reasoning_effort: self.reasoning_effort,
2516 reasoning_summary: self.reasoning_summary,
2517 context_tier: self.context_tier,
2518 streaming: self.streaming,
2519 system_message: self.system_message,
2520 tools: self.tools,
2521 canvases: wire_canvases,
2522 request_canvas_renderer: self.request_canvas_renderer,
2523 request_extensions: self.request_extensions,
2524 extension_sdk_path: self.extension_sdk_path,
2525 extension_info: self.extension_info,
2526 canvas_provider: self.canvas_provider,
2527 available_tools: self.available_tools,
2528 excluded_tools: self.excluded_tools,
2529 excluded_builtin_agents: self.excluded_builtin_agents,
2530 tool_filter_precedence: "excluded",
2531 mcp_servers: self.mcp_servers,
2532 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2533 embedding_cache_storage: self.embedding_cache_storage,
2534 env_value_mode: "direct",
2535 enable_config_discovery: self.enable_config_discovery,
2536 skip_embedding_retrieval: self.skip_embedding_retrieval,
2537 organization_custom_instructions: self.organization_custom_instructions,
2538 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2539 enable_file_hooks: self.enable_file_hooks,
2540 enable_host_git_operations: self.enable_host_git_operations,
2541 enable_session_store: self.enable_session_store,
2542 enable_skills: self.enable_skills,
2543 request_user_input,
2544 request_permission: permission_active,
2545 request_exit_plan_mode,
2546 request_auto_mode_switch,
2547 request_elicitation,
2548 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2549 github_mcp_tool_config: self.github_mcp_tool_config,
2550 hooks: hooks_flag,
2551 skill_directories: self.skill_directories,
2552 instruction_directories: self.instruction_directories,
2553 plugin_directories: self.plugin_directories,
2554 large_output: self.large_output,
2555 tool_search: self.tool_search,
2556 disabled_skills: self.disabled_skills,
2557 disabled_mcp_servers: self.disabled_mcp_servers,
2558 custom_agents: self.custom_agents,
2559 custom_agents_local_only: self.custom_agents_local_only,
2560 default_agent: self.default_agent,
2561 agent: self.agent,
2562 infinite_sessions: self.infinite_sessions,
2563 provider: self.provider,
2564 capi: self.capi,
2565 providers: self.providers,
2566 models: self.models,
2567 enable_session_telemetry: self.enable_session_telemetry,
2568 enable_citations: self.enable_citations,
2569 session_limits: self.session_limits,
2570 model_capabilities: self.model_capabilities,
2571 memory: self.memory,
2572 config_dir: self.config_directory,
2573 working_directory: self.working_directory,
2574 additional_directories: self.additional_directories,
2575 github_token: self.github_token,
2576 remote_session: self.remote_session,
2577 cloud: self.cloud,
2578 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2579 enable_github_telemetry_forwarding: None,
2580 commands: wire_commands,
2581 exp_assignments: self.exp_assignments,
2582 enable_managed_settings: self.enable_managed_settings,
2583 is_experimental_mode: self.enable_experimental_mode,
2584 managed_settings: self.managed_settings,
2585 };
2586
2587 let runtime = SessionConfigRuntime {
2588 permission_handler: self.permission_handler,
2589 permission_policy: self.permission_policy,
2590 elicitation_handler: self.elicitation_handler,
2591 mcp_auth_handler: self.mcp_auth_handler,
2592 user_input_handler: self.user_input_handler,
2593 exit_plan_mode_handler: self.exit_plan_mode_handler,
2594 auto_mode_switch_handler: self.auto_mode_switch_handler,
2595 hooks_handler: self.hooks_handler,
2596 system_message_transform: self.system_message_transform,
2597 tool_handlers,
2598 canvas_handler,
2599 session_fs_provider: self.session_fs_provider,
2600 bearer_token_providers,
2601 commands: self.commands,
2602 };
2603
2604 Ok((wire, runtime))
2605 }
2606
2607 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2611 self.permission_handler = Some(handler);
2612 self
2613 }
2614
2615 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2618 self.elicitation_handler = Some(handler);
2619 self
2620 }
2621
2622 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2624 self.mcp_auth_handler = Some(handler);
2625 self
2626 }
2627
2628 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2631 self.user_input_handler = Some(handler);
2632 self
2633 }
2634
2635 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2637 self.exit_plan_mode_handler = Some(handler);
2638 self
2639 }
2640
2641 pub fn with_auto_mode_switch_handler(
2643 mut self,
2644 handler: Arc<dyn AutoModeSwitchHandler>,
2645 ) -> Self {
2646 self.auto_mode_switch_handler = Some(handler);
2647 self
2648 }
2649
2650 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2655 self.commands = Some(commands);
2656 self
2657 }
2658
2659 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2663 self.session_fs_provider = Some(provider);
2664 self
2665 }
2666
2667 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2670 self.hooks_handler = Some(hooks);
2671 self
2672 }
2673
2674 pub fn with_system_message_transform(
2678 mut self,
2679 transform: Arc<dyn SystemMessageTransform>,
2680 ) -> Self {
2681 self.system_message_transform = Some(transform);
2682 self
2683 }
2684
2685 pub fn approve_all_permissions(mut self) -> Self {
2691 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2692 self
2693 }
2694
2695 pub fn deny_all_permissions(mut self) -> Self {
2698 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2699 self
2700 }
2701
2702 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2707 where
2708 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2709 {
2710 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2711 self
2712 }
2713
2714 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2716 self.session_id = Some(id.into());
2717 self
2718 }
2719
2720 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2722 self.model = Some(model.into());
2723 self
2724 }
2725
2726 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2728 self.client_name = Some(name.into());
2729 self
2730 }
2731
2732 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2734 self.reasoning_effort = Some(effort.into());
2735 self
2736 }
2737
2738 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2740 self.reasoning_summary = Some(summary);
2741 self
2742 }
2743
2744 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2746 self.context_tier = Some(tier.into());
2747 self
2748 }
2749
2750 pub fn with_streaming(mut self, streaming: bool) -> Self {
2752 self.streaming = Some(streaming);
2753 self
2754 }
2755
2756 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2758 self.system_message = Some(system_message);
2759 self
2760 }
2761
2762 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2764 self.tools = Some(tools.into_iter().collect());
2765 self
2766 }
2767
2768 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2773 self.canvases = Some(canvases.into_iter().collect());
2774 self
2775 }
2776
2777 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2779 self.canvas_handler = Some(handler);
2780 self
2781 }
2782
2783 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2785 self.request_canvas_renderer = Some(request);
2786 self
2787 }
2788
2789 pub fn with_request_extensions(mut self, request: bool) -> Self {
2791 self.request_extensions = Some(request);
2792 self
2793 }
2794
2795 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2799 self.extension_sdk_path = Some(path.into());
2800 self
2801 }
2802
2803 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2805 self.extension_info = Some(extension_info);
2806 self
2807 }
2808
2809 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2812 self.canvas_provider = Some(canvas_provider);
2813 self
2814 }
2815
2816 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2818 where
2819 I: IntoIterator<Item = S>,
2820 S: Into<String>,
2821 {
2822 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2823 self
2824 }
2825
2826 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2828 where
2829 I: IntoIterator<Item = S>,
2830 S: Into<String>,
2831 {
2832 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2833 self
2834 }
2835
2836 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2838 where
2839 I: IntoIterator<Item = S>,
2840 S: Into<String>,
2841 {
2842 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2843 self
2844 }
2845
2846 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2848 self.mcp_servers = Some(servers);
2849 self
2850 }
2851
2852 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2860 self.mcp_oauth_token_storage = Some(mode.into());
2861 self
2862 }
2863
2864 pub fn with_embedding_cache_storage(
2866 mut self,
2867 embedding_cache_storage: impl Into<String>,
2868 ) -> Self {
2869 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2870 self
2871 }
2872
2873 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2876 self.enable_config_discovery = Some(enable);
2877 self
2878 }
2879
2880 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2882 self.skip_embedding_retrieval = Some(value);
2883 self
2884 }
2885
2886 pub fn with_organization_custom_instructions(
2888 mut self,
2889 instructions: impl Into<String>,
2890 ) -> Self {
2891 self.organization_custom_instructions = Some(instructions.into());
2892 self
2893 }
2894
2895 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2897 self.enable_on_demand_instruction_discovery = Some(value);
2898 self
2899 }
2900
2901 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2903 self.enable_file_hooks = Some(value);
2904 self
2905 }
2906
2907 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2909 self.enable_host_git_operations = Some(value);
2910 self
2911 }
2912
2913 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2915 self.enable_session_store = Some(value);
2916 self
2917 }
2918
2919 pub fn with_enable_skills(mut self, value: bool) -> Self {
2921 self.enable_skills = Some(value);
2922 self
2923 }
2924
2925 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2931 self.enable_mcp_apps = Some(enable);
2932 self
2933 }
2934
2935 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
2937 self.github_mcp_tool_config = Some(config);
2938 self
2939 }
2940
2941 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2943 where
2944 I: IntoIterator<Item = P>,
2945 P: Into<PathBuf>,
2946 {
2947 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2948 self
2949 }
2950
2951 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2955 where
2956 I: IntoIterator<Item = P>,
2957 P: Into<PathBuf>,
2958 {
2959 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2960 self
2961 }
2962
2963 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2965 where
2966 I: IntoIterator<Item = P>,
2967 P: Into<PathBuf>,
2968 {
2969 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2970 self
2971 }
2972
2973 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2975 self.large_output = Some(config);
2976 self
2977 }
2978
2979 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
2982 self.tool_search = Some(config);
2983 self
2984 }
2985
2986 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2988 where
2989 I: IntoIterator<Item = S>,
2990 S: Into<String>,
2991 {
2992 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2993 self
2994 }
2995
2996 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
2998 where
2999 I: IntoIterator<Item = S>,
3000 S: Into<String>,
3001 {
3002 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
3003 self
3004 }
3005
3006 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3008 mut self,
3009 agents: I,
3010 ) -> Self {
3011 self.custom_agents = Some(agents.into_iter().collect());
3012 self
3013 }
3014
3015 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3017 self.default_agent = Some(agent);
3018 self
3019 }
3020
3021 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3024 self.agent = Some(name.into());
3025 self
3026 }
3027
3028 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3031 self.infinite_sessions = Some(config);
3032 self
3033 }
3034
3035 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3037 self.provider = Some(provider);
3038 self
3039 }
3040
3041 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3043 self.capi = Some(capi);
3044 self
3045 }
3046
3047 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3053 self.providers = Some(providers);
3054 self
3055 }
3056
3057 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3063 self.models = Some(models);
3064 self
3065 }
3066
3067 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3071 self.enable_session_telemetry = Some(enable);
3072 self
3073 }
3074
3075 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3077 self.enable_citations = Some(enable);
3078 self
3079 }
3080
3081 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3083 self.session_limits = Some(limits);
3084 self
3085 }
3086
3087 pub fn with_model_capabilities(
3089 mut self,
3090 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3091 ) -> Self {
3092 self.model_capabilities = Some(capabilities);
3093 self
3094 }
3095
3096 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3098 self.memory = Some(memory);
3099 self
3100 }
3101
3102 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3104 self.config_directory = Some(dir.into());
3105 self
3106 }
3107
3108 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3111 self.working_directory = Some(dir.into());
3112 self
3113 }
3114
3115 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
3117 where
3118 I: IntoIterator<Item = P>,
3119 P: Into<PathBuf>,
3120 {
3121 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
3122 self
3123 }
3124
3125 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3130 self.github_token = Some(token.into());
3131 self
3132 }
3133
3134 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3137 self.include_sub_agent_streaming_events = Some(include);
3138 self
3139 }
3140
3141 pub fn with_remote_session(
3143 mut self,
3144 mode: crate::generated::api_types::RemoteSessionMode,
3145 ) -> Self {
3146 self.remote_session = Some(mode);
3147 self
3148 }
3149
3150 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
3152 self.cloud = Some(cloud);
3153 self
3154 }
3155
3156 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3158 self.skip_custom_instructions = Some(value);
3159 self
3160 }
3161
3162 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3164 self.custom_agents_local_only = Some(value);
3165 self
3166 }
3167
3168 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
3170 self.enable_experimental_mode = Some(enable_experimental_mode);
3171 self
3172 }
3173
3174 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3176 self.coauthor_enabled = Some(value);
3177 self
3178 }
3179
3180 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3182 self.manage_schedule_enabled = Some(value);
3183 self
3184 }
3185
3186 #[doc(hidden)]
3194 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
3195 self.exp_assignments = Some(assignments);
3196 self
3197 }
3198
3199 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3205 self.enable_managed_settings = Some(enabled);
3206 self
3207 }
3208
3209 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
3214 self.managed_settings = Some(managed_settings);
3215 self
3216 }
3217}
3218#[derive(Clone)]
3225#[non_exhaustive]
3226pub struct ResumeSessionConfig {
3227 pub session_id: SessionId,
3229 pub model: Option<String>,
3232 pub client_name: Option<String>,
3234 pub reasoning_effort: Option<String>,
3236 pub reasoning_summary: Option<ReasoningSummary>,
3240 pub context_tier: Option<String>,
3243 pub streaming: Option<bool>,
3245 pub system_message: Option<SystemMessageConfig>,
3248 pub tools: Option<Vec<Tool>>,
3250 pub canvases: Option<Vec<CanvasDeclaration>>,
3252 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
3255 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3257 pub request_canvas_renderer: Option<bool>,
3259 pub request_extensions: Option<bool>,
3261 pub extension_sdk_path: Option<String>,
3265 pub extension_info: Option<ExtensionInfo>,
3267 pub canvas_provider: Option<CanvasProviderIdentity>,
3270 pub available_tools: Option<Vec<String>>,
3272 pub excluded_tools: Option<Vec<String>>,
3274 pub excluded_builtin_agents: Option<Vec<String>>,
3280 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3282 pub mcp_oauth_token_storage: Option<String>,
3285 pub enable_config_discovery: Option<bool>,
3288 pub skip_embedding_retrieval: Option<bool>,
3290 pub embedding_cache_storage: Option<String>,
3292 pub organization_custom_instructions: Option<String>,
3294 pub enable_on_demand_instruction_discovery: Option<bool>,
3296 pub enable_file_hooks: Option<bool>,
3298 pub enable_host_git_operations: Option<bool>,
3300 pub enable_session_store: Option<bool>,
3302 pub enable_skills: Option<bool>,
3304 pub enable_mcp_apps: Option<bool>,
3310 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
3315 pub skill_directories: Option<Vec<PathBuf>>,
3317 pub instruction_directories: Option<Vec<PathBuf>>,
3320 pub plugin_directories: Option<Vec<PathBuf>>,
3322 pub large_output: Option<LargeToolOutputConfig>,
3324 pub tool_search: Option<ToolSearchConfig>,
3327 pub disabled_skills: Option<Vec<String>>,
3329 pub disabled_mcp_servers: Option<Vec<String>>,
3332 pub hooks: Option<bool>,
3334 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3336 pub default_agent: Option<DefaultAgentConfig>,
3338 pub agent: Option<String>,
3340 pub infinite_sessions: Option<InfiniteSessionConfig>,
3342 pub provider: Option<ProviderConfig>,
3344 pub capi: Option<CapiSessionOptions>,
3350 pub providers: Option<Vec<NamedProviderConfig>>,
3356 pub models: Option<Vec<ProviderModelConfig>>,
3362 pub enable_session_telemetry: Option<bool>,
3370 pub enable_citations: Option<bool>,
3372 pub session_limits: Option<SessionLimitsConfig>,
3374 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3376 pub memory: Option<MemoryConfiguration>,
3378 pub config_directory: Option<PathBuf>,
3380 pub working_directory: Option<PathBuf>,
3382 pub additional_directories: Option<Vec<PathBuf>>,
3385 pub github_token: Option<String>,
3388 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3391 pub include_sub_agent_streaming_events: Option<bool>,
3393 pub commands: Option<Vec<CommandDefinition>>,
3397 #[doc(hidden)]
3402 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3403 pub enable_managed_settings: Option<bool>,
3409 pub managed_settings: Option<ManagedSettings>,
3415 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3420 pub suppress_resume_event: Option<bool>,
3423 pub continue_pending_work: Option<bool>,
3431 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3434 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3437 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3439 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3442 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3445 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3448 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3450 pub(crate) permission_policy: Option<crate::permission::Policy>,
3452 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3454 pub skip_custom_instructions: Option<bool>,
3456 pub custom_agents_local_only: Option<bool>,
3458 pub enable_experimental_mode: Option<bool>,
3463 pub coauthor_enabled: Option<bool>,
3465 pub manage_schedule_enabled: Option<bool>,
3467}
3468
3469impl std::fmt::Debug for ResumeSessionConfig {
3470 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3471 f.debug_struct("ResumeSessionConfig")
3472 .field("session_id", &self.session_id)
3473 .field("model", &self.model)
3474 .field("client_name", &self.client_name)
3475 .field("reasoning_effort", &self.reasoning_effort)
3476 .field("reasoning_summary", &self.reasoning_summary)
3477 .field("context_tier", &self.context_tier)
3478 .field("streaming", &self.streaming)
3479 .field("system_message", &self.system_message)
3480 .field("tools", &self.tools)
3481 .field("canvases", &self.canvases)
3482 .field(
3483 "canvas_handler",
3484 &self.canvas_handler.as_ref().map(|_| "<set>"),
3485 )
3486 .field("open_canvases", &self.open_canvases)
3487 .field("request_canvas_renderer", &self.request_canvas_renderer)
3488 .field("request_extensions", &self.request_extensions)
3489 .field("extension_sdk_path", &self.extension_sdk_path)
3490 .field("extension_info", &self.extension_info)
3491 .field("canvas_provider", &self.canvas_provider)
3492 .field("available_tools", &self.available_tools)
3493 .field("excluded_tools", &self.excluded_tools)
3494 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3495 .field("mcp_servers", &self.mcp_servers)
3496 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3497 .field("embedding_cache_storage", &self.embedding_cache_storage)
3498 .field("enable_config_discovery", &self.enable_config_discovery)
3499 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3500 .field(
3501 "organization_custom_instructions",
3502 &self
3503 .organization_custom_instructions
3504 .as_ref()
3505 .map(|_| "<redacted>"),
3506 )
3507 .field(
3508 "enable_on_demand_instruction_discovery",
3509 &self.enable_on_demand_instruction_discovery,
3510 )
3511 .field("enable_file_hooks", &self.enable_file_hooks)
3512 .field(
3513 "enable_host_git_operations",
3514 &self.enable_host_git_operations,
3515 )
3516 .field("enable_session_store", &self.enable_session_store)
3517 .field("enable_skills", &self.enable_skills)
3518 .field("enable_mcp_apps", &self.enable_mcp_apps)
3519 .field("skill_directories", &self.skill_directories)
3520 .field("instruction_directories", &self.instruction_directories)
3521 .field("plugin_directories", &self.plugin_directories)
3522 .field("large_output", &self.large_output)
3523 .field("tool_search", &self.tool_search)
3524 .field("disabled_skills", &self.disabled_skills)
3525 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
3526 .field("hooks", &self.hooks)
3527 .field("custom_agents", &self.custom_agents)
3528 .field("default_agent", &self.default_agent)
3529 .field("agent", &self.agent)
3530 .field("infinite_sessions", &self.infinite_sessions)
3531 .field("provider", &self.provider)
3532 .field("capi", &self.capi)
3533 .field("enable_session_telemetry", &self.enable_session_telemetry)
3534 .field("enable_citations", &self.enable_citations)
3535 .field("session_limits", &self.session_limits)
3536 .field("model_capabilities", &self.model_capabilities)
3537 .field("memory", &self.memory)
3538 .field("config_directory", &self.config_directory)
3539 .field("working_directory", &self.working_directory)
3540 .field("additional_directories", &self.additional_directories)
3541 .field(
3542 "github_token",
3543 &self.github_token.as_ref().map(|_| "<redacted>"),
3544 )
3545 .field("remote_session", &self.remote_session)
3546 .field(
3547 "include_sub_agent_streaming_events",
3548 &self.include_sub_agent_streaming_events,
3549 )
3550 .field("commands", &self.commands)
3551 .field("exp_assignments", &self.exp_assignments)
3552 .field("enable_managed_settings", &self.enable_managed_settings)
3553 .field("enable_experimental_mode", &self.enable_experimental_mode)
3554 .field("managed_settings", &self.managed_settings)
3555 .field(
3556 "session_fs_provider",
3557 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3558 )
3559 .field(
3560 "permission_handler",
3561 &self.permission_handler.as_ref().map(|_| "<set>"),
3562 )
3563 .field(
3564 "elicitation_handler",
3565 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3566 )
3567 .field(
3568 "user_input_handler",
3569 &self.user_input_handler.as_ref().map(|_| "<set>"),
3570 )
3571 .field(
3572 "exit_plan_mode_handler",
3573 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3574 )
3575 .field(
3576 "auto_mode_switch_handler",
3577 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3578 )
3579 .field(
3580 "hooks_handler",
3581 &self.hooks_handler.as_ref().map(|_| "<set>"),
3582 )
3583 .field(
3584 "system_message_transform",
3585 &self.system_message_transform.as_ref().map(|_| "<set>"),
3586 )
3587 .field("suppress_resume_event", &self.suppress_resume_event)
3588 .field("continue_pending_work", &self.continue_pending_work)
3589 .finish()
3590 }
3591}
3592
3593impl ResumeSessionConfig {
3594 pub(crate) fn into_wire(
3602 mut self,
3603 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3604 let permission_active =
3605 self.permission_handler.is_some() || self.permission_policy.is_some();
3606 let request_user_input = self.user_input_handler.is_some();
3607 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3608 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3609 let request_elicitation = self.elicitation_handler.is_some();
3610 let hooks_flag = self.hooks_handler.is_some();
3611
3612 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3613 if let Some(tools) = self.tools.as_mut() {
3614 for tool in tools.iter_mut() {
3615 if let Some(handler) = tool.handler.take()
3616 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3617 {
3618 return Err(crate::Error::with_message(
3619 crate::ErrorKind::InvalidConfig,
3620 format!("duplicate tool handler registered for name {:?}", tool.name),
3621 ));
3622 }
3623 }
3624 }
3625
3626 let wire_commands = self.commands.as_ref().map(|cmds| {
3627 cmds.iter()
3628 .map(|c| crate::wire::CommandWireDefinition {
3629 name: c.name.clone(),
3630 description: c.description.clone(),
3631 })
3632 .collect()
3633 });
3634 let wire_canvases = self.canvases.clone();
3635 let canvas_handler = self.canvas_handler.clone();
3636 let bearer_token_providers =
3637 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3638
3639 let wire = crate::wire::SessionResumeWire {
3640 session_id: self.session_id,
3641 model: self.model,
3642 client_name: self.client_name,
3643 reasoning_effort: self.reasoning_effort,
3644 reasoning_summary: self.reasoning_summary,
3645 context_tier: self.context_tier,
3646 streaming: self.streaming,
3647 system_message: self.system_message,
3648 tools: self.tools,
3649 canvases: wire_canvases,
3650 open_canvases: self.open_canvases,
3651 request_canvas_renderer: self.request_canvas_renderer,
3652 request_extensions: self.request_extensions,
3653 extension_sdk_path: self.extension_sdk_path,
3654 extension_info: self.extension_info,
3655 canvas_provider: self.canvas_provider,
3656 available_tools: self.available_tools,
3657 excluded_tools: self.excluded_tools,
3658 excluded_builtin_agents: self.excluded_builtin_agents,
3659 tool_filter_precedence: "excluded",
3660 mcp_servers: self.mcp_servers,
3661 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3662 embedding_cache_storage: self.embedding_cache_storage,
3663 env_value_mode: "direct",
3664 enable_config_discovery: self.enable_config_discovery,
3665 skip_embedding_retrieval: self.skip_embedding_retrieval,
3666 organization_custom_instructions: self.organization_custom_instructions,
3667 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3668 enable_file_hooks: self.enable_file_hooks,
3669 enable_host_git_operations: self.enable_host_git_operations,
3670 enable_session_store: self.enable_session_store,
3671 enable_skills: self.enable_skills,
3672 request_user_input,
3673 request_permission: permission_active,
3674 request_exit_plan_mode,
3675 request_auto_mode_switch,
3676 request_elicitation,
3677 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3678 github_mcp_tool_config: self.github_mcp_tool_config,
3679 hooks: hooks_flag,
3680 skill_directories: self.skill_directories,
3681 instruction_directories: self.instruction_directories,
3682 plugin_directories: self.plugin_directories,
3683 large_output: self.large_output,
3684 tool_search: self.tool_search,
3685 disabled_skills: self.disabled_skills,
3686 disabled_mcp_servers: self.disabled_mcp_servers,
3687 custom_agents: self.custom_agents,
3688 custom_agents_local_only: self.custom_agents_local_only,
3689 default_agent: self.default_agent,
3690 agent: self.agent,
3691 infinite_sessions: self.infinite_sessions,
3692 provider: self.provider,
3693 capi: self.capi,
3694 providers: self.providers,
3695 models: self.models,
3696 enable_session_telemetry: self.enable_session_telemetry,
3697 enable_citations: self.enable_citations,
3698 session_limits: self.session_limits,
3699 model_capabilities: self.model_capabilities,
3700 memory: self.memory,
3701 config_dir: self.config_directory,
3702 working_directory: self.working_directory,
3703 additional_directories: self.additional_directories,
3704 github_token: self.github_token,
3705 remote_session: self.remote_session,
3706 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3707 enable_github_telemetry_forwarding: None,
3708 commands: wire_commands,
3709 exp_assignments: self.exp_assignments,
3710 enable_managed_settings: self.enable_managed_settings,
3711 is_experimental_mode: self.enable_experimental_mode,
3712 managed_settings: self.managed_settings,
3713 suppress_resume_event: self.suppress_resume_event,
3714 continue_pending_work: self.continue_pending_work,
3715 };
3716
3717 let runtime = SessionConfigRuntime {
3718 permission_handler: self.permission_handler,
3719 permission_policy: self.permission_policy,
3720 elicitation_handler: self.elicitation_handler,
3721 mcp_auth_handler: self.mcp_auth_handler,
3722 user_input_handler: self.user_input_handler,
3723 exit_plan_mode_handler: self.exit_plan_mode_handler,
3724 auto_mode_switch_handler: self.auto_mode_switch_handler,
3725 hooks_handler: self.hooks_handler,
3726 system_message_transform: self.system_message_transform,
3727 tool_handlers,
3728 canvas_handler,
3729 session_fs_provider: self.session_fs_provider,
3730 bearer_token_providers,
3731 commands: self.commands,
3732 };
3733
3734 Ok((wire, runtime))
3735 }
3736
3737 pub fn new(session_id: SessionId) -> Self {
3742 Self {
3743 session_id,
3744 model: None,
3745 client_name: None,
3746 reasoning_effort: None,
3747 reasoning_summary: None,
3748 context_tier: None,
3749 streaming: None,
3750 system_message: None,
3751 tools: None,
3752 canvases: None,
3753 canvas_handler: None,
3754 open_canvases: None,
3755 request_canvas_renderer: None,
3756 request_extensions: None,
3757 extension_sdk_path: None,
3758 extension_info: None,
3759 canvas_provider: None,
3760 available_tools: None,
3761 excluded_tools: None,
3762 excluded_builtin_agents: None,
3763 mcp_servers: None,
3764 mcp_oauth_token_storage: None,
3765 enable_config_discovery: None,
3766 skip_embedding_retrieval: None,
3767 organization_custom_instructions: None,
3768 enable_on_demand_instruction_discovery: None,
3769 enable_file_hooks: None,
3770 enable_host_git_operations: None,
3771 enable_session_store: None,
3772 enable_skills: None,
3773 embedding_cache_storage: None,
3774 enable_mcp_apps: None,
3775 github_mcp_tool_config: None,
3776 skill_directories: None,
3777 instruction_directories: None,
3778 plugin_directories: None,
3779 large_output: None,
3780 tool_search: None,
3781 disabled_skills: None,
3782 disabled_mcp_servers: None,
3783 hooks: None,
3784 custom_agents: None,
3785 default_agent: None,
3786 agent: None,
3787 infinite_sessions: None,
3788 provider: None,
3789 capi: None,
3790 providers: None,
3791 models: None,
3792 enable_session_telemetry: None,
3793 enable_citations: None,
3794 session_limits: None,
3795 model_capabilities: None,
3796 memory: None,
3797 config_directory: None,
3798 working_directory: None,
3799 additional_directories: None,
3800 github_token: None,
3801 remote_session: None,
3802 include_sub_agent_streaming_events: None,
3803 commands: None,
3804 exp_assignments: None,
3805 enable_managed_settings: None,
3806 managed_settings: None,
3807 session_fs_provider: None,
3808 suppress_resume_event: None,
3809 continue_pending_work: None,
3810 permission_handler: None,
3811 elicitation_handler: None,
3812 mcp_auth_handler: None,
3813 user_input_handler: None,
3814 exit_plan_mode_handler: None,
3815 auto_mode_switch_handler: None,
3816 hooks_handler: None,
3817 permission_policy: None,
3818 system_message_transform: None,
3819 skip_custom_instructions: None,
3820 custom_agents_local_only: None,
3821 enable_experimental_mode: None,
3822 coauthor_enabled: None,
3823 manage_schedule_enabled: None,
3824 }
3825 }
3826
3827 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3829 self.permission_handler = Some(handler);
3830 self
3831 }
3832
3833 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3835 self.elicitation_handler = Some(handler);
3836 self
3837 }
3838
3839 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3841 self.mcp_auth_handler = Some(handler);
3842 self
3843 }
3844
3845 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3847 self.user_input_handler = Some(handler);
3848 self
3849 }
3850
3851 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3853 self.exit_plan_mode_handler = Some(handler);
3854 self
3855 }
3856
3857 pub fn with_auto_mode_switch_handler(
3859 mut self,
3860 handler: Arc<dyn AutoModeSwitchHandler>,
3861 ) -> Self {
3862 self.auto_mode_switch_handler = Some(handler);
3863 self
3864 }
3865
3866 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3869 self.hooks_handler = Some(hooks);
3870 self
3871 }
3872
3873 pub fn with_system_message_transform(
3875 mut self,
3876 transform: Arc<dyn SystemMessageTransform>,
3877 ) -> Self {
3878 self.system_message_transform = Some(transform);
3879 self
3880 }
3881
3882 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3886 self.commands = Some(commands);
3887 self
3888 }
3889
3890 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3893 self.session_fs_provider = Some(provider);
3894 self
3895 }
3896
3897 pub fn approve_all_permissions(mut self) -> Self {
3900 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3901 self
3902 }
3903
3904 pub fn deny_all_permissions(mut self) -> Self {
3907 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3908 self
3909 }
3910
3911 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3914 where
3915 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3916 {
3917 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3918 self
3919 }
3920
3921 pub fn with_model(mut self, model: impl Into<String>) -> Self {
3923 self.model = Some(model.into());
3924 self
3925 }
3926
3927 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3929 self.client_name = Some(name.into());
3930 self
3931 }
3932
3933 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3935 self.reasoning_effort = Some(effort.into());
3936 self
3937 }
3938
3939 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3941 self.reasoning_summary = Some(summary);
3942 self
3943 }
3944
3945 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3948 self.context_tier = Some(tier.into());
3949 self
3950 }
3951
3952 pub fn with_streaming(mut self, streaming: bool) -> Self {
3954 self.streaming = Some(streaming);
3955 self
3956 }
3957
3958 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3961 self.system_message = Some(system_message);
3962 self
3963 }
3964
3965 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3967 self.tools = Some(tools.into_iter().collect());
3968 self
3969 }
3970
3971 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3973 self.canvases = Some(canvases.into_iter().collect());
3974 self
3975 }
3976
3977 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
3979 self.canvas_handler = Some(handler);
3980 self
3981 }
3982
3983 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
3985 mut self,
3986 open_canvases: I,
3987 ) -> Self {
3988 self.open_canvases = Some(open_canvases.into_iter().collect());
3989 self
3990 }
3991
3992 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
3994 self.request_canvas_renderer = Some(request);
3995 self
3996 }
3997
3998 pub fn with_request_extensions(mut self, request: bool) -> Self {
4000 self.request_extensions = Some(request);
4001 self
4002 }
4003
4004 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
4008 self.extension_sdk_path = Some(path.into());
4009 self
4010 }
4011
4012 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
4014 self.extension_info = Some(extension_info);
4015 self
4016 }
4017
4018 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
4021 self.canvas_provider = Some(canvas_provider);
4022 self
4023 }
4024
4025 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
4027 where
4028 I: IntoIterator<Item = S>,
4029 S: Into<String>,
4030 {
4031 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
4032 self
4033 }
4034
4035 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
4037 where
4038 I: IntoIterator<Item = S>,
4039 S: Into<String>,
4040 {
4041 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
4042 self
4043 }
4044
4045 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
4047 where
4048 I: IntoIterator<Item = S>,
4049 S: Into<String>,
4050 {
4051 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
4052 self
4053 }
4054
4055 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
4057 self.mcp_servers = Some(servers);
4058 self
4059 }
4060
4061 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
4064 self.mcp_oauth_token_storage = Some(mode.into());
4065 self
4066 }
4067
4068 pub fn with_embedding_cache_storage(
4070 mut self,
4071 embedding_cache_storage: impl Into<String>,
4072 ) -> Self {
4073 self.embedding_cache_storage = Some(embedding_cache_storage.into());
4074 self
4075 }
4076
4077 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
4080 self.enable_config_discovery = Some(enable);
4081 self
4082 }
4083
4084 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
4086 self.skip_embedding_retrieval = Some(value);
4087 self
4088 }
4089
4090 pub fn with_organization_custom_instructions(
4092 mut self,
4093 instructions: impl Into<String>,
4094 ) -> Self {
4095 self.organization_custom_instructions = Some(instructions.into());
4096 self
4097 }
4098
4099 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
4101 self.enable_on_demand_instruction_discovery = Some(value);
4102 self
4103 }
4104
4105 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
4107 self.enable_file_hooks = Some(value);
4108 self
4109 }
4110
4111 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
4113 self.enable_host_git_operations = Some(value);
4114 self
4115 }
4116
4117 pub fn with_enable_session_store(mut self, value: bool) -> Self {
4119 self.enable_session_store = Some(value);
4120 self
4121 }
4122
4123 pub fn with_enable_skills(mut self, value: bool) -> Self {
4125 self.enable_skills = Some(value);
4126 self
4127 }
4128
4129 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
4135 self.enable_mcp_apps = Some(enable);
4136 self
4137 }
4138
4139 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
4141 self.github_mcp_tool_config = Some(config);
4142 self
4143 }
4144
4145 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
4147 where
4148 I: IntoIterator<Item = P>,
4149 P: Into<PathBuf>,
4150 {
4151 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
4152 self
4153 }
4154
4155 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
4159 where
4160 I: IntoIterator<Item = P>,
4161 P: Into<PathBuf>,
4162 {
4163 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
4164 self
4165 }
4166
4167 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
4169 where
4170 I: IntoIterator<Item = P>,
4171 P: Into<PathBuf>,
4172 {
4173 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
4174 self
4175 }
4176
4177 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
4179 self.large_output = Some(config);
4180 self
4181 }
4182
4183 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
4186 self.tool_search = Some(config);
4187 self
4188 }
4189
4190 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
4192 where
4193 I: IntoIterator<Item = S>,
4194 S: Into<String>,
4195 {
4196 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
4197 self
4198 }
4199
4200 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
4202 where
4203 I: IntoIterator<Item = S>,
4204 S: Into<String>,
4205 {
4206 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
4207 self
4208 }
4209
4210 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
4212 mut self,
4213 agents: I,
4214 ) -> Self {
4215 self.custom_agents = Some(agents.into_iter().collect());
4216 self
4217 }
4218
4219 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
4221 self.default_agent = Some(agent);
4222 self
4223 }
4224
4225 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
4227 self.agent = Some(name.into());
4228 self
4229 }
4230
4231 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
4233 self.infinite_sessions = Some(config);
4234 self
4235 }
4236
4237 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
4239 self.provider = Some(provider);
4240 self
4241 }
4242
4243 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
4245 self.capi = Some(capi);
4246 self
4247 }
4248
4249 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
4255 self.providers = Some(providers);
4256 self
4257 }
4258
4259 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
4265 self.models = Some(models);
4266 self
4267 }
4268
4269 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
4273 self.enable_session_telemetry = Some(enable);
4274 self
4275 }
4276
4277 pub fn with_enable_citations(mut self, enable: bool) -> Self {
4279 self.enable_citations = Some(enable);
4280 self
4281 }
4282
4283 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
4285 self.session_limits = Some(limits);
4286 self
4287 }
4288
4289 pub fn with_model_capabilities(
4291 mut self,
4292 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
4293 ) -> Self {
4294 self.model_capabilities = Some(capabilities);
4295 self
4296 }
4297
4298 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
4300 self.memory = Some(memory);
4301 self
4302 }
4303
4304 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4306 self.config_directory = Some(dir.into());
4307 self
4308 }
4309
4310 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4312 self.working_directory = Some(dir.into());
4313 self
4314 }
4315
4316 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
4318 where
4319 I: IntoIterator<Item = P>,
4320 P: Into<PathBuf>,
4321 {
4322 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
4323 self
4324 }
4325
4326 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
4330 self.github_token = Some(token.into());
4331 self
4332 }
4333
4334 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4336 self.include_sub_agent_streaming_events = Some(include);
4337 self
4338 }
4339
4340 pub fn with_remote_session(
4342 mut self,
4343 mode: crate::generated::api_types::RemoteSessionMode,
4344 ) -> Self {
4345 self.remote_session = Some(mode);
4346 self
4347 }
4348
4349 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4352 self.suppress_resume_event = Some(suppress);
4353 self
4354 }
4355
4356 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4362 self.continue_pending_work = Some(continue_pending);
4363 self
4364 }
4365
4366 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4368 self.skip_custom_instructions = Some(value);
4369 self
4370 }
4371
4372 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4374 self.custom_agents_local_only = Some(value);
4375 self
4376 }
4377
4378 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
4380 self.enable_experimental_mode = Some(enable_experimental_mode);
4381 self
4382 }
4383
4384 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4386 self.coauthor_enabled = Some(value);
4387 self
4388 }
4389
4390 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4392 self.manage_schedule_enabled = Some(value);
4393 self
4394 }
4395
4396 #[doc(hidden)]
4400 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4401 self.exp_assignments = Some(assignments);
4402 self
4403 }
4404
4405 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4408 self.enable_managed_settings = Some(enabled);
4409 self
4410 }
4411
4412 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
4416 self.managed_settings = Some(managed_settings);
4417 self
4418 }
4419}
4420
4421#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4427#[serde(rename_all = "camelCase")]
4428#[non_exhaustive]
4429pub struct SystemMessageConfig {
4430 #[serde(skip_serializing_if = "Option::is_none")]
4432 pub mode: Option<String>,
4433 #[serde(skip_serializing_if = "Option::is_none")]
4435 pub content: Option<String>,
4436 #[serde(skip_serializing_if = "Option::is_none")]
4438 pub sections: Option<HashMap<String, SectionOverride>>,
4439}
4440
4441impl SystemMessageConfig {
4442 pub fn new() -> Self {
4445 Self::default()
4446 }
4447
4448 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4451 self.mode = Some(mode.into());
4452 self
4453 }
4454
4455 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4458 self.content = Some(content.into());
4459 self
4460 }
4461
4462 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4464 self.sections = Some(sections);
4465 self
4466 }
4467}
4468
4469#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4475#[serde(rename_all = "camelCase")]
4476pub struct SectionOverride {
4477 #[serde(skip_serializing_if = "Option::is_none")]
4480 pub action: Option<String>,
4481 #[serde(skip_serializing_if = "Option::is_none")]
4483 pub content: Option<String>,
4484}
4485
4486#[derive(Debug, Clone, Serialize, Deserialize)]
4488#[serde(rename_all = "camelCase")]
4489pub struct CreateSessionResult {
4490 pub session_id: SessionId,
4492 #[serde(skip_serializing_if = "Option::is_none")]
4494 pub workspace_path: Option<PathBuf>,
4495 #[serde(default, alias = "remote_url")]
4497 pub remote_url: Option<String>,
4498 #[serde(skip_serializing_if = "Option::is_none")]
4500 pub capabilities: Option<SessionCapabilities>,
4501}
4502
4503#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4505#[serde(rename_all = "camelCase")]
4506pub(crate) struct ResumeSessionResult {
4507 #[serde(default)]
4509 pub session_id: Option<SessionId>,
4510 #[serde(default, skip_serializing_if = "Option::is_none")]
4512 pub workspace_path: Option<PathBuf>,
4513 #[serde(default, alias = "remote_url")]
4515 pub remote_url: Option<String>,
4516 #[serde(default, skip_serializing_if = "Option::is_none")]
4518 pub capabilities: Option<SessionCapabilities>,
4519 #[serde(
4521 default,
4522 alias = "openCanvasInstances",
4523 skip_serializing_if = "Option::is_none"
4524 )]
4525 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4526}
4527
4528#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4530#[serde(rename_all = "lowercase")]
4531pub enum LogLevel {
4532 #[default]
4534 Info,
4535 Warning,
4537 Error,
4539}
4540
4541#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4546#[serde(rename_all = "camelCase")]
4547pub struct LogOptions {
4548 #[serde(skip_serializing_if = "Option::is_none")]
4550 pub level: Option<LogLevel>,
4551 #[serde(skip_serializing_if = "Option::is_none")]
4554 pub ephemeral: Option<bool>,
4555}
4556
4557impl LogOptions {
4558 pub fn with_level(mut self, level: LogLevel) -> Self {
4560 self.level = Some(level);
4561 self
4562 }
4563
4564 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4566 self.ephemeral = Some(ephemeral);
4567 self
4568 }
4569}
4570
4571#[derive(Debug, Clone, Default)]
4575pub struct SetModelOptions {
4576 pub reasoning_effort: Option<String>,
4579 pub reasoning_summary: Option<ReasoningSummary>,
4583 pub context_tier: Option<ContextTier>,
4586 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4590}
4591
4592impl SetModelOptions {
4593 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4595 self.reasoning_effort = Some(effort.into());
4596 self
4597 }
4598
4599 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4601 self.reasoning_summary = Some(summary);
4602 self
4603 }
4604
4605 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4607 self.context_tier = Some(tier);
4608 self
4609 }
4610
4611 pub fn with_model_capabilities(
4613 mut self,
4614 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4615 ) -> Self {
4616 self.model_capabilities = Some(caps);
4617 self
4618 }
4619}
4620
4621#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4628#[serde(rename_all = "camelCase")]
4629pub struct PingResponse {
4630 #[serde(default)]
4632 pub message: String,
4633 #[serde(default)]
4635 pub timestamp: String,
4636 #[serde(skip_serializing_if = "Option::is_none")]
4638 pub protocol_version: Option<u32>,
4639}
4640
4641#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4643#[serde(rename_all = "camelCase")]
4644pub struct AttachmentLineRange {
4645 pub start: u32,
4647 pub end: u32,
4649}
4650
4651#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4653#[serde(rename_all = "camelCase")]
4654pub struct AttachmentSelectionPosition {
4655 pub line: u32,
4657 pub character: u32,
4659}
4660
4661#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4663#[serde(rename_all = "camelCase")]
4664pub struct AttachmentSelectionRange {
4665 pub start: AttachmentSelectionPosition,
4667 pub end: AttachmentSelectionPosition,
4669}
4670
4671#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4673#[serde(rename_all = "snake_case")]
4674#[non_exhaustive]
4675pub enum GitHubReferenceType {
4676 Issue,
4678 Pr,
4680 Discussion,
4682}
4683
4684#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4690#[serde(rename_all = "camelCase")]
4691pub struct GitHubRepoPointer {
4692 #[serde(skip_serializing_if = "Option::is_none")]
4694 pub id: Option<i64>,
4695 pub name: String,
4697 pub owner: String,
4699}
4700
4701#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4703#[serde(rename_all = "camelCase")]
4704pub struct GitHubFileDiffSide {
4705 pub path: String,
4707 pub r#ref: String,
4709 pub repo: GitHubRepoPointer,
4711}
4712
4713#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4715#[serde(rename_all = "camelCase")]
4716pub struct GitHubTreeComparisonSide {
4717 pub repo: GitHubRepoPointer,
4719 pub revision: String,
4721}
4722
4723#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4725#[serde(rename_all = "camelCase")]
4726pub struct GitHubSnippetLineRange {
4727 pub start: i64,
4729 pub end: i64,
4731}
4732
4733#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4735#[serde(
4736 tag = "type",
4737 rename_all = "camelCase",
4738 rename_all_fields = "camelCase"
4739)]
4740#[non_exhaustive]
4741pub enum Attachment {
4742 File {
4744 path: PathBuf,
4746 #[serde(skip_serializing_if = "Option::is_none")]
4748 display_name: Option<String>,
4749 #[serde(skip_serializing_if = "Option::is_none")]
4751 line_range: Option<AttachmentLineRange>,
4752 },
4753 Directory {
4755 path: PathBuf,
4757 #[serde(skip_serializing_if = "Option::is_none")]
4759 display_name: Option<String>,
4760 },
4761 Selection {
4763 file_path: PathBuf,
4765 text: String,
4767 #[serde(skip_serializing_if = "Option::is_none")]
4769 display_name: Option<String>,
4770 selection: AttachmentSelectionRange,
4772 },
4773 Blob {
4775 data: String,
4777 mime_type: String,
4779 #[serde(skip_serializing_if = "Option::is_none")]
4781 display_name: Option<String>,
4782 },
4783 #[serde(rename = "github_reference")]
4785 GitHubReference {
4786 number: u64,
4788 title: String,
4790 reference_type: GitHubReferenceType,
4792 state: String,
4794 url: String,
4796 },
4797 #[serde(rename = "github_commit")]
4799 GitHubCommit {
4800 message: String,
4802 oid: String,
4804 repo: GitHubRepoPointer,
4806 url: String,
4808 },
4809 #[serde(rename = "github_release")]
4811 GitHubRelease {
4812 name: String,
4814 repo: GitHubRepoPointer,
4816 tag_name: String,
4818 url: String,
4820 },
4821 #[serde(rename = "github_actions_job")]
4823 GitHubActionsJob {
4824 #[serde(skip_serializing_if = "Option::is_none")]
4827 conclusion: Option<String>,
4828 job_id: i64,
4830 job_name: String,
4832 repo: GitHubRepoPointer,
4834 url: String,
4836 workflow_name: String,
4838 },
4839 #[serde(rename = "github_repository")]
4841 GitHubRepository {
4842 #[serde(skip_serializing_if = "Option::is_none")]
4844 description: Option<String>,
4845 #[serde(skip_serializing_if = "Option::is_none")]
4848 r#ref: Option<String>,
4849 repo: GitHubRepoPointer,
4851 url: String,
4853 },
4854 #[serde(rename = "github_file_diff")]
4856 GitHubFileDiff {
4857 #[serde(skip_serializing_if = "Option::is_none")]
4859 base: Option<GitHubFileDiffSide>,
4860 #[serde(skip_serializing_if = "Option::is_none")]
4862 head: Option<GitHubFileDiffSide>,
4863 url: String,
4865 },
4866 #[serde(rename = "github_tree_comparison")]
4868 GitHubTreeComparison {
4869 base: GitHubTreeComparisonSide,
4871 head: GitHubTreeComparisonSide,
4873 url: String,
4875 },
4876 #[serde(rename = "github_url")]
4878 GitHubUrl {
4879 url: String,
4881 },
4882 #[serde(rename = "github_file")]
4884 GitHubFile {
4885 path: String,
4887 r#ref: String,
4889 repo: GitHubRepoPointer,
4891 url: String,
4893 },
4894 #[serde(rename = "github_snippet")]
4896 GitHubSnippet {
4897 line_range: GitHubSnippetLineRange,
4899 path: String,
4901 r#ref: String,
4903 repo: GitHubRepoPointer,
4905 url: String,
4907 },
4908}
4909
4910impl Attachment {
4911 pub fn display_name(&self) -> Option<&str> {
4913 match self {
4914 Self::File { display_name, .. }
4915 | Self::Directory { display_name, .. }
4916 | Self::Selection { display_name, .. }
4917 | Self::Blob { display_name, .. } => display_name.as_deref(),
4918 Self::GitHubReference { .. }
4919 | Self::GitHubCommit { .. }
4920 | Self::GitHubRelease { .. }
4921 | Self::GitHubActionsJob { .. }
4922 | Self::GitHubRepository { .. }
4923 | Self::GitHubFileDiff { .. }
4924 | Self::GitHubTreeComparison { .. }
4925 | Self::GitHubUrl { .. }
4926 | Self::GitHubFile { .. }
4927 | Self::GitHubSnippet { .. } => None,
4928 }
4929 }
4930
4931 pub fn label(&self) -> Option<String> {
4933 if let Some(display_name) = self
4934 .display_name()
4935 .map(str::trim)
4936 .filter(|name| !name.is_empty())
4937 {
4938 return Some(display_name.to_string());
4939 }
4940
4941 match self {
4942 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4943 format!("#{}", number)
4944 } else {
4945 title.trim().to_string()
4946 }),
4947 _ => self.derived_display_name(),
4948 }
4949 }
4950
4951 pub fn ensure_display_name(&mut self) {
4953 if self
4954 .display_name()
4955 .map(str::trim)
4956 .is_some_and(|name| !name.is_empty())
4957 {
4958 return;
4959 }
4960
4961 let Some(derived_display_name) = self.derived_display_name() else {
4962 return;
4963 };
4964
4965 match self {
4966 Self::File { display_name, .. }
4967 | Self::Directory { display_name, .. }
4968 | Self::Selection { display_name, .. }
4969 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4970 Self::GitHubReference { .. }
4971 | Self::GitHubCommit { .. }
4972 | Self::GitHubRelease { .. }
4973 | Self::GitHubActionsJob { .. }
4974 | Self::GitHubRepository { .. }
4975 | Self::GitHubFileDiff { .. }
4976 | Self::GitHubTreeComparison { .. }
4977 | Self::GitHubUrl { .. }
4978 | Self::GitHubFile { .. }
4979 | Self::GitHubSnippet { .. } => {}
4980 }
4981 }
4982
4983 fn derived_display_name(&self) -> Option<String> {
4984 match self {
4985 Self::File { path, .. } | Self::Directory { path, .. } => {
4986 Some(attachment_name_from_path(path))
4987 }
4988 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4989 Self::Blob { .. } => Some("attachment".to_string()),
4990 Self::GitHubReference { .. }
4991 | Self::GitHubCommit { .. }
4992 | Self::GitHubRelease { .. }
4993 | Self::GitHubActionsJob { .. }
4994 | Self::GitHubRepository { .. }
4995 | Self::GitHubFileDiff { .. }
4996 | Self::GitHubTreeComparison { .. }
4997 | Self::GitHubUrl { .. }
4998 | Self::GitHubFile { .. }
4999 | Self::GitHubSnippet { .. } => None,
5000 }
5001 }
5002}
5003
5004fn attachment_name_from_path(path: &Path) -> String {
5005 path.file_name()
5006 .map(|name| name.to_string_lossy().into_owned())
5007 .filter(|name| !name.is_empty())
5008 .unwrap_or_else(|| {
5009 let full = path.to_string_lossy();
5010 if full.is_empty() {
5011 "attachment".to_string()
5012 } else {
5013 full.into_owned()
5014 }
5015 })
5016}
5017
5018pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
5020 for attachment in attachments {
5021 attachment.ensure_display_name();
5022 }
5023}
5024
5025#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5030#[serde(rename_all = "lowercase")]
5031#[non_exhaustive]
5032pub enum DeliveryMode {
5033 Enqueue,
5035 Immediate,
5037}
5038
5039#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5044#[serde(rename_all = "lowercase")]
5045#[non_exhaustive]
5046pub enum AgentMode {
5047 Interactive,
5049 Plan,
5051 Autopilot,
5053 Shell,
5055}
5056
5057#[derive(Debug, Clone)]
5086#[non_exhaustive]
5087pub struct MessageOptions {
5088 pub prompt: String,
5090 pub mode: Option<DeliveryMode>,
5096 pub agent_mode: Option<AgentMode>,
5100 pub attachments: Option<Vec<Attachment>>,
5102 pub wait_timeout: Option<Duration>,
5105 pub request_headers: Option<HashMap<String, String>>,
5109 pub traceparent: Option<String>,
5116 pub tracestate: Option<String>,
5120 pub display_prompt: Option<String>,
5122}
5123
5124impl MessageOptions {
5125 pub fn new(prompt: impl Into<String>) -> Self {
5127 Self {
5128 prompt: prompt.into(),
5129 mode: None,
5130 agent_mode: None,
5131 attachments: None,
5132 wait_timeout: None,
5133 request_headers: None,
5134 traceparent: None,
5135 tracestate: None,
5136 display_prompt: None,
5137 }
5138 }
5139
5140 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
5146 self.mode = Some(mode);
5147 self
5148 }
5149
5150 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
5154 self.agent_mode = Some(agent_mode);
5155 self
5156 }
5157
5158 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
5160 self.attachments = Some(attachments);
5161 self
5162 }
5163
5164 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
5166 self.wait_timeout = Some(timeout);
5167 self
5168 }
5169
5170 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
5172 self.request_headers = Some(headers);
5173 self
5174 }
5175
5176 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
5181 self.traceparent = ctx.traceparent;
5182 self.tracestate = ctx.tracestate;
5183 self
5184 }
5185
5186 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5188 self.traceparent = Some(traceparent.into());
5189 self
5190 }
5191
5192 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5194 self.tracestate = Some(tracestate.into());
5195 self
5196 }
5197
5198 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5200 self.display_prompt = Some(display_prompt.into());
5201 self
5202 }
5203}
5204
5205impl From<&str> for MessageOptions {
5206 fn from(prompt: &str) -> Self {
5207 Self::new(prompt)
5208 }
5209}
5210
5211impl From<String> for MessageOptions {
5212 fn from(prompt: String) -> Self {
5213 Self::new(prompt)
5214 }
5215}
5216
5217impl From<&String> for MessageOptions {
5218 fn from(prompt: &String) -> Self {
5219 Self::new(prompt.clone())
5220 }
5221}
5222
5223#[derive(Debug, Clone, Serialize, Deserialize)]
5225#[serde(rename_all = "camelCase")]
5226#[non_exhaustive]
5227pub struct GetStatusResponse {
5228 pub version: String,
5230 pub protocol_version: u32,
5232}
5233
5234#[derive(Debug, Clone, Serialize, Deserialize)]
5236#[serde(rename_all = "camelCase")]
5237#[non_exhaustive]
5238pub struct GetAuthStatusResponse {
5239 pub is_authenticated: bool,
5241 #[serde(skip_serializing_if = "Option::is_none")]
5244 pub auth_type: Option<String>,
5245 #[serde(skip_serializing_if = "Option::is_none")]
5247 pub host: Option<String>,
5248 #[serde(skip_serializing_if = "Option::is_none")]
5250 pub login: Option<String>,
5251 #[serde(skip_serializing_if = "Option::is_none")]
5253 pub status_message: Option<String>,
5254}
5255
5256#[derive(Debug, Clone, Serialize, Deserialize)]
5260#[serde(rename_all = "camelCase")]
5261pub struct SessionEventNotification {
5262 pub session_id: SessionId,
5264 pub event: SessionEvent,
5266}
5267
5268#[derive(Debug, Clone, Serialize, Deserialize)]
5275#[serde(rename_all = "camelCase")]
5276pub struct SessionEvent {
5277 pub id: String,
5279 pub timestamp: String,
5281 pub parent_id: Option<String>,
5283 #[serde(skip_serializing_if = "Option::is_none")]
5285 pub ephemeral: Option<bool>,
5286 #[serde(skip_serializing_if = "Option::is_none")]
5289 pub agent_id: Option<String>,
5290 #[serde(skip_serializing_if = "Option::is_none")]
5292 pub debug_cli_received_at_ms: Option<i64>,
5293 #[serde(skip_serializing_if = "Option::is_none")]
5295 pub debug_ws_forwarded_at_ms: Option<i64>,
5296 #[serde(rename = "type")]
5298 pub event_type: String,
5299 pub data: Value,
5301}
5302
5303impl SessionEvent {
5304 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5309 use serde::de::IntoDeserializer;
5310 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5311 self.event_type.as_str().into_deserializer();
5312 crate::generated::SessionEventType::deserialize(deserializer)
5313 .unwrap_or(crate::generated::SessionEventType::Unknown)
5314 }
5315
5316 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5322 serde_json::from_value(self.data.clone()).ok()
5323 }
5324
5325 pub fn is_transient_error(&self) -> bool {
5329 self.event_type == "session.error"
5330 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5331 }
5332}
5333
5334#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5339#[serde(rename_all = "camelCase")]
5340#[non_exhaustive]
5341pub struct ToolInvocation {
5342 pub session_id: SessionId,
5344 pub tool_call_id: String,
5346 pub tool_name: String,
5348 pub arguments: Value,
5350 #[serde(skip)]
5358 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5359 #[serde(default, skip_serializing_if = "Option::is_none")]
5364 pub traceparent: Option<String>,
5365 #[serde(default, skip_serializing_if = "Option::is_none")]
5368 pub tracestate: Option<String>,
5369}
5370
5371impl ToolInvocation {
5372 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5393 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5394 }
5395
5396 pub fn trace_context(&self) -> TraceContext {
5399 TraceContext {
5400 traceparent: self.traceparent.clone(),
5401 tracestate: self.tracestate.clone(),
5402 }
5403 }
5404}
5405
5406#[derive(Debug, Clone, Serialize, Deserialize)]
5408#[serde(rename_all = "camelCase")]
5409pub struct ToolBinaryResult {
5410 pub data: String,
5412 pub mime_type: String,
5414 pub r#type: String,
5416 #[serde(default, skip_serializing_if = "Option::is_none")]
5418 pub description: Option<String>,
5419}
5420
5421#[derive(Debug, Clone, Serialize, Deserialize)]
5428#[serde(rename_all = "camelCase")]
5429#[non_exhaustive]
5430pub struct ToolResultExpanded {
5431 pub text_result_for_llm: String,
5433 pub result_type: String,
5435 #[serde(default, skip_serializing_if = "Option::is_none")]
5437 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5438 #[serde(skip_serializing_if = "Option::is_none")]
5440 pub session_log: Option<String>,
5441 #[serde(skip_serializing_if = "Option::is_none")]
5443 pub error: Option<String>,
5444 #[serde(default, skip_serializing_if = "Option::is_none")]
5446 pub tool_telemetry: Option<HashMap<String, Value>>,
5447 #[serde(default, skip_serializing_if = "Option::is_none")]
5449 pub tool_references: Option<Vec<String>>,
5450}
5451
5452impl ToolResultExpanded {
5453 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5457 Self {
5458 text_result_for_llm: text_result_for_llm.into(),
5459 result_type: result_type.into(),
5460 binary_results_for_llm: None,
5461 session_log: None,
5462 error: None,
5463 tool_telemetry: None,
5464 tool_references: None,
5465 }
5466 }
5467
5468 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5470 self.binary_results_for_llm = Some(results);
5471 self
5472 }
5473
5474 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5476 self.session_log = Some(session_log.into());
5477 self
5478 }
5479
5480 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5482 self.error = Some(error.into());
5483 self
5484 }
5485
5486 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5488 self.tool_telemetry = Some(telemetry);
5489 self
5490 }
5491
5492 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5494 where
5495 I: IntoIterator<Item = S>,
5496 S: Into<String>,
5497 {
5498 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5499 self
5500 }
5501}
5502
5503#[derive(Debug, Clone, Serialize, Deserialize)]
5505#[serde(untagged)]
5506#[non_exhaustive]
5507pub enum ToolResult {
5508 Text(String),
5510 Expanded(ToolResultExpanded),
5512}
5513
5514#[derive(Debug, Clone, Serialize, Deserialize)]
5516#[serde(rename_all = "camelCase")]
5517pub struct ToolResultResponse {
5518 pub result: ToolResult,
5520}
5521
5522#[derive(Debug, Clone, Serialize, Deserialize)]
5524#[serde(rename_all = "camelCase")]
5525pub struct SessionMetadata {
5526 pub session_id: SessionId,
5528 pub start_time: String,
5530 pub modified_time: String,
5532 #[serde(skip_serializing_if = "Option::is_none")]
5534 pub summary: Option<String>,
5535 pub is_remote: bool,
5537}
5538
5539#[derive(Debug, Clone, Serialize, Deserialize)]
5541#[serde(rename_all = "camelCase")]
5542pub struct ListSessionsResponse {
5543 pub sessions: Vec<SessionMetadata>,
5545}
5546
5547#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5551#[serde(rename_all = "camelCase")]
5552pub struct SessionListFilter {
5553 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5555 pub working_directory: Option<String>,
5556 #[serde(default, skip_serializing_if = "Option::is_none")]
5558 pub git_root: Option<String>,
5559 #[serde(default, skip_serializing_if = "Option::is_none")]
5561 pub repository: Option<String>,
5562 #[serde(default, skip_serializing_if = "Option::is_none")]
5564 pub branch: Option<String>,
5565}
5566
5567#[derive(Debug, Clone, Serialize, Deserialize)]
5569#[serde(rename_all = "camelCase")]
5570pub struct GetSessionMetadataResponse {
5571 #[serde(skip_serializing_if = "Option::is_none")]
5573 pub session: Option<SessionMetadata>,
5574}
5575
5576#[derive(Debug, Clone, Serialize, Deserialize)]
5578#[serde(rename_all = "camelCase")]
5579pub struct GetLastSessionIdResponse {
5580 #[serde(skip_serializing_if = "Option::is_none")]
5582 pub session_id: Option<SessionId>,
5583}
5584
5585#[derive(Debug, Clone, Serialize, Deserialize)]
5587#[serde(rename_all = "camelCase")]
5588pub struct GetForegroundSessionResponse {
5589 #[serde(skip_serializing_if = "Option::is_none")]
5591 pub session_id: Option<SessionId>,
5592}
5593
5594#[derive(Debug, Clone, Serialize, Deserialize)]
5596#[serde(rename_all = "camelCase")]
5597pub struct GetMessagesResponse {
5598 pub events: Vec<SessionEvent>,
5600}
5601
5602#[derive(Debug, Clone, Serialize, Deserialize)]
5604#[serde(rename_all = "camelCase")]
5605pub struct ElicitationResult {
5606 pub action: String,
5608 #[serde(skip_serializing_if = "Option::is_none")]
5610 pub content: Option<Value>,
5611}
5612
5613#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5619#[serde(rename_all = "camelCase")]
5620#[non_exhaustive]
5621pub enum ElicitationMode {
5622 Form,
5624 Url,
5626 #[serde(other)]
5628 Unknown,
5629}
5630
5631#[derive(Debug, Clone, Serialize, Deserialize)]
5638#[serde(rename_all = "camelCase")]
5639pub struct ElicitationRequest {
5640 pub message: String,
5642 #[serde(skip_serializing_if = "Option::is_none")]
5644 pub requested_schema: Option<Value>,
5645 #[serde(skip_serializing_if = "Option::is_none")]
5647 pub mode: Option<ElicitationMode>,
5648 #[serde(skip_serializing_if = "Option::is_none")]
5650 pub elicitation_source: Option<String>,
5651 #[serde(skip_serializing_if = "Option::is_none")]
5653 pub url: Option<String>,
5654}
5655
5656#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5661#[serde(rename_all = "camelCase")]
5662pub struct SessionCapabilities {
5663 #[serde(skip_serializing_if = "Option::is_none")]
5665 pub ui: Option<UiCapabilities>,
5666}
5667
5668#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5670#[serde(rename_all = "camelCase")]
5671pub struct UiCapabilities {
5672 #[serde(skip_serializing_if = "Option::is_none")]
5674 pub elicitation: Option<bool>,
5675 #[serde(skip_serializing_if = "Option::is_none")]
5686 pub mcp_apps: Option<bool>,
5687 #[serde(skip_serializing_if = "Option::is_none")]
5689 pub canvases: Option<bool>,
5690}
5691
5692#[derive(Debug, Clone, Default)]
5694pub struct UiInputOptions<'a> {
5695 pub title: Option<&'a str>,
5697 pub description: Option<&'a str>,
5699 pub min_length: Option<u64>,
5701 pub max_length: Option<u64>,
5703 pub format: Option<InputFormat>,
5705 pub default: Option<&'a str>,
5707}
5708
5709#[derive(Debug, Clone, Copy)]
5711#[non_exhaustive]
5712pub enum InputFormat {
5713 Email,
5715 Uri,
5717 Date,
5719 DateTime,
5721}
5722
5723impl InputFormat {
5724 pub fn as_str(&self) -> &'static str {
5726 match self {
5727 Self::Email => "email",
5728 Self::Uri => "uri",
5729 Self::Date => "date",
5730 Self::DateTime => "date-time",
5731 }
5732 }
5733}
5734
5735pub use crate::generated::api_types::{
5740 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5741 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5742 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5743 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
5744};
5745
5746#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5752#[serde(rename_all = "kebab-case")]
5753#[non_exhaustive]
5754pub enum PermissionRequestKind {
5755 Shell,
5757 Write,
5759 Read,
5761 Url,
5763 Mcp,
5765 CustomTool,
5767 Memory,
5769 Hook,
5771 #[serde(other)]
5774 Unknown,
5775}
5776
5777#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5783#[serde(rename_all = "camelCase")]
5784pub struct PermissionRequestData {
5785 #[serde(default, skip_serializing_if = "Option::is_none")]
5789 pub kind: Option<PermissionRequestKind>,
5790 #[serde(default, skip_serializing_if = "Option::is_none")]
5793 pub tool_call_id: Option<String>,
5794 #[serde(default, skip_serializing_if = "Option::is_none")]
5796 pub managed_approval_required: Option<bool>,
5797 #[serde(default, skip_serializing_if = "is_false")]
5799 pub managed_settings_enabled: bool,
5800 #[serde(flatten)]
5804 pub extra: Value,
5805}
5806
5807#[derive(Debug, Clone, Serialize, Deserialize)]
5809#[serde(rename_all = "camelCase")]
5810pub struct ExitPlanModeData {
5811 #[serde(default)]
5813 pub summary: String,
5814 #[serde(default, skip_serializing_if = "Option::is_none")]
5816 pub plan_content: Option<String>,
5817 #[serde(default)]
5819 pub actions: Vec<String>,
5820 #[serde(default = "default_recommended_action")]
5822 pub recommended_action: String,
5823}
5824
5825fn default_recommended_action() -> String {
5826 "autopilot".to_string()
5827}
5828
5829impl Default for ExitPlanModeData {
5830 fn default() -> Self {
5831 Self {
5832 summary: String::new(),
5833 plan_content: None,
5834 actions: Vec::new(),
5835 recommended_action: default_recommended_action(),
5836 }
5837 }
5838}
5839
5840#[cfg(test)]
5841mod tests {
5842 use std::collections::HashMap;
5843 use std::path::PathBuf;
5844
5845 use serde_json::json;
5846
5847 use super::{
5848 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5849 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5850 CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry,
5851 ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
5852 InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
5853 MemoryConfiguration, NamedProviderConfig, ProviderConfig, ProviderModelConfig,
5854 ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, SessionId,
5855 SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
5856 ToolResultResponse, ensure_attachment_display_names,
5857 };
5858 use crate::generated::session_events::TypedSessionEvent;
5859
5860 #[test]
5861 fn tool_builder_composes() {
5862 let tool = Tool::new("greet")
5863 .with_description("Say hello")
5864 .with_namespaced_name("hello/greet")
5865 .with_instructions("Pass the user's name")
5866 .with_parameters(json!({
5867 "type": "object",
5868 "properties": { "name": { "type": "string" } },
5869 "required": ["name"]
5870 }))
5871 .with_overrides_built_in_tool(true)
5872 .with_skip_permission(true);
5873 assert_eq!(tool.name, "greet");
5874 assert_eq!(tool.description, "Say hello");
5875 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5876 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5877 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5878 assert!(tool.overrides_built_in_tool);
5879 assert!(tool.skip_permission);
5880 }
5881
5882 #[test]
5883 fn tool_defer_serialization() {
5884 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5885 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5886 let value = serde_json::to_value(&tool).unwrap();
5887 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5888
5889 let plain = Tool::new("plain");
5890 let value = serde_json::to_value(&plain).unwrap();
5891 assert!(value.get("defer").is_none());
5892 }
5893
5894 #[test]
5895 fn tool_metadata_serialization() {
5896 use indexmap::IndexMap;
5897
5898 let mut metadata = IndexMap::new();
5899 metadata.insert(
5900 "github.com/copilot:safeForTelemetry".to_string(),
5901 json!({ "name": true, "inputsNames": false }),
5902 );
5903 let tool = Tool::new("lookup").with_metadata(metadata);
5904 let value = serde_json::to_value(&tool).unwrap();
5905 assert_eq!(
5906 value
5907 .get("metadata")
5908 .unwrap()
5909 .get("github.com/copilot:safeForTelemetry")
5910 .unwrap(),
5911 &json!({ "name": true, "inputsNames": false })
5912 );
5913
5914 let plain = Tool::new("plain");
5916 let value = serde_json::to_value(&plain).unwrap();
5917 assert!(value.get("metadata").is_none());
5918 }
5919
5920 #[test]
5921 fn custom_agent_config_builder_with_model() {
5922 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5923 .with_model("claude-haiku-4.5")
5924 .with_display_name("My Agent");
5925 assert_eq!(agent.name, "my-agent");
5926 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5927 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5928 }
5929
5930 #[test]
5931 fn custom_agent_config_serializes_model() {
5932 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5933 let wire = serde_json::to_value(&agent).unwrap();
5934 assert_eq!(wire["model"], "claude-haiku-4.5");
5935 assert_eq!(wire["name"], "model-agent");
5936 }
5937
5938 #[test]
5939 fn custom_agent_config_omits_model_when_none() {
5940 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5941 let wire = serde_json::to_value(&agent).unwrap();
5942 assert!(wire.get("model").is_none());
5943 }
5944
5945 #[test]
5946 fn custom_agent_config_builder_with_reasoning_effort() {
5947 let agent =
5948 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5949 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
5950 }
5951
5952 #[test]
5953 fn custom_agent_config_serializes_reasoning_effort() {
5954 let agent =
5955 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5956 let wire = serde_json::to_value(&agent).unwrap();
5957 assert_eq!(wire["reasoningEffort"], "high");
5958 }
5959
5960 #[test]
5961 fn custom_agent_config_omits_reasoning_effort_when_none() {
5962 let agent = CustomAgentConfig::new("default-agent", "prompt");
5963 let wire = serde_json::to_value(&agent).unwrap();
5964 assert!(wire.get("reasoningEffort").is_none());
5965 }
5966
5967 #[test]
5968 #[should_panic(expected = "tool parameter schema must be a JSON object")]
5969 fn tool_with_parameters_panics_on_non_object_value() {
5970 let _ = Tool::new("noop").with_parameters(json!(null));
5971 }
5972
5973 #[test]
5974 fn tool_result_expanded_serializes_binary_results_for_llm() {
5975 let response = ToolResultResponse {
5976 result: ToolResult::Expanded(ToolResultExpanded {
5977 text_result_for_llm: "rendered chart".to_string(),
5978 result_type: "success".to_string(),
5979 binary_results_for_llm: Some(vec![ToolBinaryResult {
5980 data: "aW1n".to_string(),
5981 mime_type: "image/png".to_string(),
5982 r#type: "image".to_string(),
5983 description: Some("chart preview".to_string()),
5984 }]),
5985 session_log: None,
5986 error: None,
5987 tool_telemetry: None,
5988 tool_references: None,
5989 }),
5990 };
5991
5992 let wire = serde_json::to_value(&response).unwrap();
5993
5994 assert_eq!(
5995 wire,
5996 json!({
5997 "result": {
5998 "textResultForLlm": "rendered chart",
5999 "resultType": "success",
6000 "binaryResultsForLlm": [
6001 {
6002 "data": "aW1n",
6003 "mimeType": "image/png",
6004 "type": "image",
6005 "description": "chart preview"
6006 }
6007 ]
6008 }
6009 })
6010 );
6011 }
6012
6013 #[test]
6014 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
6015 let response = ToolResultResponse {
6016 result: ToolResult::Expanded(ToolResultExpanded {
6017 text_result_for_llm: "ok".to_string(),
6018 result_type: "success".to_string(),
6019 binary_results_for_llm: None,
6020 session_log: None,
6021 error: None,
6022 tool_telemetry: None,
6023 tool_references: None,
6024 }),
6025 };
6026
6027 let wire = serde_json::to_value(&response).unwrap();
6028
6029 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6030 assert!(wire["result"].get("binaryResultsForLlm").is_none());
6031 }
6032
6033 #[test]
6034 fn tool_result_expanded_serializes_tool_references() {
6035 let response = ToolResultResponse {
6036 result: ToolResult::Expanded(
6037 ToolResultExpanded::new("found 2 tools", "success")
6038 .with_tool_references(["get_weather", "check_status"]),
6039 ),
6040 };
6041
6042 let wire = serde_json::to_value(&response).unwrap();
6043
6044 assert_eq!(
6045 wire,
6046 json!({
6047 "result": {
6048 "textResultForLlm": "found 2 tools",
6049 "resultType": "success",
6050 "toolReferences": ["get_weather", "check_status"]
6051 }
6052 })
6053 );
6054 }
6055
6056 #[test]
6057 fn tool_result_expanded_omits_tool_references_when_none() {
6058 let response = ToolResultResponse {
6059 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
6060 };
6061
6062 let wire = serde_json::to_value(&response).unwrap();
6063
6064 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6065 assert!(wire["result"].get("toolReferences").is_none());
6066 }
6067
6068 #[test]
6069 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
6070 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
6073 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
6074
6075 assert_eq!(
6076 expanded.tool_references.as_deref(),
6077 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6078 );
6079 }
6080
6081 #[test]
6082 fn tool_result_expanded_deserializes_tool_references() {
6083 let wire = json!({
6084 "textResultForLlm": "found tools",
6085 "resultType": "success",
6086 "toolReferences": ["alpha", "beta"]
6087 });
6088
6089 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
6090
6091 assert_eq!(
6092 expanded.tool_references.as_deref(),
6093 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6094 );
6095 }
6096
6097 #[test]
6098 fn session_config_default_wire_flags_off_without_handlers() {
6099 let cfg = SessionConfig::default();
6100 assert_eq!(cfg.mcp_oauth_token_storage, None);
6101 let (wire, _runtime) = cfg
6105 .into_wire(Some(SessionId::from("default-flags")))
6106 .expect("default config has no duplicate handlers");
6107 assert!(!wire.request_user_input);
6108 assert!(!wire.request_permission);
6109 assert!(!wire.request_elicitation);
6110 assert!(!wire.request_exit_plan_mode);
6111 assert!(!wire.request_auto_mode_switch);
6112 assert!(!wire.hooks);
6113 assert!(!wire.request_mcp_apps);
6114 }
6115
6116 #[test]
6117 fn resume_session_config_new_wire_flags_off_without_handlers() {
6118 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
6119 assert_eq!(cfg.mcp_oauth_token_storage, None);
6120 let (wire, _runtime) = cfg
6121 .into_wire()
6122 .expect("default resume config has no duplicate handlers");
6123 assert!(!wire.request_user_input);
6124 assert!(!wire.request_permission);
6125 assert!(!wire.request_elicitation);
6126 assert!(!wire.request_exit_plan_mode);
6127 assert!(!wire.request_auto_mode_switch);
6128 assert!(!wire.hooks);
6129 assert!(!wire.request_mcp_apps);
6130 }
6131
6132 #[test]
6133 fn custom_agents_local_only_serializes_on_create_and_resume() {
6134 let (create_wire, _) = SessionConfig::default()
6135 .with_custom_agents_local_only(false)
6136 .into_wire(Some(SessionId::from("create-locality")))
6137 .expect("create config has no duplicate handlers");
6138 let create_json = serde_json::to_value(&create_wire).unwrap();
6139 assert_eq!(create_json["customAgentsLocalOnly"], false);
6140
6141 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
6142 .with_custom_agents_local_only(false)
6143 .into_wire()
6144 .expect("resume config has no duplicate handlers");
6145 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6146 assert_eq!(resume_json["customAgentsLocalOnly"], false);
6147
6148 let (unset_create_wire, _) = SessionConfig::default()
6149 .into_wire(Some(SessionId::from("create-unset")))
6150 .expect("create config has no duplicate handlers");
6151 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6152 assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
6153
6154 let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
6155 .into_wire()
6156 .expect("resume config has no duplicate handlers");
6157 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6158 assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
6159 }
6160
6161 #[test]
6162 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6163 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
6164 assert_eq!(cfg.enable_mcp_apps, Some(true));
6165
6166 let (wire, _runtime) = cfg
6167 .into_wire(Some(SessionId::from("enable-mcp-apps")))
6168 .expect("enable_mcp_apps config has no duplicate handlers");
6169 assert!(wire.request_mcp_apps);
6170
6171 let json = serde_json::to_value(&wire).unwrap();
6172 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6173 }
6174
6175 #[test]
6176 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6177 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
6178 .with_enable_mcp_apps(true);
6179 assert_eq!(cfg.enable_mcp_apps, Some(true));
6180
6181 let (wire, _runtime) = cfg
6182 .into_wire()
6183 .expect("resume enable_mcp_apps config has no duplicate handlers");
6184 assert!(wire.request_mcp_apps);
6185
6186 let json = serde_json::to_value(&wire).unwrap();
6187 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6188 }
6189
6190 #[test]
6191 fn github_mcp_tool_config_serializes_for_create_and_resume() {
6192 let github_config = GitHubMcpToolConfig::new()
6193 .with_enable_all_tools(true)
6194 .with_additional_toolsets(["repos"])
6195 .with_additional_tools(["get_issue"])
6196 .with_enable_insiders_mode(true)
6197 .with_disable_form_deferral(true);
6198
6199 let (create_wire, _) = SessionConfig::default()
6200 .with_github_mcp_tool_config(github_config.clone())
6201 .into_wire(Some(SessionId::from("github-mcp")))
6202 .expect("create config has no duplicate handlers");
6203 assert_eq!(
6204 serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6205 serde_json::json!({
6206 "enableAllTools": true,
6207 "additionalToolsets": ["repos"],
6208 "additionalTools": ["get_issue"],
6209 "enableInsidersMode": true,
6210 "disableFormDeferral": true,
6211 })
6212 );
6213
6214 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6215 .with_github_mcp_tool_config(github_config)
6216 .into_wire()
6217 .expect("resume config has no duplicate handlers");
6218 assert!(resume_wire.github_mcp_tool_config.is_some());
6219
6220 let (unset_wire, _) = SessionConfig::default()
6221 .into_wire(Some(SessionId::from("github-mcp-unset")))
6222 .expect("default config has no duplicate handlers");
6223 assert!(
6224 serde_json::to_value(&unset_wire)
6225 .unwrap()
6226 .get("githubMcpToolConfig")
6227 .is_none()
6228 );
6229 }
6230
6231 #[test]
6232 fn memory_configuration_constructors_and_serde() {
6233 assert!(MemoryConfiguration::enabled().enabled);
6234 assert!(!MemoryConfiguration::disabled().enabled);
6235 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6236
6237 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6238 assert_eq!(json, serde_json::json!({ "enabled": true }));
6239 }
6240
6241 #[test]
6242 fn session_config_with_memory_serializes() {
6243 let (wire, _runtime) = SessionConfig::default()
6244 .with_memory(MemoryConfiguration::enabled())
6245 .into_wire(Some(SessionId::from("memory-on")))
6246 .expect("no duplicate handlers");
6247 let json = serde_json::to_value(&wire).unwrap();
6248 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6249
6250 let (wire_off, _) = SessionConfig::default()
6251 .with_memory(MemoryConfiguration::disabled())
6252 .into_wire(Some(SessionId::from("memory-off")))
6253 .expect("no duplicate handlers");
6254 let json_off = serde_json::to_value(&wire_off).unwrap();
6255 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6256
6257 let (empty_wire, _) = SessionConfig::default()
6259 .into_wire(Some(SessionId::from("memory-unset")))
6260 .expect("no duplicate handlers");
6261 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6262 assert!(empty_json.get("memory").is_none());
6263 }
6264
6265 #[test]
6266 fn resume_session_config_with_memory_serializes() {
6267 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6268 .with_memory(MemoryConfiguration::enabled())
6269 .into_wire()
6270 .expect("no duplicate handlers");
6271 let json = serde_json::to_value(&wire).unwrap();
6272 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6273
6274 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6276 .into_wire()
6277 .expect("no duplicate handlers");
6278 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6279 assert!(empty_json.get("memory").is_none());
6280 }
6281
6282 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6283 CopilotExpAssignmentResponse {
6284 features: vec!["copilot_exp_flag".to_string()],
6285 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6286 configs: vec![ExpConfigEntry {
6287 id: "cfg-1".to_string(),
6288 parameters: HashMap::from([
6289 ("threshold".to_string(), ExpFlagValue::Integer(5)),
6290 ("enabled".to_string(), ExpFlagValue::Bool(true)),
6291 ]),
6292 }],
6293 assignment_context: context.to_string(),
6294 ..Default::default()
6295 }
6296 }
6297
6298 #[test]
6299 fn exp_flag_value_round_trips_all_variants() {
6300 let values = serde_json::json!({
6301 "s": "text",
6302 "i": 7,
6303 "f": 1.5,
6304 "b": true,
6305 "n": null,
6306 });
6307 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6308 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6309 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6310 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6311 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6312 assert_eq!(parsed["n"], ExpFlagValue::Null);
6313 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6314 }
6315
6316 #[test]
6317 fn session_config_with_exp_assignments_serializes() {
6318 let assignments = sample_exp_assignments("ctx-123");
6319 let expected = serde_json::to_value(&assignments).unwrap();
6320 let (wire, _runtime) = SessionConfig::default()
6321 .with_exp_assignments(assignments)
6322 .into_wire(Some(SessionId::from("exp-on")))
6323 .expect("no duplicate handlers");
6324 let json = serde_json::to_value(&wire).unwrap();
6325 assert_eq!(json["expAssignments"], expected);
6326 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6327 assert_eq!(
6328 json["expAssignments"]["Flights"]["copilot_exp_flag"],
6329 "treatment"
6330 );
6331
6332 let (empty_wire, _) = SessionConfig::default()
6334 .into_wire(Some(SessionId::from("exp-unset")))
6335 .expect("no duplicate handlers");
6336 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6337 assert!(empty_json.get("expAssignments").is_none());
6338 }
6339
6340 #[test]
6341 fn resume_session_config_with_exp_assignments_serializes() {
6342 let assignments = sample_exp_assignments("ctx-456");
6343 let expected = serde_json::to_value(&assignments).unwrap();
6344 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6345 .with_exp_assignments(assignments)
6346 .into_wire()
6347 .expect("no duplicate handlers");
6348 let json = serde_json::to_value(&wire).unwrap();
6349 assert_eq!(json["expAssignments"], expected);
6350
6351 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6353 .into_wire()
6354 .expect("no duplicate handlers");
6355 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6356 assert!(empty_json.get("expAssignments").is_none());
6357 }
6358
6359 #[test]
6360 fn session_config_clone_preserves_exp_assignments() {
6361 let assignments = sample_exp_assignments("ctx-clone");
6362 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6363 let cloned = config.clone();
6364
6365 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6366
6367 let (wire, _runtime) = cloned
6368 .into_wire(Some(SessionId::from("exp-clone")))
6369 .expect("no duplicate handlers");
6370 let json = serde_json::to_value(&wire).unwrap();
6371 assert_eq!(
6372 json["expAssignments"],
6373 serde_json::to_value(&assignments).unwrap()
6374 );
6375 }
6376
6377 #[test]
6378 fn resume_session_config_clone_preserves_exp_assignments() {
6379 let assignments = sample_exp_assignments("ctx-clone-resume");
6380 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6381 .with_exp_assignments(assignments.clone());
6382 let cloned = config.clone();
6383
6384 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6385
6386 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6387 let json = serde_json::to_value(&wire).unwrap();
6388 assert_eq!(
6389 json["expAssignments"],
6390 serde_json::to_value(&assignments).unwrap()
6391 );
6392 }
6393
6394 #[test]
6395 #[allow(clippy::field_reassign_with_default)]
6396 fn session_config_into_wire_serializes_bucket_b_fields() {
6397 use std::path::PathBuf;
6398
6399 use super::{CloudSessionOptions, CloudSessionRepository};
6400
6401 let mut cfg = SessionConfig::default();
6402 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6403 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6404 cfg.github_token = Some("ghs_secret".to_string());
6405 cfg.include_sub_agent_streaming_events = Some(false);
6406 cfg.enable_session_telemetry = Some(false);
6407 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
6408 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
6409 cfg.enable_on_demand_instruction_discovery = Some(false);
6410 cfg.cloud = Some(CloudSessionOptions::with_repository(
6411 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
6412 ));
6413
6414 let (wire, _runtime) = cfg
6415 .into_wire(Some(SessionId::from("custom-id")))
6416 .expect("no duplicate handlers");
6417 let wire_json = serde_json::to_value(&wire).unwrap();
6418 assert_eq!(wire_json["sessionId"], "custom-id");
6419 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6420 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6421 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6422 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6423 assert_eq!(wire_json["enableSessionTelemetry"], false);
6424 assert_eq!(wire_json["reasoningSummary"], "concise");
6425 assert_eq!(wire_json["remoteSession"], "export");
6426 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6427 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6428 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6429 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6430
6431 let (empty_wire, _) = SessionConfig::default()
6433 .into_wire(Some(SessionId::from("empty")))
6434 .expect("default has no duplicate handlers");
6435 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6436 assert!(empty_json.get("gitHubToken").is_none());
6437 assert!(empty_json.get("enableSessionTelemetry").is_none());
6438 assert!(empty_json.get("reasoningSummary").is_none());
6439 assert!(empty_json.get("remoteSession").is_none());
6440 assert!(
6441 empty_json
6442 .get("enableOnDemandInstructionDiscovery")
6443 .is_none()
6444 );
6445 assert!(empty_json.get("cloud").is_none());
6446 }
6447
6448 #[test]
6449 fn session_config_into_wire_serializes_named_providers_and_models() {
6450 let cfg = SessionConfig::default()
6451 .with_providers(vec![
6452 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6453 .with_provider_type("openai")
6454 .with_wire_api("responses")
6455 .with_api_key("sk-test"),
6456 ])
6457 .with_models(vec![
6458 ProviderModelConfig::new("gpt-x", "my-openai")
6459 .with_wire_model("gpt-x-2025")
6460 .with_max_output_tokens(2048),
6461 ]);
6462
6463 let (wire, _) = cfg
6464 .into_wire(Some(SessionId::from("sess-providers")))
6465 .expect("no duplicate handlers");
6466 let wire_json = serde_json::to_value(&wire).unwrap();
6467 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6468 assert_eq!(
6469 wire_json["providers"][0]["baseUrl"],
6470 "https://api.example.com/v1"
6471 );
6472 assert_eq!(wire_json["providers"][0]["type"], "openai");
6473 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6474 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6475 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
6476 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
6477 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
6478 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
6479
6480 let (empty_wire, _) = SessionConfig::default()
6481 .into_wire(Some(SessionId::from("empty")))
6482 .expect("default has no duplicate handlers");
6483 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6484 assert!(empty_json.get("providers").is_none());
6485 assert!(empty_json.get("models").is_none());
6486 }
6487
6488 #[test]
6489 fn resume_config_into_wire_serializes_named_providers_and_models() {
6490 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
6491 .with_providers(vec![
6492 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
6493 .with_provider_type("azure")
6494 .with_azure(AzureProviderOptions {
6495 api_version: Some("2024-10-21".to_string()),
6496 }),
6497 ])
6498 .with_models(vec![
6499 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
6500 ]);
6501
6502 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6503 let wire_json = serde_json::to_value(&wire).unwrap();
6504 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
6505 assert_eq!(wire_json["providers"][0]["type"], "azure");
6506 assert_eq!(
6507 wire_json["providers"][0]["azure"]["apiVersion"],
6508 "2024-10-21"
6509 );
6510 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
6511 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
6512 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
6513
6514 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
6515 .into_wire()
6516 .expect("default has no duplicate handlers");
6517 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6518 assert!(empty_json.get("providers").is_none());
6519 assert!(empty_json.get("models").is_none());
6520 }
6521
6522 #[test]
6523 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
6524 use std::path::PathBuf;
6525
6526 let cfg = SessionConfig {
6527 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
6528 disabled_mcp_servers: Some(vec![
6529 "local-files".to_string(),
6530 "remote-github".to_string(),
6531 ]),
6532 large_output: Some(
6533 LargeToolOutputConfig::new()
6534 .with_enabled(true)
6535 .with_max_size_bytes(1024)
6536 .with_output_directory(PathBuf::from("/tmp/large-output")),
6537 ),
6538 ..Default::default()
6539 };
6540
6541 let (wire, _) = cfg
6542 .into_wire(Some(SessionId::from("sess-1")))
6543 .expect("no duplicate handlers");
6544 let wire_json = serde_json::to_value(&wire).unwrap();
6545 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
6546 assert_eq!(
6547 wire_json["disabledMcpServers"],
6548 serde_json::json!(["local-files", "remote-github"])
6549 );
6550 assert_eq!(wire_json["largeOutput"]["enabled"], true);
6551 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
6552 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
6553
6554 let (empty_wire, _) = SessionConfig::default()
6555 .into_wire(Some(SessionId::from("empty")))
6556 .expect("default has no duplicate handlers");
6557 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6558 assert!(empty_json.get("pluginDirectories").is_none());
6559 assert!(empty_json.get("disabledMcpServers").is_none());
6560 assert!(empty_json.get("largeOutput").is_none());
6561 }
6562
6563 #[test]
6564 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
6565 use std::path::PathBuf;
6566
6567 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6568 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6569 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6570 cfg.github_token = Some("ghs_secret".to_string());
6571 cfg.include_sub_agent_streaming_events = Some(true);
6572 cfg.enable_session_telemetry = Some(false);
6573 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
6574 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
6575 cfg.enable_on_demand_instruction_discovery = Some(false);
6576
6577 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6578 let wire_json = serde_json::to_value(&wire).unwrap();
6579 assert_eq!(wire_json["sessionId"], "sess-1");
6580 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6581 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6582 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6583 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
6584 assert_eq!(wire_json["enableSessionTelemetry"], false);
6585 assert_eq!(wire_json["reasoningSummary"], "detailed");
6586 assert_eq!(wire_json["remoteSession"], "on");
6587 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6588
6589 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6591 .into_wire()
6592 .expect("default resume has no duplicate handlers");
6593 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6594 assert!(empty_json.get("reasoningSummary").is_none());
6595 assert!(empty_json.get("remoteSession").is_none());
6596 assert!(
6597 empty_json
6598 .get("enableOnDemandInstructionDiscovery")
6599 .is_none()
6600 );
6601 }
6602
6603 #[test]
6604 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
6605 use std::path::PathBuf;
6606
6607 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6608 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6609 cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]);
6610 cfg.large_output = Some(
6611 LargeToolOutputConfig::new()
6612 .with_enabled(false)
6613 .with_max_size_bytes(2048)
6614 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6615 );
6616
6617 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6618 let wire_json = serde_json::to_value(&wire).unwrap();
6619 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6620 assert_eq!(
6621 wire_json["disabledMcpServers"],
6622 serde_json::json!(["local-files-r"])
6623 );
6624 assert_eq!(wire_json["largeOutput"]["enabled"], false);
6625 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6626 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6627
6628 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6629 .into_wire()
6630 .expect("default resume has no duplicate handlers");
6631 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6632 assert!(empty_json.get("pluginDirectories").is_none());
6633 assert!(empty_json.get("disabledMcpServers").is_none());
6634 assert!(empty_json.get("largeOutput").is_none());
6635 }
6636
6637 #[test]
6638 fn session_config_clones_disabled_mcp_servers() {
6639 let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]);
6640 let mut create_clone = create.clone();
6641 create_clone
6642 .disabled_mcp_servers
6643 .as_mut()
6644 .expect("configured disabled MCP servers")
6645 .push("remote-github".to_string());
6646 assert_eq!(
6647 create.disabled_mcp_servers.as_deref(),
6648 Some(&["local-files".to_string()][..])
6649 );
6650
6651 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6652 .with_disabled_mcp_servers(["local-files"]);
6653 let mut resume_clone = resume.clone();
6654 resume_clone
6655 .disabled_mcp_servers
6656 .as_mut()
6657 .expect("configured disabled MCP servers")
6658 .push("remote-github".to_string());
6659 assert_eq!(
6660 resume.disabled_mcp_servers.as_deref(),
6661 Some(&["local-files".to_string()][..])
6662 );
6663 }
6664
6665 #[test]
6666 fn session_config_builder_composes() {
6667 use indexmap::IndexMap;
6668
6669 let cfg = SessionConfig::default()
6670 .with_session_id(SessionId::from("sess-1"))
6671 .with_model("claude-sonnet-4")
6672 .with_client_name("test-app")
6673 .with_reasoning_effort("medium")
6674 .with_reasoning_summary(ReasoningSummary::Concise)
6675 .with_context_tier("long_context")
6676 .with_streaming(true)
6677 .with_tools([Tool::new("greet")])
6678 .with_available_tools(["bash", "view"])
6679 .with_excluded_tools(["dangerous"])
6680 .with_mcp_servers(IndexMap::new())
6681 .with_mcp_oauth_token_storage("persistent")
6682 .with_enable_config_discovery(true)
6683 .with_enable_on_demand_instruction_discovery(true)
6684 .with_skill_directories([PathBuf::from("/tmp/skills")])
6685 .with_disabled_skills(["broken-skill"])
6686 .with_disabled_mcp_servers(["local-files"])
6687 .with_agent("researcher")
6688 .with_config_directory(PathBuf::from("/tmp/config"))
6689 .with_working_directory(PathBuf::from("/tmp/work"))
6690 .with_additional_directories([PathBuf::from("/tmp/shared")])
6691 .with_github_token("ghp_test")
6692 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6693 .with_enable_session_telemetry(false)
6694 .with_include_sub_agent_streaming_events(false)
6695 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6696
6697 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
6698 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
6699 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6700 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
6701 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
6702 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
6703 assert_eq!(cfg.streaming, Some(true));
6704 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6705 assert_eq!(
6706 cfg.available_tools.as_deref(),
6707 Some(&["bash".to_string(), "view".to_string()][..])
6708 );
6709 assert_eq!(
6710 cfg.excluded_tools.as_deref(),
6711 Some(&["dangerous".to_string()][..])
6712 );
6713 assert!(cfg.mcp_servers.is_some());
6714 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6715 assert_eq!(cfg.enable_config_discovery, Some(true));
6716 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
6717 assert_eq!(
6718 cfg.skill_directories.as_deref(),
6719 Some(&[PathBuf::from("/tmp/skills")][..])
6720 );
6721 assert_eq!(
6722 cfg.disabled_skills.as_deref(),
6723 Some(&["broken-skill".to_string()][..])
6724 );
6725 assert_eq!(
6726 cfg.disabled_mcp_servers.as_deref(),
6727 Some(&["local-files".to_string()][..])
6728 );
6729 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6730 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6731 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6732 assert_eq!(
6733 cfg.additional_directories.as_deref(),
6734 Some(&[PathBuf::from("/tmp/shared")][..])
6735 );
6736 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6737 assert_eq!(
6738 cfg.capi,
6739 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6740 );
6741 assert_eq!(cfg.enable_session_telemetry, Some(false));
6742 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
6743 assert_eq!(
6744 cfg.extension_info,
6745 Some(ExtensionInfo::new("github-app", "counter"))
6746 );
6747 }
6748
6749 #[test]
6750 fn resume_session_config_builder_composes() {
6751 use indexmap::IndexMap;
6752
6753 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
6754 .with_client_name("test-app")
6755 .with_reasoning_summary(ReasoningSummary::None)
6756 .with_context_tier("default")
6757 .with_streaming(true)
6758 .with_tools([Tool::new("greet")])
6759 .with_available_tools(["bash", "view"])
6760 .with_excluded_tools(["dangerous"])
6761 .with_mcp_servers(IndexMap::new())
6762 .with_mcp_oauth_token_storage("persistent")
6763 .with_enable_config_discovery(true)
6764 .with_enable_on_demand_instruction_discovery(false)
6765 .with_skill_directories([PathBuf::from("/tmp/skills")])
6766 .with_disabled_skills(["broken-skill"])
6767 .with_disabled_mcp_servers(["local-files"])
6768 .with_agent("researcher")
6769 .with_config_directory(PathBuf::from("/tmp/config"))
6770 .with_working_directory(PathBuf::from("/tmp/work"))
6771 .with_additional_directories([PathBuf::from("/tmp/shared")])
6772 .with_github_token("ghp_test")
6773 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6774 .with_enable_session_telemetry(false)
6775 .with_include_sub_agent_streaming_events(true)
6776 .with_suppress_resume_event(true)
6777 .with_continue_pending_work(true)
6778 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6779
6780 assert_eq!(cfg.session_id.as_str(), "sess-2");
6781 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6782 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
6783 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
6784 assert_eq!(cfg.streaming, Some(true));
6785 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6786 assert_eq!(
6787 cfg.available_tools.as_deref(),
6788 Some(&["bash".to_string(), "view".to_string()][..])
6789 );
6790 assert_eq!(
6791 cfg.excluded_tools.as_deref(),
6792 Some(&["dangerous".to_string()][..])
6793 );
6794 assert!(cfg.mcp_servers.is_some());
6795 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6796 assert_eq!(cfg.enable_config_discovery, Some(true));
6797 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
6798 assert_eq!(
6799 cfg.skill_directories.as_deref(),
6800 Some(&[PathBuf::from("/tmp/skills")][..])
6801 );
6802 assert_eq!(
6803 cfg.disabled_skills.as_deref(),
6804 Some(&["broken-skill".to_string()][..])
6805 );
6806 assert_eq!(
6807 cfg.disabled_mcp_servers.as_deref(),
6808 Some(&["local-files".to_string()][..])
6809 );
6810 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6811 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6812 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6813 assert_eq!(
6814 cfg.additional_directories.as_deref(),
6815 Some(&[PathBuf::from("/tmp/shared")][..])
6816 );
6817 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6818 assert_eq!(
6819 cfg.capi,
6820 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6821 );
6822 assert_eq!(cfg.enable_session_telemetry, Some(false));
6823 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
6824 assert_eq!(cfg.suppress_resume_event, Some(true));
6825 assert_eq!(cfg.continue_pending_work, Some(true));
6826 assert_eq!(
6827 cfg.extension_info,
6828 Some(ExtensionInfo::new("github-app", "counter"))
6829 );
6830 }
6831
6832 #[test]
6836 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
6837 let cfg =
6838 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
6839 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6840 let json = serde_json::to_value(&wire).unwrap();
6841 assert_eq!(json["continuePendingWork"], true);
6842
6843 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6845 .into_wire()
6846 .expect("no duplicate handlers");
6847 let json = serde_json::to_value(&wire).unwrap();
6848 assert!(json.get("continuePendingWork").is_none());
6849 }
6850
6851 #[test]
6852 fn session_configs_serialize_additional_directories() {
6853 let create = SessionConfig::default().with_additional_directories([
6854 PathBuf::from("/tmp/shared"),
6855 PathBuf::from("/tmp/generated"),
6856 ]);
6857 let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
6858 let create_json = serde_json::to_value(&create_wire).unwrap();
6859 assert_eq!(
6860 create_json["additionalDirectories"],
6861 serde_json::json!(["/tmp/shared", "/tmp/generated"])
6862 );
6863
6864 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6865 .with_additional_directories([PathBuf::from("/tmp/resumed")]);
6866 let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
6867 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6868 assert_eq!(
6869 resume_json["additionalDirectories"],
6870 serde_json::json!(["/tmp/resumed"])
6871 );
6872 }
6873
6874 #[test]
6878 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
6879 let cfg =
6880 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
6881 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6882 let json = serde_json::to_value(&wire).unwrap();
6883 assert_eq!(json["disableResume"], true);
6884 assert!(json.get("suppressResumeEvent").is_none());
6885 }
6886
6887 #[test]
6890 fn session_config_serializes_instruction_directories_to_camel_case() {
6891 let cfg =
6892 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
6893 let (wire, _) = cfg
6894 .into_wire(Some(SessionId::from("instr-on")))
6895 .expect("no duplicate handlers");
6896 let json = serde_json::to_value(&wire).unwrap();
6897 assert_eq!(
6898 json["instructionDirectories"],
6899 serde_json::json!(["/tmp/instr"])
6900 );
6901
6902 let (wire, _) = SessionConfig::default()
6904 .into_wire(Some(SessionId::from("instr-off")))
6905 .expect("no duplicate handlers");
6906 let json = serde_json::to_value(&wire).unwrap();
6907 assert!(json.get("instructionDirectories").is_none());
6908 }
6909
6910 #[test]
6913 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
6914 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
6915 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
6916 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6917 let json = serde_json::to_value(&wire).unwrap();
6918 assert_eq!(
6919 json["instructionDirectories"],
6920 serde_json::json!(["/tmp/instr"])
6921 );
6922
6923 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6924 .into_wire()
6925 .expect("no duplicate handlers");
6926 let json = serde_json::to_value(&wire).unwrap();
6927 assert!(json.get("instructionDirectories").is_none());
6928 }
6929
6930 #[test]
6931 fn custom_agent_config_builder_composes() {
6932 use indexmap::IndexMap;
6933
6934 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
6935 .with_display_name("Research Assistant")
6936 .with_description("Investigates technical questions.")
6937 .with_tools(["bash", "view"])
6938 .with_mcp_servers(IndexMap::new())
6939 .with_infer(true)
6940 .with_skills(["rust-coding-skill"]);
6941
6942 assert_eq!(cfg.name, "researcher");
6943 assert_eq!(cfg.prompt, "You are a research assistant.");
6944 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
6945 assert_eq!(
6946 cfg.description.as_deref(),
6947 Some("Investigates technical questions.")
6948 );
6949 assert_eq!(
6950 cfg.tools.as_deref(),
6951 Some(&["bash".to_string(), "view".to_string()][..])
6952 );
6953 assert!(cfg.mcp_servers.is_some());
6954 assert_eq!(cfg.infer, Some(true));
6955 assert_eq!(
6956 cfg.skills.as_deref(),
6957 Some(&["rust-coding-skill".to_string()][..])
6958 );
6959 }
6960
6961 #[test]
6962 fn mcp_servers_serialize_in_insertion_order() {
6963 use indexmap::IndexMap;
6964
6965 let order = [
6971 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
6972 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
6973 ];
6974 let mut servers = IndexMap::new();
6975 for name in order {
6976 servers.insert(
6977 name.to_string(),
6978 McpServerConfig::Stdio(McpStdioServerConfig {
6979 command: "run".to_string(),
6980 ..Default::default()
6981 }),
6982 );
6983 }
6984
6985 let (wire, _runtime) = SessionConfig::default()
6986 .with_mcp_servers(servers)
6987 .into_wire(None)
6988 .expect("into_wire should succeed");
6989 let json = serde_json::to_string(&wire).expect("serialize wire");
6990
6991 let positions: Vec<usize> = order
6992 .iter()
6993 .map(|name| {
6994 json.find(&format!("\"{name}\""))
6995 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
6996 })
6997 .collect();
6998 let mut ascending = positions.clone();
6999 ascending.sort_unstable();
7000 assert_eq!(
7001 positions, ascending,
7002 "mcp server keys must serialize in insertion order: {json}"
7003 );
7004 }
7005
7006 #[test]
7007 fn infinite_session_config_builder_composes() {
7008 let cfg = InfiniteSessionConfig::new()
7009 .with_enabled(true)
7010 .with_background_compaction_threshold(0.75)
7011 .with_buffer_exhaustion_threshold(0.92);
7012
7013 assert_eq!(cfg.enabled, Some(true));
7014 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
7015 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
7016 }
7017
7018 #[test]
7019 fn provider_config_builder_composes() {
7020 use std::collections::HashMap;
7021
7022 let mut headers = HashMap::new();
7023 headers.insert("X-Custom".to_string(), "value".to_string());
7024
7025 let cfg = ProviderConfig::new("https://api.example.com")
7026 .with_provider_type("openai")
7027 .with_wire_api("completions")
7028 .with_transport("websockets")
7029 .with_api_key("sk-test")
7030 .with_bearer_token("bearer-test")
7031 .with_headers(headers)
7032 .with_model_id("gpt-4")
7033 .with_wire_model("azure-gpt-4-deployment")
7034 .with_max_prompt_tokens(8192)
7035 .with_max_output_tokens(2048);
7036
7037 assert_eq!(cfg.base_url, "https://api.example.com");
7038 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
7039 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
7040 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
7041 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
7042 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
7043 assert_eq!(
7044 cfg.headers
7045 .as_ref()
7046 .and_then(|h| h.get("X-Custom"))
7047 .map(String::as_str),
7048 Some("value"),
7049 );
7050 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
7051 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
7052 assert_eq!(cfg.max_prompt_tokens, Some(8192));
7053 assert_eq!(cfg.max_output_tokens, Some(2048));
7054
7055 let wire = serde_json::to_value(&cfg).unwrap();
7057 assert_eq!(wire["modelId"], "gpt-4");
7058 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
7059 assert_eq!(wire["maxPromptTokens"], 8192);
7060 assert_eq!(wire["maxOutputTokens"], 2048);
7061
7062 let unset = ProviderConfig::new("https://api.example.com");
7063 let wire_unset = serde_json::to_value(&unset).unwrap();
7064 assert!(wire_unset.get("modelId").is_none());
7065 assert!(wire_unset.get("wireModel").is_none());
7066 assert!(wire_unset.get("maxPromptTokens").is_none());
7067 assert!(wire_unset.get("maxOutputTokens").is_none());
7068 }
7069
7070 #[test]
7071 fn capi_session_options_builder_composes_and_serializes() {
7072 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
7073
7074 assert_eq!(cfg.enable_web_socket_responses, Some(false));
7075
7076 let wire = serde_json::to_value(&cfg).unwrap();
7077 assert_eq!(
7078 wire,
7079 serde_json::json!({ "enableWebSocketResponses": false })
7080 );
7081
7082 let unset = CapiSessionOptions::new();
7083 let wire_unset = serde_json::to_value(&unset).unwrap();
7084 assert!(wire_unset.get("enableWebSocketResponses").is_none());
7085 }
7086
7087 #[test]
7088 fn session_config_with_capi_serializes() {
7089 let (wire, _) = SessionConfig::default()
7090 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7091 .into_wire(Some(SessionId::from("capi-create")))
7092 .expect("no duplicate handlers");
7093 let json = serde_json::to_value(&wire).unwrap();
7094 assert_eq!(
7095 json["capi"],
7096 serde_json::json!({ "enableWebSocketResponses": false })
7097 );
7098
7099 let (empty_wire, _) = SessionConfig::default()
7100 .into_wire(Some(SessionId::from("capi-create-unset")))
7101 .expect("no duplicate handlers");
7102 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7103 assert!(empty_json.get("capi").is_none());
7104 }
7105
7106 #[test]
7107 fn resume_session_config_with_capi_serializes() {
7108 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7109 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7110 .into_wire()
7111 .expect("no duplicate handlers");
7112 let json = serde_json::to_value(&wire).unwrap();
7113 assert_eq!(
7114 json["capi"],
7115 serde_json::json!({ "enableWebSocketResponses": false })
7116 );
7117
7118 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
7119 .into_wire()
7120 .expect("no duplicate handlers");
7121 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7122 assert!(empty_json.get("capi").is_none());
7123 }
7124
7125 #[test]
7126 fn system_message_config_builder_composes() {
7127 use std::collections::HashMap;
7128
7129 let cfg = SystemMessageConfig::new()
7130 .with_mode("replace")
7131 .with_content("Custom system message.")
7132 .with_sections(HashMap::new());
7133
7134 assert_eq!(cfg.mode.as_deref(), Some("replace"));
7135 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
7136 assert!(cfg.sections.is_some());
7137 }
7138
7139 #[test]
7140 fn delivery_mode_serializes_to_kebab_case_strings() {
7141 assert_eq!(
7142 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
7143 "\"enqueue\""
7144 );
7145 assert_eq!(
7146 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
7147 "\"immediate\""
7148 );
7149 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
7150 assert_eq!(parsed, DeliveryMode::Immediate);
7151 }
7152
7153 #[test]
7154 fn agent_mode_serializes_to_kebab_case_strings() {
7155 assert_eq!(
7156 serde_json::to_string(&AgentMode::Interactive).unwrap(),
7157 "\"interactive\""
7158 );
7159 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
7160 assert_eq!(
7161 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
7162 "\"autopilot\""
7163 );
7164 assert_eq!(
7165 serde_json::to_string(&AgentMode::Shell).unwrap(),
7166 "\"shell\""
7167 );
7168 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
7169 assert_eq!(parsed, AgentMode::Plan);
7170 }
7171
7172 #[test]
7173 fn connection_state_distinguishes_variants() {
7174 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
7177 }
7178
7179 #[test]
7185 fn session_event_round_trips_agent_id_on_envelope() {
7186 let wire = json!({
7187 "id": "evt-1",
7188 "timestamp": "2026-04-30T12:00:00Z",
7189 "parentId": null,
7190 "agentId": "sub-agent-42",
7191 "type": "assistant.message",
7192 "data": { "message": "hi" }
7193 });
7194
7195 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
7196 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7197
7198 let roundtripped = serde_json::to_value(&event).unwrap();
7200 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7201
7202 let main_agent_event: SessionEvent = serde_json::from_value(json!({
7204 "id": "evt-2",
7205 "timestamp": "2026-04-30T12:00:01Z",
7206 "parentId": null,
7207 "type": "session.idle",
7208 "data": {}
7209 }))
7210 .unwrap();
7211 assert!(main_agent_event.agent_id.is_none());
7212 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
7213 assert!(roundtripped.get("agentId").is_none());
7214 }
7215
7216 #[test]
7218 fn typed_session_event_round_trips_agent_id_on_envelope() {
7219 let wire = json!({
7220 "id": "evt-1",
7221 "timestamp": "2026-04-30T12:00:00Z",
7222 "parentId": null,
7223 "agentId": "sub-agent-42",
7224 "type": "session.idle",
7225 "data": {}
7226 });
7227
7228 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
7229 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7230
7231 let roundtripped = serde_json::to_value(&event).unwrap();
7232 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7233 }
7234
7235 #[test]
7236 fn connection_state_variants_compile() {
7237 let _ = ConnectionState::Disconnected;
7241 let _ = ConnectionState::Connecting;
7242 let _ = ConnectionState::Connected;
7243 let _ = ConnectionState::Error;
7244 }
7245
7246 #[test]
7247 fn deserializes_runtime_attachment_variants() {
7248 let attachments: Vec<Attachment> = serde_json::from_value(json!([
7249 {
7250 "type": "file",
7251 "path": "/tmp/file.rs",
7252 "displayName": "file.rs",
7253 "lineRange": { "start": 7, "end": 12 }
7254 },
7255 {
7256 "type": "directory",
7257 "path": "/tmp/project",
7258 "displayName": "project"
7259 },
7260 {
7261 "type": "selection",
7262 "filePath": "/tmp/lib.rs",
7263 "displayName": "lib.rs",
7264 "text": "fn main() {}",
7265 "selection": {
7266 "start": { "line": 1, "character": 2 },
7267 "end": { "line": 3, "character": 4 }
7268 }
7269 },
7270 {
7271 "type": "blob",
7272 "data": "Zm9v",
7273 "mimeType": "image/png",
7274 "displayName": "image.png"
7275 },
7276 {
7277 "type": "github_reference",
7278 "number": 42,
7279 "title": "Fix rendering",
7280 "referenceType": "issue",
7281 "state": "open",
7282 "url": "https://github.com/example/repo/issues/42"
7283 }
7284 ]))
7285 .expect("attachments should deserialize");
7286
7287 assert_eq!(attachments.len(), 5);
7288 assert!(matches!(
7289 &attachments[0],
7290 Attachment::File {
7291 path,
7292 display_name,
7293 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7294 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7295 ));
7296 assert!(matches!(
7297 &attachments[1],
7298 Attachment::Directory { path, display_name }
7299 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7300 ));
7301 assert!(matches!(
7302 &attachments[2],
7303 Attachment::Selection {
7304 file_path,
7305 display_name,
7306 selection:
7307 AttachmentSelectionRange {
7308 start: AttachmentSelectionPosition { line: 1, character: 2 },
7309 end: AttachmentSelectionPosition { line: 3, character: 4 },
7310 },
7311 ..
7312 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
7313 ));
7314 assert!(matches!(
7315 &attachments[3],
7316 Attachment::Blob {
7317 data,
7318 mime_type,
7319 display_name,
7320 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
7321 ));
7322 assert!(matches!(
7323 &attachments[4],
7324 Attachment::GitHubReference {
7325 number: 42,
7326 title,
7327 reference_type: GitHubReferenceType::Issue,
7328 state,
7329 url,
7330 } if title == "Fix rendering"
7331 && state == "open"
7332 && url == "https://github.com/example/repo/issues/42"
7333 ));
7334 }
7335
7336 #[test]
7337 fn ensures_display_names_for_variants_that_support_them() {
7338 let mut attachments = vec![
7339 Attachment::File {
7340 path: PathBuf::from("/tmp/file.rs"),
7341 display_name: None,
7342 line_range: None,
7343 },
7344 Attachment::Selection {
7345 file_path: PathBuf::from("/tmp/src/lib.rs"),
7346 display_name: None,
7347 text: "fn main() {}".to_string(),
7348 selection: AttachmentSelectionRange {
7349 start: AttachmentSelectionPosition {
7350 line: 0,
7351 character: 0,
7352 },
7353 end: AttachmentSelectionPosition {
7354 line: 0,
7355 character: 10,
7356 },
7357 },
7358 },
7359 Attachment::Blob {
7360 data: "Zm9v".to_string(),
7361 mime_type: "image/png".to_string(),
7362 display_name: None,
7363 },
7364 Attachment::GitHubReference {
7365 number: 7,
7366 title: "Track regressions".to_string(),
7367 reference_type: GitHubReferenceType::Issue,
7368 state: "open".to_string(),
7369 url: "https://example.com/issues/7".to_string(),
7370 },
7371 ];
7372
7373 ensure_attachment_display_names(&mut attachments);
7374
7375 assert_eq!(attachments[0].display_name(), Some("file.rs"));
7376 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
7377 assert_eq!(attachments[2].display_name(), Some("attachment"));
7378 assert_eq!(attachments[3].display_name(), None);
7379 assert_eq!(
7380 attachments[3].label(),
7381 Some("Track regressions".to_string())
7382 );
7383 }
7384
7385 #[test]
7386 fn github_anchored_attachment_variants_round_trip() {
7387 let cases = vec![
7388 (
7389 "github_commit",
7390 json!({
7391 "type": "github_commit",
7392 "message": "Fix the thing",
7393 "oid": "abc123",
7394 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
7395 "url": "https://github.com/octocat/repo/commit/abc123"
7396 }),
7397 ),
7398 (
7399 "github_release",
7400 json!({
7401 "type": "github_release",
7402 "name": "v1.2.3",
7403 "repo": { "name": "repo", "owner": "octocat" },
7404 "tagName": "v1.2.3",
7405 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
7406 }),
7407 ),
7408 (
7409 "github_actions_job",
7410 json!({
7411 "type": "github_actions_job",
7412 "conclusion": "failure",
7413 "jobId": 99,
7414 "jobName": "build",
7415 "repo": { "name": "repo", "owner": "octocat" },
7416 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
7417 "workflowName": "CI"
7418 }),
7419 ),
7420 (
7421 "github_repository",
7422 json!({
7423 "type": "github_repository",
7424 "description": "An example repository",
7425 "ref": "main",
7426 "repo": { "name": "repo", "owner": "octocat" },
7427 "url": "https://github.com/octocat/repo"
7428 }),
7429 ),
7430 (
7431 "github_file_diff",
7432 json!({
7433 "type": "github_file_diff",
7434 "base": {
7435 "path": "src/lib.rs",
7436 "ref": "main",
7437 "repo": { "name": "repo", "owner": "octocat" }
7438 },
7439 "head": {
7440 "path": "src/lib.rs",
7441 "ref": "feature",
7442 "repo": { "name": "repo", "owner": "octocat" }
7443 },
7444 "url": "https://github.com/octocat/repo/compare/main...feature"
7445 }),
7446 ),
7447 (
7448 "github_tree_comparison",
7449 json!({
7450 "type": "github_tree_comparison",
7451 "base": {
7452 "repo": { "name": "repo", "owner": "octocat" },
7453 "revision": "main"
7454 },
7455 "head": {
7456 "repo": { "name": "repo", "owner": "octocat" },
7457 "revision": "feature"
7458 },
7459 "url": "https://github.com/octocat/repo/compare/main...feature"
7460 }),
7461 ),
7462 (
7463 "github_url",
7464 json!({
7465 "type": "github_url",
7466 "url": "https://github.com/octocat/repo/wiki"
7467 }),
7468 ),
7469 (
7470 "github_file",
7471 json!({
7472 "type": "github_file",
7473 "path": "src/main.rs",
7474 "ref": "main",
7475 "repo": { "name": "repo", "owner": "octocat" },
7476 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
7477 }),
7478 ),
7479 (
7480 "github_snippet",
7481 json!({
7482 "type": "github_snippet",
7483 "lineRange": { "start": 10, "end": 20 },
7484 "path": "src/main.rs",
7485 "ref": "main",
7486 "repo": { "name": "repo", "owner": "octocat" },
7487 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
7488 }),
7489 ),
7490 ];
7491
7492 for (expected_type, input) in cases {
7493 let attachment: Attachment = serde_json::from_value(input.clone())
7494 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
7495
7496 let serialized_string = serde_json::to_string(&attachment)
7501 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
7502
7503 assert_eq!(
7505 serialized_string.matches("\"type\":").count(),
7506 1,
7507 "{expected_type} must serialize a single `type` key"
7508 );
7509
7510 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
7511 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
7512 assert_eq!(
7513 serialized.get("type").and_then(|value| value.as_str()),
7514 Some(expected_type),
7515 "{expected_type} must serialize the correct discriminator"
7516 );
7517
7518 assert_eq!(
7520 serialized, input,
7521 "{expected_type} should round-trip without data loss"
7522 );
7523 let reparsed: Attachment = serde_json::from_value(serialized)
7524 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
7525 assert_eq!(
7526 reparsed, attachment,
7527 "{expected_type} should re-deserialize to the same value"
7528 );
7529 }
7530 }
7531}
7532
7533#[cfg(test)]
7534mod permission_builder_tests {
7535 use std::sync::Arc;
7536
7537 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
7538 use crate::permission;
7539 use crate::types::{
7540 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
7541 SessionId,
7542 };
7543
7544 fn data() -> PermissionRequestData {
7545 PermissionRequestData {
7546 extra: serde_json::json!({"tool": "shell"}),
7547 ..Default::default()
7548 }
7549 }
7550
7551 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7554 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7555 }
7556
7557 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7558 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7559 }
7560
7561 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
7562 handler
7563 .handle(SessionId::from("s1"), RequestId::new("1"), data())
7564 .await
7565 }
7566
7567 #[tokio::test]
7568 async fn approve_all_with_handler_present_approves() {
7569 let cfg = SessionConfig::default()
7570 .with_permission_handler(Arc::new(ApproveAllHandler))
7571 .approve_all_permissions();
7572 let h = resolve_create(cfg).expect("policy + handler yields handler");
7573 assert!(matches!(
7574 dispatch(&h).await,
7575 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7576 ));
7577 }
7578
7579 #[tokio::test]
7580 async fn approve_all_standalone_produces_handler() {
7581 let cfg = SessionConfig::default().approve_all_permissions();
7582 let h = resolve_create(cfg).expect("policy alone yields handler");
7583 assert!(matches!(
7584 dispatch(&h).await,
7585 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7586 ));
7587 }
7588
7589 #[tokio::test]
7592 async fn approve_all_is_order_independent() {
7593 let a = SessionConfig::default()
7594 .with_permission_handler(Arc::new(ApproveAllHandler))
7595 .approve_all_permissions();
7596 let b = SessionConfig::default()
7597 .approve_all_permissions()
7598 .with_permission_handler(Arc::new(ApproveAllHandler));
7599 let ha = resolve_create(a).unwrap();
7600 let hb = resolve_create(b).unwrap();
7601 assert!(matches!(
7602 dispatch(&ha).await,
7603 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7604 ));
7605 assert!(matches!(
7606 dispatch(&hb).await,
7607 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7608 ));
7609 }
7610
7611 #[tokio::test]
7612 async fn deny_all_is_order_independent() {
7613 let a = SessionConfig::default()
7614 .with_permission_handler(Arc::new(ApproveAllHandler))
7615 .deny_all_permissions();
7616 let b = SessionConfig::default()
7617 .deny_all_permissions()
7618 .with_permission_handler(Arc::new(ApproveAllHandler));
7619 let ha = resolve_create(a).unwrap();
7620 let hb = resolve_create(b).unwrap();
7621 assert!(matches!(
7622 dispatch(&ha).await,
7623 PermissionResult::Decision(PermissionDecision::Reject(_))
7624 ));
7625 assert!(matches!(
7626 dispatch(&hb).await,
7627 PermissionResult::Decision(PermissionDecision::Reject(_))
7628 ));
7629 }
7630
7631 #[tokio::test]
7632 async fn approve_permissions_if_consults_predicate() {
7633 let cfg = SessionConfig::default().approve_permissions_if(|d| {
7634 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7635 });
7636 let h = resolve_create(cfg).unwrap();
7637 assert!(matches!(
7638 dispatch(&h).await,
7639 PermissionResult::Decision(PermissionDecision::Reject(_))
7640 ));
7641 }
7642
7643 #[tokio::test]
7644 async fn approve_permissions_if_is_order_independent() {
7645 let predicate = |d: &PermissionRequestData| {
7646 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7647 };
7648 let a = SessionConfig::default()
7649 .with_permission_handler(Arc::new(ApproveAllHandler))
7650 .approve_permissions_if(predicate);
7651 let b = SessionConfig::default()
7652 .approve_permissions_if(predicate)
7653 .with_permission_handler(Arc::new(ApproveAllHandler));
7654 let ha = resolve_create(a).unwrap();
7655 let hb = resolve_create(b).unwrap();
7656 assert!(matches!(
7657 dispatch(&ha).await,
7658 PermissionResult::Decision(PermissionDecision::Reject(_))
7659 ));
7660 assert!(matches!(
7661 dispatch(&hb).await,
7662 PermissionResult::Decision(PermissionDecision::Reject(_))
7663 ));
7664 }
7665
7666 #[tokio::test]
7667 async fn resume_session_config_approve_all_works() {
7668 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
7669 .with_permission_handler(Arc::new(ApproveAllHandler))
7670 .approve_all_permissions();
7671 let h = resolve_resume(cfg).unwrap();
7672 assert!(matches!(
7673 dispatch(&h).await,
7674 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7675 ));
7676 }
7677
7678 #[tokio::test]
7679 async fn resume_session_config_approve_all_is_order_independent() {
7680 let a = ResumeSessionConfig::new(SessionId::from("s1"))
7681 .with_permission_handler(Arc::new(ApproveAllHandler))
7682 .approve_all_permissions();
7683 let b = ResumeSessionConfig::new(SessionId::from("s1"))
7684 .approve_all_permissions()
7685 .with_permission_handler(Arc::new(ApproveAllHandler));
7686 let ha = resolve_resume(a).unwrap();
7687 let hb = resolve_resume(b).unwrap();
7688 assert!(matches!(
7689 dispatch(&ha).await,
7690 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7691 ));
7692 assert!(matches!(
7693 dispatch(&hb).await,
7694 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7695 ));
7696 }
7697
7698 #[test]
7699 fn session_config_enable_experimental_mode_serializes_when_set() {
7700 let cfg = SessionConfig::default().with_enable_experimental_mode(false);
7701 assert_eq!(cfg.enable_experimental_mode, Some(false));
7702
7703 let (wire, _runtime) = cfg
7704 .into_wire(Some(SessionId::from("experimental-mode")))
7705 .expect("enable_experimental_mode config has no duplicate handlers");
7706 assert_eq!(wire.is_experimental_mode, Some(false));
7707
7708 let json = serde_json::to_value(&wire).unwrap();
7709 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7710 }
7711
7712 #[test]
7713 fn session_config_enable_experimental_mode_omitted_when_none() {
7714 let cfg = SessionConfig::default();
7715 assert_eq!(cfg.enable_experimental_mode, None);
7716
7717 let (wire, _runtime) = cfg
7718 .into_wire(Some(SessionId::from("no-experimental-mode")))
7719 .expect("default config has no duplicate handlers");
7720 assert_eq!(wire.is_experimental_mode, None);
7721
7722 let json = serde_json::to_value(&wire).unwrap();
7723 assert!(json.get("isExperimentalMode").is_none());
7724 }
7725
7726 #[test]
7727 fn resume_session_config_enable_experimental_mode_serializes_when_set() {
7728 let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
7729 .with_enable_experimental_mode(false);
7730 assert_eq!(cfg.enable_experimental_mode, Some(false));
7731
7732 let (wire, _runtime) = cfg
7733 .into_wire()
7734 .expect("resume enable_experimental_mode config has no duplicate handlers");
7735 assert_eq!(wire.is_experimental_mode, Some(false));
7736
7737 let json = serde_json::to_value(&wire).unwrap();
7738 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7739 }
7740
7741 #[test]
7742 fn resume_session_config_enable_experimental_mode_omitted_when_none() {
7743 let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
7744 assert_eq!(cfg.enable_experimental_mode, None);
7745
7746 let (wire, _runtime) = cfg
7747 .into_wire()
7748 .expect("default resume config has no duplicate handlers");
7749 assert_eq!(wire.is_experimental_mode, None);
7750
7751 let json = serde_json::to_value(&wire).unwrap();
7752 assert!(json.get("isExperimentalMode").is_none());
7753 }
7754}
7755
7756#[cfg(test)]
7757mod is_terminal_tests {
7758 use super::Tool;
7759
7760 #[test]
7761 fn is_terminal_serializes_as_camel_case_when_set() {
7762 let tool = Tool {
7763 name: "clear_context".to_owned(),
7764 is_terminal: true,
7765 ..Default::default()
7766 };
7767 let value = serde_json::to_value(&tool).expect("tool serializes");
7768 assert_eq!(
7769 value.get("isTerminal"),
7770 Some(&serde_json::Value::Bool(true))
7771 );
7772 }
7773
7774 #[test]
7775 fn is_terminal_is_omitted_when_false() {
7776 let tool = Tool {
7777 name: "plain".to_owned(),
7778 ..Default::default()
7779 };
7780 let value = serde_json::to_value(&tool).expect("tool serializes");
7781 assert!(value.get("isTerminal").is_none());
7782 }
7783
7784 #[test]
7787 fn is_terminal_appears_in_debug_output() {
7788 let terminal = Tool {
7789 name: "clear_context".to_owned(),
7790 is_terminal: true,
7791 ..Default::default()
7792 };
7793 assert!(format!("{terminal:?}").contains("is_terminal: true"));
7794
7795 let plain = Tool {
7796 name: "plain".to_owned(),
7797 ..Default::default()
7798 };
7799 assert!(format!("{plain:?}").contains("is_terminal: false"));
7800 }
7801}