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 mut state = serializer.serialize_struct("CommandDefinition", 2)?;
618 state.serialize_field("name", &self.name)?;
619 state.serialize_field("description", self.description.as_deref().unwrap_or(""))?;
620 state.end()
621 }
622}
623
624#[derive(Debug, Clone, Default, Serialize, Deserialize)]
631#[serde(rename_all = "camelCase")]
632#[non_exhaustive]
633pub struct CustomAgentConfig {
634 pub name: String,
636 #[serde(default, skip_serializing_if = "Option::is_none")]
638 pub display_name: Option<String>,
639 #[serde(default, skip_serializing_if = "Option::is_none")]
641 pub description: Option<String>,
642 #[serde(default, skip_serializing_if = "Option::is_none")]
644 pub tools: Option<Vec<String>>,
645 pub prompt: String,
647 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
650 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub infer: Option<bool>,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub skills: Option<Vec<String>>,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
661 pub model: Option<String>,
662 #[serde(default, skip_serializing_if = "Option::is_none")]
667 pub reasoning_effort: Option<String>,
668}
669
670impl CustomAgentConfig {
671 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
678 Self {
679 name: name.into(),
680 prompt: prompt.into(),
681 ..Self::default()
682 }
683 }
684
685 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
687 self.display_name = Some(display_name.into());
688 self
689 }
690
691 pub fn with_description(mut self, description: impl Into<String>) -> Self {
693 self.description = Some(description.into());
694 self
695 }
696
697 pub fn with_tools<I, S>(mut self, tools: I) -> Self
700 where
701 I: IntoIterator<Item = S>,
702 S: Into<String>,
703 {
704 self.tools = Some(tools.into_iter().map(Into::into).collect());
705 self
706 }
707
708 pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
710 self.mcp_servers = Some(mcp_servers);
711 self
712 }
713
714 pub fn with_infer(mut self, infer: bool) -> Self {
716 self.infer = Some(infer);
717 self
718 }
719
720 pub fn with_skills<I, S>(mut self, skills: I) -> Self
722 where
723 I: IntoIterator<Item = S>,
724 S: Into<String>,
725 {
726 self.skills = Some(skills.into_iter().map(Into::into).collect());
727 self
728 }
729
730 pub fn with_model(mut self, model: impl Into<String>) -> Self {
732 self.model = Some(model.into());
733 self
734 }
735
736 pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into<String>) -> Self {
738 self.reasoning_effort = Some(reasoning_effort.into());
739 self
740 }
741}
742
743#[derive(Debug, Clone, Default, Serialize, Deserialize)]
750#[serde(rename_all = "camelCase")]
751pub struct DefaultAgentConfig {
752 #[serde(default, skip_serializing_if = "Option::is_none")]
754 pub excluded_tools: Option<Vec<String>>,
755}
756
757#[derive(Debug, Clone, Default, Serialize, Deserialize)]
763#[serde(rename_all = "camelCase")]
764#[non_exhaustive]
765pub struct LargeToolOutputConfig {
766 #[serde(default, skip_serializing_if = "Option::is_none")]
768 pub enabled: Option<bool>,
769 #[serde(default, skip_serializing_if = "Option::is_none")]
772 pub max_size_bytes: Option<u64>,
773 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
776 pub output_directory: Option<PathBuf>,
777}
778
779impl LargeToolOutputConfig {
780 pub fn new() -> Self {
783 Self::default()
784 }
785
786 pub fn with_enabled(mut self, enabled: bool) -> Self {
788 self.enabled = Some(enabled);
789 self
790 }
791
792 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
794 self.max_size_bytes = Some(max_size_bytes);
795 self
796 }
797
798 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
800 self.output_directory = Some(output_directory.into());
801 self
802 }
803}
804
805#[derive(Debug, Clone, Default, Serialize, Deserialize)]
811#[serde(rename_all = "camelCase")]
812#[non_exhaustive]
813pub struct ToolSearchConfig {
814 #[serde(default, skip_serializing_if = "Option::is_none")]
816 pub enabled: Option<bool>,
817 #[serde(default, skip_serializing_if = "Option::is_none")]
820 pub defer_threshold: Option<u32>,
821}
822
823impl ToolSearchConfig {
824 pub fn new() -> Self {
827 Self::default()
828 }
829
830 pub fn with_enabled(mut self, enabled: bool) -> Self {
832 self.enabled = Some(enabled);
833 self
834 }
835
836 pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
839 self.defer_threshold = Some(defer_threshold);
840 self
841 }
842}
843
844#[derive(Debug, Clone, Default, Serialize, Deserialize)]
849#[serde(rename_all = "camelCase")]
850#[non_exhaustive]
851pub struct GitHubMcpToolConfig {
852 #[serde(default, skip_serializing_if = "Option::is_none")]
854 pub enable_all_tools: Option<bool>,
855 #[serde(default, skip_serializing_if = "Option::is_none")]
857 pub additional_toolsets: Option<Vec<String>>,
858 #[serde(default, skip_serializing_if = "Option::is_none")]
860 pub additional_tools: Option<Vec<String>>,
861 #[serde(default, skip_serializing_if = "Option::is_none")]
863 pub enable_insiders_mode: Option<bool>,
864 #[serde(default, skip_serializing_if = "Option::is_none")]
868 pub disable_form_deferral: Option<bool>,
869}
870
871impl GitHubMcpToolConfig {
872 pub fn new() -> Self {
874 Self::default()
875 }
876
877 pub fn with_enable_all_tools(mut self, value: bool) -> Self {
879 self.enable_all_tools = Some(value);
880 self
881 }
882
883 pub fn with_additional_toolsets<I, S>(mut self, values: I) -> Self
885 where
886 I: IntoIterator<Item = S>,
887 S: Into<String>,
888 {
889 self.additional_toolsets = Some(values.into_iter().map(Into::into).collect());
890 self
891 }
892
893 pub fn with_additional_tools<I, S>(mut self, values: I) -> Self
895 where
896 I: IntoIterator<Item = S>,
897 S: Into<String>,
898 {
899 self.additional_tools = Some(values.into_iter().map(Into::into).collect());
900 self
901 }
902
903 pub fn with_enable_insiders_mode(mut self, value: bool) -> Self {
905 self.enable_insiders_mode = Some(value);
906 self
907 }
908
909 pub fn with_disable_form_deferral(mut self, value: bool) -> Self {
913 self.disable_form_deferral = Some(value);
914 self
915 }
916}
917
918#[derive(Debug, Clone, Default, Serialize, Deserialize)]
925#[serde(rename_all = "camelCase")]
926#[non_exhaustive]
927pub struct InfiniteSessionConfig {
928 #[serde(default, skip_serializing_if = "Option::is_none")]
930 pub enabled: Option<bool>,
931 #[serde(default, skip_serializing_if = "Option::is_none")]
934 pub background_compaction_threshold: Option<f64>,
935 #[serde(default, skip_serializing_if = "Option::is_none")]
938 pub buffer_exhaustion_threshold: Option<f64>,
939}
940
941impl InfiniteSessionConfig {
942 pub fn new() -> Self {
945 Self::default()
946 }
947
948 pub fn with_enabled(mut self, enabled: bool) -> Self {
951 self.enabled = Some(enabled);
952 self
953 }
954
955 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
958 self.background_compaction_threshold = Some(threshold);
959 self
960 }
961
962 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
965 self.buffer_exhaustion_threshold = Some(threshold);
966 self
967 }
968}
969
970#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
981#[serde(rename_all = "camelCase")]
982#[non_exhaustive]
983pub struct MemoryConfiguration {
984 pub enabled: bool,
986}
987
988impl MemoryConfiguration {
989 pub fn enabled() -> Self {
991 Self { enabled: true }
992 }
993
994 pub fn disabled() -> Self {
996 Self { enabled: false }
997 }
998
999 pub fn with_enabled(mut self, enabled: bool) -> Self {
1001 self.enabled = enabled;
1002 self
1003 }
1004}
1005
1006#[derive(Debug, Clone, Serialize, Deserialize)]
1008#[serde(rename_all = "camelCase")]
1009#[non_exhaustive]
1010pub struct CloudSessionRepository {
1011 pub owner: String,
1013 pub name: String,
1015 #[serde(skip_serializing_if = "Option::is_none")]
1017 pub branch: Option<String>,
1018}
1019
1020impl CloudSessionRepository {
1021 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
1023 Self {
1024 owner: owner.into(),
1025 name: name.into(),
1026 branch: None,
1027 }
1028 }
1029
1030 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
1032 self.branch = Some(branch.into());
1033 self
1034 }
1035}
1036
1037#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1039#[serde(rename_all = "camelCase")]
1040#[non_exhaustive]
1041pub struct CloudSessionOptions {
1042 #[serde(skip_serializing_if = "Option::is_none")]
1044 pub repository: Option<CloudSessionRepository>,
1045}
1046
1047impl CloudSessionOptions {
1048 pub fn with_repository(repository: CloudSessionRepository) -> Self {
1050 Self {
1051 repository: Some(repository),
1052 }
1053 }
1054}
1055
1056#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1058#[serde(rename_all = "camelCase")]
1059pub struct ExtensionInfo {
1060 pub source: String,
1062 pub name: String,
1064}
1065
1066impl ExtensionInfo {
1067 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
1069 Self {
1070 source: source.into(),
1071 name: name.into(),
1072 }
1073 }
1074}
1075
1076#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1087#[serde(rename_all = "camelCase")]
1088pub struct CanvasProviderIdentity {
1089 pub id: String,
1091 #[serde(skip_serializing_if = "Option::is_none")]
1093 pub name: Option<String>,
1094}
1095
1096impl CanvasProviderIdentity {
1097 pub fn new(id: impl Into<String>) -> Self {
1099 Self {
1100 id: id.into(),
1101 name: None,
1102 }
1103 }
1104
1105 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1107 self.name = Some(name.into());
1108 self
1109 }
1110}
1111
1112#[derive(Debug, Clone, Serialize, Deserialize)]
1146#[serde(tag = "type", rename_all = "lowercase")]
1147#[non_exhaustive]
1148pub enum McpServerConfig {
1149 #[serde(alias = "local")]
1153 Stdio(McpStdioServerConfig),
1154 Http(McpHttpServerConfig),
1156 Sse(McpHttpServerConfig),
1158}
1159
1160#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1164#[serde(rename_all = "camelCase")]
1165pub struct McpStdioServerConfig {
1166 #[serde(default, skip_serializing_if = "Option::is_none")]
1172 pub tools: Option<Vec<String>>,
1173 #[serde(default, skip_serializing_if = "Option::is_none")]
1175 pub timeout: Option<i64>,
1176 pub command: String,
1178 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1180 pub args: Vec<String>,
1181 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1184 pub env: HashMap<String, String>,
1185 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1187 pub working_directory: Option<String>,
1188}
1189
1190#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1194#[serde(rename_all = "camelCase")]
1195pub struct McpHttpServerConfig {
1196 #[serde(default, skip_serializing_if = "Option::is_none")]
1202 pub tools: Option<Vec<String>>,
1203 #[serde(default, skip_serializing_if = "Option::is_none")]
1205 pub timeout: Option<i64>,
1206 pub url: String,
1208 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1210 pub headers: HashMap<String, String>,
1211}
1212
1213#[derive(Clone, Default, Serialize, Deserialize)]
1219#[serde(rename_all = "camelCase")]
1220#[non_exhaustive]
1221pub struct ProviderConfig {
1222 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1225 pub provider_type: Option<String>,
1226 #[serde(default, skip_serializing_if = "Option::is_none")]
1229 pub wire_api: Option<String>,
1230 #[serde(default, skip_serializing_if = "Option::is_none")]
1235 pub transport: Option<String>,
1236 pub base_url: String,
1238 #[serde(default, skip_serializing_if = "Option::is_none")]
1240 pub api_key: Option<String>,
1241 #[serde(default, skip_serializing_if = "Option::is_none")]
1245 pub bearer_token: Option<String>,
1246 #[serde(skip)]
1249 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1250 #[serde(default, skip_serializing_if = "Option::is_none")]
1251 pub(crate) has_bearer_token_provider: Option<bool>,
1252 #[serde(default, skip_serializing_if = "Option::is_none")]
1254 pub azure: Option<AzureProviderOptions>,
1255 #[serde(default, skip_serializing_if = "Option::is_none")]
1257 pub headers: Option<HashMap<String, String>>,
1258 #[serde(default, skip_serializing_if = "Option::is_none")]
1262 pub model_id: Option<String>,
1263 #[serde(default, skip_serializing_if = "Option::is_none")]
1270 pub wire_model: Option<String>,
1271 #[serde(default, skip_serializing_if = "Option::is_none")]
1276 pub max_prompt_tokens: Option<i64>,
1277 #[serde(default, skip_serializing_if = "Option::is_none")]
1280 pub max_output_tokens: Option<i64>,
1281}
1282
1283impl std::fmt::Debug for ProviderConfig {
1284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1285 f.debug_struct("ProviderConfig")
1286 .field("provider_type", &self.provider_type)
1287 .field("wire_api", &self.wire_api)
1288 .field("transport", &self.transport)
1289 .field("base_url", &self.base_url)
1290 .field("api_key", &self.api_key)
1291 .field("bearer_token", &self.bearer_token)
1292 .field(
1293 "bearer_token_provider",
1294 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1295 )
1296 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1297 .field("azure", &self.azure)
1298 .field("headers", &self.headers)
1299 .field("model_id", &self.model_id)
1300 .field("wire_model", &self.wire_model)
1301 .field("max_prompt_tokens", &self.max_prompt_tokens)
1302 .field("max_output_tokens", &self.max_output_tokens)
1303 .finish()
1304 }
1305}
1306
1307impl ProviderConfig {
1308 pub fn new(base_url: impl Into<String>) -> Self {
1311 Self {
1312 base_url: base_url.into(),
1313 ..Self::default()
1314 }
1315 }
1316
1317 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1319 self.provider_type = Some(provider_type.into());
1320 self
1321 }
1322
1323 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1325 self.wire_api = Some(wire_api.into());
1326 self
1327 }
1328
1329 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1332 self.transport = Some(transport.into());
1333 self
1334 }
1335
1336 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1338 self.api_key = Some(api_key.into());
1339 self
1340 }
1341
1342 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1345 self.bearer_token = Some(bearer_token.into());
1346 self
1347 }
1348
1349 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1355 self.bearer_token_provider = Some(provider);
1356 self
1357 }
1358
1359 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1361 self.azure = Some(azure);
1362 self
1363 }
1364
1365 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1367 self.headers = Some(headers);
1368 self
1369 }
1370
1371 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1374 self.model_id = Some(model_id.into());
1375 self
1376 }
1377
1378 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1383 self.wire_model = Some(wire_model.into());
1384 self
1385 }
1386
1387 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1391 self.max_prompt_tokens = Some(max);
1392 self
1393 }
1394
1395 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1398 self.max_output_tokens = Some(max);
1399 self
1400 }
1401}
1402
1403#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1416#[serde(rename_all = "camelCase")]
1417#[non_exhaustive]
1418pub struct CapiSessionOptions {
1419 #[serde(default, skip_serializing_if = "Option::is_none")]
1425 pub enable_web_socket_responses: Option<bool>,
1426}
1427
1428impl CapiSessionOptions {
1429 pub fn new() -> Self {
1431 Self::default()
1432 }
1433
1434 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1436 self.enable_web_socket_responses = Some(enable);
1437 self
1438 }
1439}
1440
1441#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1443#[serde(rename_all = "camelCase")]
1444pub struct AzureProviderOptions {
1445 #[serde(default, skip_serializing_if = "Option::is_none")]
1447 pub api_version: Option<String>,
1448}
1449
1450#[derive(Clone, Default, Serialize, Deserialize)]
1461#[serde(rename_all = "camelCase")]
1462#[non_exhaustive]
1463pub struct NamedProviderConfig {
1464 pub name: String,
1467 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1470 pub provider_type: Option<String>,
1471 #[serde(default, skip_serializing_if = "Option::is_none")]
1474 pub wire_api: Option<String>,
1475 pub base_url: String,
1477 #[serde(default, skip_serializing_if = "Option::is_none")]
1479 pub api_key: Option<String>,
1480 #[serde(default, skip_serializing_if = "Option::is_none")]
1483 pub bearer_token: Option<String>,
1484 #[serde(skip)]
1487 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1488 #[serde(default, skip_serializing_if = "Option::is_none")]
1489 pub(crate) has_bearer_token_provider: Option<bool>,
1490 #[serde(default, skip_serializing_if = "Option::is_none")]
1492 pub azure: Option<AzureProviderOptions>,
1493 #[serde(default, skip_serializing_if = "Option::is_none")]
1495 pub headers: Option<HashMap<String, String>>,
1496}
1497
1498impl std::fmt::Debug for NamedProviderConfig {
1499 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1500 f.debug_struct("NamedProviderConfig")
1501 .field("name", &self.name)
1502 .field("provider_type", &self.provider_type)
1503 .field("wire_api", &self.wire_api)
1504 .field("base_url", &self.base_url)
1505 .field("api_key", &self.api_key)
1506 .field("bearer_token", &self.bearer_token)
1507 .field(
1508 "bearer_token_provider",
1509 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1510 )
1511 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1512 .field("azure", &self.azure)
1513 .field("headers", &self.headers)
1514 .finish()
1515 }
1516}
1517
1518impl NamedProviderConfig {
1519 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1522 Self {
1523 name: name.into(),
1524 base_url: base_url.into(),
1525 ..Self::default()
1526 }
1527 }
1528
1529 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1531 self.provider_type = Some(provider_type.into());
1532 self
1533 }
1534
1535 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1537 self.wire_api = Some(wire_api.into());
1538 self
1539 }
1540
1541 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1543 self.api_key = Some(api_key.into());
1544 self
1545 }
1546
1547 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1550 self.bearer_token = Some(bearer_token.into());
1551 self
1552 }
1553
1554 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1560 self.bearer_token_provider = Some(provider);
1561 self
1562 }
1563
1564 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1566 self.azure = Some(azure);
1567 self
1568 }
1569
1570 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1572 self.headers = Some(headers);
1573 self
1574 }
1575}
1576
1577fn prepare_bearer_token_providers(
1578 provider: &mut Option<ProviderConfig>,
1579 providers: &mut Option<Vec<NamedProviderConfig>>,
1580) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1581 let mut bearer_token_providers = HashMap::new();
1582
1583 if let Some(provider) = provider.as_mut()
1584 && let Some(token_provider) = provider.bearer_token_provider.take()
1585 {
1586 provider.has_bearer_token_provider = Some(true);
1587 bearer_token_providers.insert("default".to_string(), token_provider);
1588 }
1589
1590 if let Some(providers) = providers.as_mut() {
1591 for provider in providers {
1592 if let Some(token_provider) = provider.bearer_token_provider.take() {
1593 provider.has_bearer_token_provider = Some(true);
1594 bearer_token_providers.insert(provider.name.clone(), token_provider);
1595 }
1596 }
1597 }
1598
1599 bearer_token_providers
1600}
1601
1602#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1610#[serde(rename_all = "camelCase")]
1611#[non_exhaustive]
1612pub struct ProviderModelConfig {
1613 pub id: String,
1616 pub provider: String,
1618 #[serde(default, skip_serializing_if = "Option::is_none")]
1621 pub wire_model: Option<String>,
1622 #[serde(default, skip_serializing_if = "Option::is_none")]
1625 pub model_id: Option<String>,
1626 #[serde(default, skip_serializing_if = "Option::is_none")]
1628 pub name: Option<String>,
1629 #[serde(default, skip_serializing_if = "Option::is_none")]
1631 pub max_prompt_tokens: Option<i64>,
1632 #[serde(default, skip_serializing_if = "Option::is_none")]
1634 pub max_context_window_tokens: Option<i64>,
1635 #[serde(default, skip_serializing_if = "Option::is_none")]
1637 pub max_output_tokens: Option<i64>,
1638 #[serde(default, skip_serializing_if = "Option::is_none")]
1641 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1642}
1643
1644impl ProviderModelConfig {
1645 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1648 Self {
1649 id: id.into(),
1650 provider: provider.into(),
1651 ..Self::default()
1652 }
1653 }
1654
1655 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1657 self.wire_model = Some(wire_model.into());
1658 self
1659 }
1660
1661 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1664 self.model_id = Some(model_id.into());
1665 self
1666 }
1667
1668 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1670 self.name = Some(name.into());
1671 self
1672 }
1673
1674 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1676 self.max_prompt_tokens = Some(max);
1677 self
1678 }
1679
1680 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1682 self.max_context_window_tokens = Some(max);
1683 self
1684 }
1685
1686 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1688 self.max_output_tokens = Some(max);
1689 self
1690 }
1691
1692 pub fn with_capabilities(
1694 mut self,
1695 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1696 ) -> Self {
1697 self.capabilities = Some(capabilities);
1698 self
1699 }
1700}
1701
1702#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1706#[serde(untagged)]
1707pub enum ExpFlagValue {
1708 Bool(bool),
1710 Integer(i64),
1712 Float(f64),
1714 String(String),
1716 Null,
1718}
1719
1720#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1724#[serde(rename_all = "PascalCase")]
1725pub struct ExpConfigEntry {
1726 pub id: String,
1728 pub parameters: HashMap<String, ExpFlagValue>,
1730}
1731
1732#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1738#[serde(rename_all = "PascalCase")]
1739pub struct CopilotExpAssignmentResponse {
1740 #[serde(default)]
1742 pub features: Vec<String>,
1743 #[serde(default)]
1745 pub flights: HashMap<String, String>,
1746 #[serde(default)]
1748 pub configs: Vec<ExpConfigEntry>,
1749 #[serde(default, skip_serializing_if = "Option::is_none")]
1751 pub parameter_groups: Option<Value>,
1752 #[serde(default, skip_serializing_if = "Option::is_none")]
1754 pub flighting_version: Option<i64>,
1755 #[serde(default, skip_serializing_if = "Option::is_none")]
1757 pub impression_id: Option<String>,
1758 #[serde(default)]
1760 pub assignment_context: String,
1761}
1762
1763pub struct DisableBypassPermissionsModes;
1765
1766impl DisableBypassPermissionsModes {
1767 pub const ALLOW_AUTO_ONLY: &'static str = "allow-auto-only";
1769 pub const DISABLE: &'static str = "disable";
1771}
1772
1773#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1782#[serde(rename_all = "camelCase")]
1783#[non_exhaustive]
1784pub struct ManagedSettingsPermissions {
1785 #[serde(default, skip_serializing_if = "Option::is_none")]
1789 pub disable_bypass_permissions_mode: Option<String>,
1790 #[serde(default, skip_serializing_if = "Option::is_none")]
1792 pub deny: Option<Vec<String>>,
1793 #[serde(default, skip_serializing_if = "Option::is_none")]
1795 pub ask: Option<Vec<String>>,
1796 #[serde(default, skip_serializing_if = "Option::is_none")]
1798 pub allow: Option<Vec<String>>,
1799}
1800
1801impl ManagedSettingsPermissions {
1802 pub fn with_disable_bypass_permissions_mode(mut self, value: impl Into<String>) -> Self {
1804 self.disable_bypass_permissions_mode = Some(value.into());
1805 self
1806 }
1807
1808 pub fn with_deny(mut self, rules: Vec<String>) -> Self {
1810 self.deny = Some(rules);
1811 self
1812 }
1813
1814 pub fn with_ask(mut self, rules: Vec<String>) -> Self {
1816 self.ask = Some(rules);
1817 self
1818 }
1819
1820 pub fn with_allow(mut self, rules: Vec<String>) -> Self {
1822 self.allow = Some(rules);
1823 self
1824 }
1825}
1826
1827#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1837#[serde(rename_all = "camelCase")]
1838#[non_exhaustive]
1839pub struct ManagedSettings {
1840 #[serde(default, skip_serializing_if = "Option::is_none")]
1842 pub permissions: Option<ManagedSettingsPermissions>,
1843}
1844
1845impl ManagedSettings {
1846 pub fn with_permissions(mut self, permissions: ManagedSettingsPermissions) -> Self {
1848 self.permissions = Some(permissions);
1849 self
1850 }
1851}
1852
1853#[derive(Clone)]
1905#[non_exhaustive]
1906pub struct SessionConfig {
1907 pub session_id: Option<SessionId>,
1909 pub model: Option<String>,
1911 pub client_name: Option<String>,
1913 pub reasoning_effort: Option<String>,
1915 pub reasoning_summary: Option<ReasoningSummary>,
1919 pub context_tier: Option<String>,
1922 pub streaming: Option<bool>,
1924 pub system_message: Option<SystemMessageConfig>,
1926 pub tools: Option<Vec<Tool>>,
1928 pub canvases: Option<Vec<CanvasDeclaration>>,
1930 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1935 pub request_canvas_renderer: Option<bool>,
1937 pub request_extensions: Option<bool>,
1939 pub extension_sdk_path: Option<String>,
1943 pub extension_info: Option<ExtensionInfo>,
1945 pub canvas_provider: Option<CanvasProviderIdentity>,
1948 pub available_tools: Option<Vec<String>>,
1950 pub excluded_tools: Option<Vec<String>>,
1952 pub excluded_builtin_agents: Option<Vec<String>>,
1958 pub included_builtin_skills: Option<Vec<String>>,
1962 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
1964 pub mcp_oauth_token_storage: Option<String>,
1973 pub enable_config_discovery: Option<bool>,
1976 pub skip_embedding_retrieval: Option<bool>,
1978 pub embedding_cache_storage: Option<String>,
1981 pub organization_custom_instructions: Option<String>,
1983 pub enable_on_demand_instruction_discovery: Option<bool>,
1985 pub enable_file_hooks: Option<bool>,
1987 pub enable_host_git_operations: Option<bool>,
1989 pub enable_session_store: Option<bool>,
1991 pub enable_skills: Option<bool>,
1993 pub enable_mcp_apps: Option<bool>,
2020 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
2025 pub skill_directories: Option<Vec<PathBuf>>,
2027 pub instruction_directories: Option<Vec<PathBuf>>,
2030 pub plugin_directories: Option<Vec<PathBuf>>,
2032 pub large_output: Option<LargeToolOutputConfig>,
2034 pub tool_search: Option<ToolSearchConfig>,
2038 pub disabled_skills: Option<Vec<String>>,
2041 pub disabled_mcp_servers: Option<Vec<String>>,
2045 pub hooks: Option<bool>,
2049 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2051 pub default_agent: Option<DefaultAgentConfig>,
2055 pub agent: Option<String>,
2058 pub infinite_sessions: Option<InfiniteSessionConfig>,
2061 pub provider: Option<ProviderConfig>,
2065 pub capi: Option<CapiSessionOptions>,
2071 pub providers: Option<Vec<NamedProviderConfig>>,
2078 pub models: Option<Vec<ProviderModelConfig>>,
2084 pub enable_session_telemetry: Option<bool>,
2092 pub enable_citations: Option<bool>,
2094 pub enable_file_change_tracking: Option<bool>,
2097 pub session_limits: Option<SessionLimitsConfig>,
2099 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2102 pub memory: Option<MemoryConfiguration>,
2104 pub config_directory: Option<PathBuf>,
2107 pub working_directory: Option<PathBuf>,
2110 pub additional_directories: Option<Vec<PathBuf>>,
2114 pub github_token: Option<String>,
2120 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2126 pub cloud: Option<CloudSessionOptions>,
2129 pub include_sub_agent_streaming_events: Option<bool>,
2133 pub commands: Option<Vec<CommandDefinition>>,
2137 #[doc(hidden)]
2144 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
2145 pub enable_managed_settings: Option<bool>,
2152 pub managed_settings: Option<ManagedSettings>,
2161 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2166 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2170 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2173 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2176 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2180 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2183 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2186 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2190 pub(crate) permission_policy: Option<crate::permission::Policy>,
2194 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2199 pub skip_custom_instructions: Option<bool>,
2203 pub custom_agents_local_only: Option<bool>,
2207 pub enable_experimental_mode: Option<bool>,
2212 pub coauthor_enabled: Option<bool>,
2216 pub manage_schedule_enabled: Option<bool>,
2220}
2221
2222impl std::fmt::Debug for SessionConfig {
2223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2224 f.debug_struct("SessionConfig")
2225 .field("session_id", &self.session_id)
2226 .field("model", &self.model)
2227 .field("client_name", &self.client_name)
2228 .field("reasoning_effort", &self.reasoning_effort)
2229 .field("reasoning_summary", &self.reasoning_summary)
2230 .field("context_tier", &self.context_tier)
2231 .field("streaming", &self.streaming)
2232 .field("system_message", &self.system_message)
2233 .field("tools", &self.tools)
2234 .field("canvases", &self.canvases)
2235 .field(
2236 "canvas_handler",
2237 &self.canvas_handler.as_ref().map(|_| "<set>"),
2238 )
2239 .field("request_canvas_renderer", &self.request_canvas_renderer)
2240 .field("request_extensions", &self.request_extensions)
2241 .field("extension_sdk_path", &self.extension_sdk_path)
2242 .field("extension_info", &self.extension_info)
2243 .field("canvas_provider", &self.canvas_provider)
2244 .field("available_tools", &self.available_tools)
2245 .field("excluded_tools", &self.excluded_tools)
2246 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2247 .field("included_builtin_skills", &self.included_builtin_skills)
2248 .field("mcp_servers", &self.mcp_servers)
2249 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2250 .field("embedding_cache_storage", &self.embedding_cache_storage)
2251 .field("enable_config_discovery", &self.enable_config_discovery)
2252 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2253 .field(
2254 "organization_custom_instructions",
2255 &self
2256 .organization_custom_instructions
2257 .as_ref()
2258 .map(|_| "<redacted>"),
2259 )
2260 .field(
2261 "enable_on_demand_instruction_discovery",
2262 &self.enable_on_demand_instruction_discovery,
2263 )
2264 .field("enable_file_hooks", &self.enable_file_hooks)
2265 .field(
2266 "enable_host_git_operations",
2267 &self.enable_host_git_operations,
2268 )
2269 .field("enable_session_store", &self.enable_session_store)
2270 .field("enable_skills", &self.enable_skills)
2271 .field("enable_mcp_apps", &self.enable_mcp_apps)
2272 .field("skill_directories", &self.skill_directories)
2273 .field("instruction_directories", &self.instruction_directories)
2274 .field("plugin_directories", &self.plugin_directories)
2275 .field("large_output", &self.large_output)
2276 .field("tool_search", &self.tool_search)
2277 .field("disabled_skills", &self.disabled_skills)
2278 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
2279 .field("hooks", &self.hooks)
2280 .field("custom_agents", &self.custom_agents)
2281 .field("default_agent", &self.default_agent)
2282 .field("agent", &self.agent)
2283 .field("infinite_sessions", &self.infinite_sessions)
2284 .field("provider", &self.provider)
2285 .field("capi", &self.capi)
2286 .field("enable_session_telemetry", &self.enable_session_telemetry)
2287 .field("enable_citations", &self.enable_citations)
2288 .field(
2289 "enable_file_change_tracking",
2290 &self.enable_file_change_tracking,
2291 )
2292 .field("session_limits", &self.session_limits)
2293 .field("model_capabilities", &self.model_capabilities)
2294 .field("memory", &self.memory)
2295 .field("config_directory", &self.config_directory)
2296 .field("working_directory", &self.working_directory)
2297 .field("additional_directories", &self.additional_directories)
2298 .field(
2299 "github_token",
2300 &self.github_token.as_ref().map(|_| "<redacted>"),
2301 )
2302 .field("remote_session", &self.remote_session)
2303 .field("cloud", &self.cloud)
2304 .field(
2305 "include_sub_agent_streaming_events",
2306 &self.include_sub_agent_streaming_events,
2307 )
2308 .field("commands", &self.commands)
2309 .field("exp_assignments", &self.exp_assignments)
2310 .field("enable_managed_settings", &self.enable_managed_settings)
2311 .field("enable_experimental_mode", &self.enable_experimental_mode)
2312 .field("managed_settings", &self.managed_settings)
2313 .field(
2314 "session_fs_provider",
2315 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2316 )
2317 .field(
2318 "permission_handler",
2319 &self.permission_handler.as_ref().map(|_| "<set>"),
2320 )
2321 .field(
2322 "elicitation_handler",
2323 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2324 )
2325 .field(
2326 "mcp_auth_handler",
2327 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2328 )
2329 .field(
2330 "user_input_handler",
2331 &self.user_input_handler.as_ref().map(|_| "<set>"),
2332 )
2333 .field(
2334 "exit_plan_mode_handler",
2335 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2336 )
2337 .field(
2338 "auto_mode_switch_handler",
2339 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2340 )
2341 .field(
2342 "hooks_handler",
2343 &self.hooks_handler.as_ref().map(|_| "<set>"),
2344 )
2345 .field(
2346 "system_message_transform",
2347 &self.system_message_transform.as_ref().map(|_| "<set>"),
2348 )
2349 .finish()
2350 }
2351}
2352
2353impl Default for SessionConfig {
2354 fn default() -> Self {
2360 Self {
2361 session_id: None,
2362 model: None,
2363 client_name: None,
2364 reasoning_effort: None,
2365 reasoning_summary: None,
2366 context_tier: None,
2367 streaming: None,
2368 system_message: None,
2369 tools: None,
2370 canvases: None,
2371 canvas_handler: None,
2372 request_canvas_renderer: None,
2373 request_extensions: None,
2374 extension_sdk_path: None,
2375 extension_info: None,
2376 canvas_provider: None,
2377 available_tools: None,
2378 excluded_tools: None,
2379 excluded_builtin_agents: None,
2380 included_builtin_skills: None,
2381 mcp_servers: None,
2382 mcp_oauth_token_storage: None,
2383 enable_config_discovery: None,
2384 skip_embedding_retrieval: None,
2385 organization_custom_instructions: None,
2386 enable_on_demand_instruction_discovery: None,
2387 enable_file_hooks: None,
2388 enable_host_git_operations: None,
2389 enable_session_store: None,
2390 enable_skills: None,
2391 embedding_cache_storage: None,
2392 enable_mcp_apps: None,
2393 github_mcp_tool_config: None,
2394 skill_directories: None,
2395 instruction_directories: None,
2396 plugin_directories: None,
2397 large_output: None,
2398 tool_search: None,
2399 disabled_skills: None,
2400 disabled_mcp_servers: None,
2401 hooks: None,
2402 custom_agents: None,
2403 default_agent: None,
2404 agent: None,
2405 infinite_sessions: None,
2406 provider: None,
2407 capi: None,
2408 providers: None,
2409 models: None,
2410 enable_session_telemetry: None,
2411 enable_citations: None,
2412 enable_file_change_tracking: None,
2413 session_limits: None,
2414 model_capabilities: None,
2415 memory: None,
2416 config_directory: None,
2417 working_directory: None,
2418 additional_directories: None,
2419 github_token: None,
2420 remote_session: None,
2421 cloud: None,
2422 include_sub_agent_streaming_events: None,
2423 commands: None,
2424 exp_assignments: None,
2425 enable_managed_settings: None,
2426 managed_settings: None,
2427 session_fs_provider: None,
2428 permission_handler: None,
2429 elicitation_handler: None,
2430 mcp_auth_handler: None,
2431 user_input_handler: None,
2432 exit_plan_mode_handler: None,
2433 auto_mode_switch_handler: None,
2434 hooks_handler: None,
2435 permission_policy: None,
2436 system_message_transform: None,
2437 skip_custom_instructions: None,
2438 custom_agents_local_only: None,
2439 enable_experimental_mode: None,
2440 coauthor_enabled: None,
2441 manage_schedule_enabled: None,
2442 }
2443 }
2444}
2445
2446pub(crate) struct SessionConfigRuntime {
2452 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2453 pub permission_policy: Option<crate::permission::Policy>,
2454 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2455 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2456 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2457 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2458 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2459 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2460 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2461 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2462 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2463 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2464 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2465 pub commands: Option<Vec<CommandDefinition>>,
2466}
2467
2468impl SessionConfig {
2469 pub(crate) fn into_wire(
2481 mut self,
2482 session_id: Option<SessionId>,
2483 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2484 let permission_active =
2485 self.permission_handler.is_some() || self.permission_policy.is_some();
2486 let request_user_input = self.user_input_handler.is_some();
2487 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2488 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2489 let request_elicitation = self.elicitation_handler.is_some();
2490 let hooks_flag = self.hooks_handler.is_some();
2491
2492 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2493 if let Some(tools) = self.tools.as_mut() {
2494 for tool in tools.iter_mut() {
2495 if let Some(handler) = tool.handler.take()
2496 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2497 {
2498 return Err(crate::Error::with_message(
2499 crate::ErrorKind::InvalidConfig,
2500 format!("duplicate tool handler registered for name {:?}", tool.name),
2501 ));
2502 }
2503 }
2504 }
2505
2506 let wire_commands = self.commands.as_ref().map(|cmds| {
2507 cmds.iter()
2508 .map(|c| crate::wire::CommandWireDefinition {
2509 name: c.name.clone(),
2510 description: c.description.clone().unwrap_or_default(),
2511 })
2512 .collect()
2513 });
2514 let wire_canvases = self.canvases.clone();
2515 let canvas_handler = self.canvas_handler.clone();
2516 let bearer_token_providers =
2517 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2518
2519 let wire = crate::wire::SessionCreateWire {
2520 session_id,
2521 model: self.model,
2522 client_name: self.client_name,
2523 reasoning_effort: self.reasoning_effort,
2524 reasoning_summary: self.reasoning_summary,
2525 context_tier: self.context_tier,
2526 streaming: self.streaming,
2527 system_message: self.system_message,
2528 tools: self.tools,
2529 canvases: wire_canvases,
2530 request_canvas_renderer: self.request_canvas_renderer,
2531 request_extensions: self.request_extensions,
2532 extension_sdk_path: self.extension_sdk_path,
2533 extension_info: self.extension_info,
2534 canvas_provider: self.canvas_provider,
2535 available_tools: self.available_tools,
2536 excluded_tools: self.excluded_tools,
2537 excluded_builtin_agents: self.excluded_builtin_agents,
2538 tool_filter_precedence: "excluded",
2539 mcp_servers: self.mcp_servers,
2540 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2541 embedding_cache_storage: self.embedding_cache_storage,
2542 env_value_mode: "direct",
2543 enable_config_discovery: self.enable_config_discovery,
2544 skip_embedding_retrieval: self.skip_embedding_retrieval,
2545 organization_custom_instructions: self.organization_custom_instructions,
2546 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2547 enable_file_hooks: self.enable_file_hooks,
2548 enable_host_git_operations: self.enable_host_git_operations,
2549 enable_session_store: self.enable_session_store,
2550 enable_skills: self.enable_skills,
2551 request_user_input,
2552 request_permission: permission_active,
2553 request_exit_plan_mode,
2554 request_auto_mode_switch,
2555 request_elicitation,
2556 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2557 github_mcp_tool_config: self.github_mcp_tool_config,
2558 hooks: hooks_flag,
2559 skill_directories: self.skill_directories,
2560 instruction_directories: self.instruction_directories,
2561 plugin_directories: self.plugin_directories,
2562 large_output: self.large_output,
2563 tool_search: self.tool_search,
2564 disabled_skills: self.disabled_skills,
2565 disabled_mcp_servers: self.disabled_mcp_servers,
2566 custom_agents: self.custom_agents,
2567 custom_agents_local_only: self.custom_agents_local_only,
2568 default_agent: self.default_agent,
2569 agent: self.agent,
2570 infinite_sessions: self.infinite_sessions,
2571 provider: self.provider,
2572 capi: self.capi,
2573 providers: self.providers,
2574 models: self.models,
2575 enable_session_telemetry: self.enable_session_telemetry,
2576 enable_citations: self.enable_citations,
2577 enable_file_change_tracking: self.enable_file_change_tracking,
2578 session_limits: self.session_limits,
2579 model_capabilities: self.model_capabilities,
2580 memory: self.memory,
2581 config_dir: self.config_directory,
2582 working_directory: self.working_directory,
2583 additional_directories: self.additional_directories,
2584 github_token: self.github_token,
2585 remote_session: self.remote_session,
2586 cloud: self.cloud,
2587 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2588 enable_github_telemetry_forwarding: None,
2589 commands: wire_commands,
2590 exp_assignments: self.exp_assignments,
2591 enable_managed_settings: self.enable_managed_settings,
2592 is_experimental_mode: self.enable_experimental_mode,
2593 managed_settings: self.managed_settings,
2594 };
2595
2596 let runtime = SessionConfigRuntime {
2597 permission_handler: self.permission_handler,
2598 permission_policy: self.permission_policy,
2599 elicitation_handler: self.elicitation_handler,
2600 mcp_auth_handler: self.mcp_auth_handler,
2601 user_input_handler: self.user_input_handler,
2602 exit_plan_mode_handler: self.exit_plan_mode_handler,
2603 auto_mode_switch_handler: self.auto_mode_switch_handler,
2604 hooks_handler: self.hooks_handler,
2605 system_message_transform: self.system_message_transform,
2606 tool_handlers,
2607 canvas_handler,
2608 session_fs_provider: self.session_fs_provider,
2609 bearer_token_providers,
2610 commands: self.commands,
2611 };
2612
2613 Ok((wire, runtime))
2614 }
2615
2616 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2620 self.permission_handler = Some(handler);
2621 self
2622 }
2623
2624 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2627 self.elicitation_handler = Some(handler);
2628 self
2629 }
2630
2631 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2633 self.mcp_auth_handler = Some(handler);
2634 self
2635 }
2636
2637 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2640 self.user_input_handler = Some(handler);
2641 self
2642 }
2643
2644 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2646 self.exit_plan_mode_handler = Some(handler);
2647 self
2648 }
2649
2650 pub fn with_auto_mode_switch_handler(
2652 mut self,
2653 handler: Arc<dyn AutoModeSwitchHandler>,
2654 ) -> Self {
2655 self.auto_mode_switch_handler = Some(handler);
2656 self
2657 }
2658
2659 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2664 self.commands = Some(commands);
2665 self
2666 }
2667
2668 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2672 self.session_fs_provider = Some(provider);
2673 self
2674 }
2675
2676 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2679 self.hooks_handler = Some(hooks);
2680 self
2681 }
2682
2683 pub fn with_system_message_transform(
2687 mut self,
2688 transform: Arc<dyn SystemMessageTransform>,
2689 ) -> Self {
2690 self.system_message_transform = Some(transform);
2691 self
2692 }
2693
2694 pub fn approve_all_permissions(mut self) -> Self {
2700 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2701 self
2702 }
2703
2704 pub fn deny_all_permissions(mut self) -> Self {
2707 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2708 self
2709 }
2710
2711 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2716 where
2717 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2718 {
2719 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2720 self
2721 }
2722
2723 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2725 self.session_id = Some(id.into());
2726 self
2727 }
2728
2729 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2731 self.model = Some(model.into());
2732 self
2733 }
2734
2735 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2737 self.client_name = Some(name.into());
2738 self
2739 }
2740
2741 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2743 self.reasoning_effort = Some(effort.into());
2744 self
2745 }
2746
2747 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2749 self.reasoning_summary = Some(summary);
2750 self
2751 }
2752
2753 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2755 self.context_tier = Some(tier.into());
2756 self
2757 }
2758
2759 pub fn with_streaming(mut self, streaming: bool) -> Self {
2761 self.streaming = Some(streaming);
2762 self
2763 }
2764
2765 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2767 self.system_message = Some(system_message);
2768 self
2769 }
2770
2771 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2773 self.tools = Some(tools.into_iter().collect());
2774 self
2775 }
2776
2777 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2782 self.canvases = Some(canvases.into_iter().collect());
2783 self
2784 }
2785
2786 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2788 self.canvas_handler = Some(handler);
2789 self
2790 }
2791
2792 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2794 self.request_canvas_renderer = Some(request);
2795 self
2796 }
2797
2798 pub fn with_request_extensions(mut self, request: bool) -> Self {
2800 self.request_extensions = Some(request);
2801 self
2802 }
2803
2804 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2808 self.extension_sdk_path = Some(path.into());
2809 self
2810 }
2811
2812 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2814 self.extension_info = Some(extension_info);
2815 self
2816 }
2817
2818 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2821 self.canvas_provider = Some(canvas_provider);
2822 self
2823 }
2824
2825 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2827 where
2828 I: IntoIterator<Item = S>,
2829 S: Into<String>,
2830 {
2831 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2832 self
2833 }
2834
2835 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2837 where
2838 I: IntoIterator<Item = S>,
2839 S: Into<String>,
2840 {
2841 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2842 self
2843 }
2844
2845 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2847 where
2848 I: IntoIterator<Item = S>,
2849 S: Into<String>,
2850 {
2851 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2852 self
2853 }
2854
2855 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2857 self.mcp_servers = Some(servers);
2858 self
2859 }
2860
2861 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2869 self.mcp_oauth_token_storage = Some(mode.into());
2870 self
2871 }
2872
2873 pub fn with_embedding_cache_storage(
2875 mut self,
2876 embedding_cache_storage: impl Into<String>,
2877 ) -> Self {
2878 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2879 self
2880 }
2881
2882 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2885 self.enable_config_discovery = Some(enable);
2886 self
2887 }
2888
2889 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2891 self.skip_embedding_retrieval = Some(value);
2892 self
2893 }
2894
2895 pub fn with_organization_custom_instructions(
2897 mut self,
2898 instructions: impl Into<String>,
2899 ) -> Self {
2900 self.organization_custom_instructions = Some(instructions.into());
2901 self
2902 }
2903
2904 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2906 self.enable_on_demand_instruction_discovery = Some(value);
2907 self
2908 }
2909
2910 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2912 self.enable_file_hooks = Some(value);
2913 self
2914 }
2915
2916 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2918 self.enable_host_git_operations = Some(value);
2919 self
2920 }
2921
2922 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2924 self.enable_session_store = Some(value);
2925 self
2926 }
2927
2928 pub fn with_enable_skills(mut self, value: bool) -> Self {
2930 self.enable_skills = Some(value);
2931 self
2932 }
2933
2934 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2940 self.enable_mcp_apps = Some(enable);
2941 self
2942 }
2943
2944 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
2946 self.github_mcp_tool_config = Some(config);
2947 self
2948 }
2949
2950 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2952 where
2953 I: IntoIterator<Item = P>,
2954 P: Into<PathBuf>,
2955 {
2956 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2957 self
2958 }
2959
2960 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
2962 where
2963 I: IntoIterator<Item = S>,
2964 S: Into<String>,
2965 {
2966 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
2967 self
2968 }
2969
2970 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2974 where
2975 I: IntoIterator<Item = P>,
2976 P: Into<PathBuf>,
2977 {
2978 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2979 self
2980 }
2981
2982 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2984 where
2985 I: IntoIterator<Item = P>,
2986 P: Into<PathBuf>,
2987 {
2988 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2989 self
2990 }
2991
2992 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2994 self.large_output = Some(config);
2995 self
2996 }
2997
2998 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
3001 self.tool_search = Some(config);
3002 self
3003 }
3004
3005 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3007 where
3008 I: IntoIterator<Item = S>,
3009 S: Into<String>,
3010 {
3011 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3012 self
3013 }
3014
3015 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
3017 where
3018 I: IntoIterator<Item = S>,
3019 S: Into<String>,
3020 {
3021 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
3022 self
3023 }
3024
3025 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3027 mut self,
3028 agents: I,
3029 ) -> Self {
3030 self.custom_agents = Some(agents.into_iter().collect());
3031 self
3032 }
3033
3034 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3036 self.default_agent = Some(agent);
3037 self
3038 }
3039
3040 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3043 self.agent = Some(name.into());
3044 self
3045 }
3046
3047 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3050 self.infinite_sessions = Some(config);
3051 self
3052 }
3053
3054 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3056 self.provider = Some(provider);
3057 self
3058 }
3059
3060 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3062 self.capi = Some(capi);
3063 self
3064 }
3065
3066 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3072 self.providers = Some(providers);
3073 self
3074 }
3075
3076 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3082 self.models = Some(models);
3083 self
3084 }
3085
3086 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3090 self.enable_session_telemetry = Some(enable);
3091 self
3092 }
3093
3094 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3096 self.enable_citations = Some(enable);
3097 self
3098 }
3099
3100 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
3103 self.enable_file_change_tracking = Some(enable);
3104 self
3105 }
3106
3107 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3109 self.session_limits = Some(limits);
3110 self
3111 }
3112
3113 pub fn with_model_capabilities(
3115 mut self,
3116 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3117 ) -> Self {
3118 self.model_capabilities = Some(capabilities);
3119 self
3120 }
3121
3122 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3124 self.memory = Some(memory);
3125 self
3126 }
3127
3128 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3130 self.config_directory = Some(dir.into());
3131 self
3132 }
3133
3134 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3137 self.working_directory = Some(dir.into());
3138 self
3139 }
3140
3141 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
3143 where
3144 I: IntoIterator<Item = P>,
3145 P: Into<PathBuf>,
3146 {
3147 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
3148 self
3149 }
3150
3151 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3156 self.github_token = Some(token.into());
3157 self
3158 }
3159
3160 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3163 self.include_sub_agent_streaming_events = Some(include);
3164 self
3165 }
3166
3167 pub fn with_remote_session(
3169 mut self,
3170 mode: crate::generated::api_types::RemoteSessionMode,
3171 ) -> Self {
3172 self.remote_session = Some(mode);
3173 self
3174 }
3175
3176 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
3178 self.cloud = Some(cloud);
3179 self
3180 }
3181
3182 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3184 self.skip_custom_instructions = Some(value);
3185 self
3186 }
3187
3188 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3190 self.custom_agents_local_only = Some(value);
3191 self
3192 }
3193
3194 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
3196 self.enable_experimental_mode = Some(enable_experimental_mode);
3197 self
3198 }
3199
3200 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3202 self.coauthor_enabled = Some(value);
3203 self
3204 }
3205
3206 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3208 self.manage_schedule_enabled = Some(value);
3209 self
3210 }
3211
3212 #[doc(hidden)]
3220 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
3221 self.exp_assignments = Some(assignments);
3222 self
3223 }
3224
3225 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3231 self.enable_managed_settings = Some(enabled);
3232 self
3233 }
3234
3235 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
3240 self.managed_settings = Some(managed_settings);
3241 self
3242 }
3243}
3244#[derive(Clone)]
3251#[non_exhaustive]
3252pub struct ResumeSessionConfig {
3253 pub session_id: SessionId,
3255 pub model: Option<String>,
3258 pub client_name: Option<String>,
3260 pub reasoning_effort: Option<String>,
3262 pub reasoning_summary: Option<ReasoningSummary>,
3266 pub context_tier: Option<String>,
3269 pub streaming: Option<bool>,
3271 pub system_message: Option<SystemMessageConfig>,
3274 pub tools: Option<Vec<Tool>>,
3276 pub canvases: Option<Vec<CanvasDeclaration>>,
3278 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
3281 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3283 pub request_canvas_renderer: Option<bool>,
3285 pub request_extensions: Option<bool>,
3287 pub extension_sdk_path: Option<String>,
3291 pub extension_info: Option<ExtensionInfo>,
3293 pub canvas_provider: Option<CanvasProviderIdentity>,
3296 pub available_tools: Option<Vec<String>>,
3298 pub excluded_tools: Option<Vec<String>>,
3300 pub excluded_builtin_agents: Option<Vec<String>>,
3306 pub included_builtin_skills: Option<Vec<String>>,
3310 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3312 pub mcp_oauth_token_storage: Option<String>,
3315 pub enable_config_discovery: Option<bool>,
3318 pub skip_embedding_retrieval: Option<bool>,
3320 pub embedding_cache_storage: Option<String>,
3322 pub organization_custom_instructions: Option<String>,
3324 pub enable_on_demand_instruction_discovery: Option<bool>,
3326 pub enable_file_hooks: Option<bool>,
3328 pub enable_host_git_operations: Option<bool>,
3330 pub enable_session_store: Option<bool>,
3332 pub enable_skills: Option<bool>,
3334 pub enable_mcp_apps: Option<bool>,
3340 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
3345 pub skill_directories: Option<Vec<PathBuf>>,
3347 pub instruction_directories: Option<Vec<PathBuf>>,
3350 pub plugin_directories: Option<Vec<PathBuf>>,
3352 pub large_output: Option<LargeToolOutputConfig>,
3354 pub tool_search: Option<ToolSearchConfig>,
3357 pub disabled_skills: Option<Vec<String>>,
3359 pub disabled_mcp_servers: Option<Vec<String>>,
3362 pub hooks: Option<bool>,
3364 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3366 pub default_agent: Option<DefaultAgentConfig>,
3368 pub agent: Option<String>,
3370 pub infinite_sessions: Option<InfiniteSessionConfig>,
3372 pub provider: Option<ProviderConfig>,
3374 pub capi: Option<CapiSessionOptions>,
3380 pub providers: Option<Vec<NamedProviderConfig>>,
3386 pub models: Option<Vec<ProviderModelConfig>>,
3392 pub enable_session_telemetry: Option<bool>,
3400 pub enable_citations: Option<bool>,
3402 pub enable_file_change_tracking: Option<bool>,
3406 pub session_limits: Option<SessionLimitsConfig>,
3408 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3410 pub memory: Option<MemoryConfiguration>,
3412 pub config_directory: Option<PathBuf>,
3414 pub working_directory: Option<PathBuf>,
3416 pub additional_directories: Option<Vec<PathBuf>>,
3419 pub github_token: Option<String>,
3422 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3425 pub include_sub_agent_streaming_events: Option<bool>,
3427 pub commands: Option<Vec<CommandDefinition>>,
3431 #[doc(hidden)]
3436 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3437 pub enable_managed_settings: Option<bool>,
3443 pub managed_settings: Option<ManagedSettings>,
3449 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3454 pub suppress_resume_event: Option<bool>,
3457 pub continue_pending_work: Option<bool>,
3465 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3468 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3471 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3473 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3476 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3479 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3482 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3484 pub(crate) permission_policy: Option<crate::permission::Policy>,
3486 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3488 pub skip_custom_instructions: Option<bool>,
3490 pub custom_agents_local_only: Option<bool>,
3492 pub enable_experimental_mode: Option<bool>,
3497 pub coauthor_enabled: Option<bool>,
3499 pub manage_schedule_enabled: Option<bool>,
3501}
3502
3503impl std::fmt::Debug for ResumeSessionConfig {
3504 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3505 f.debug_struct("ResumeSessionConfig")
3506 .field("session_id", &self.session_id)
3507 .field("model", &self.model)
3508 .field("client_name", &self.client_name)
3509 .field("reasoning_effort", &self.reasoning_effort)
3510 .field("reasoning_summary", &self.reasoning_summary)
3511 .field("context_tier", &self.context_tier)
3512 .field("streaming", &self.streaming)
3513 .field("system_message", &self.system_message)
3514 .field("tools", &self.tools)
3515 .field("canvases", &self.canvases)
3516 .field(
3517 "canvas_handler",
3518 &self.canvas_handler.as_ref().map(|_| "<set>"),
3519 )
3520 .field("open_canvases", &self.open_canvases)
3521 .field("request_canvas_renderer", &self.request_canvas_renderer)
3522 .field("request_extensions", &self.request_extensions)
3523 .field("extension_sdk_path", &self.extension_sdk_path)
3524 .field("extension_info", &self.extension_info)
3525 .field("canvas_provider", &self.canvas_provider)
3526 .field("available_tools", &self.available_tools)
3527 .field("excluded_tools", &self.excluded_tools)
3528 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3529 .field("included_builtin_skills", &self.included_builtin_skills)
3530 .field("mcp_servers", &self.mcp_servers)
3531 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3532 .field("embedding_cache_storage", &self.embedding_cache_storage)
3533 .field("enable_config_discovery", &self.enable_config_discovery)
3534 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3535 .field(
3536 "organization_custom_instructions",
3537 &self
3538 .organization_custom_instructions
3539 .as_ref()
3540 .map(|_| "<redacted>"),
3541 )
3542 .field(
3543 "enable_on_demand_instruction_discovery",
3544 &self.enable_on_demand_instruction_discovery,
3545 )
3546 .field("enable_file_hooks", &self.enable_file_hooks)
3547 .field(
3548 "enable_host_git_operations",
3549 &self.enable_host_git_operations,
3550 )
3551 .field("enable_session_store", &self.enable_session_store)
3552 .field("enable_skills", &self.enable_skills)
3553 .field("enable_mcp_apps", &self.enable_mcp_apps)
3554 .field("skill_directories", &self.skill_directories)
3555 .field("instruction_directories", &self.instruction_directories)
3556 .field("plugin_directories", &self.plugin_directories)
3557 .field("large_output", &self.large_output)
3558 .field("tool_search", &self.tool_search)
3559 .field("disabled_skills", &self.disabled_skills)
3560 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
3561 .field("hooks", &self.hooks)
3562 .field("custom_agents", &self.custom_agents)
3563 .field("default_agent", &self.default_agent)
3564 .field("agent", &self.agent)
3565 .field("infinite_sessions", &self.infinite_sessions)
3566 .field("provider", &self.provider)
3567 .field("capi", &self.capi)
3568 .field("enable_session_telemetry", &self.enable_session_telemetry)
3569 .field("enable_citations", &self.enable_citations)
3570 .field(
3571 "enable_file_change_tracking",
3572 &self.enable_file_change_tracking,
3573 )
3574 .field("session_limits", &self.session_limits)
3575 .field("model_capabilities", &self.model_capabilities)
3576 .field("memory", &self.memory)
3577 .field("config_directory", &self.config_directory)
3578 .field("working_directory", &self.working_directory)
3579 .field("additional_directories", &self.additional_directories)
3580 .field(
3581 "github_token",
3582 &self.github_token.as_ref().map(|_| "<redacted>"),
3583 )
3584 .field("remote_session", &self.remote_session)
3585 .field(
3586 "include_sub_agent_streaming_events",
3587 &self.include_sub_agent_streaming_events,
3588 )
3589 .field("commands", &self.commands)
3590 .field("exp_assignments", &self.exp_assignments)
3591 .field("enable_managed_settings", &self.enable_managed_settings)
3592 .field("enable_experimental_mode", &self.enable_experimental_mode)
3593 .field("managed_settings", &self.managed_settings)
3594 .field(
3595 "session_fs_provider",
3596 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3597 )
3598 .field(
3599 "permission_handler",
3600 &self.permission_handler.as_ref().map(|_| "<set>"),
3601 )
3602 .field(
3603 "elicitation_handler",
3604 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3605 )
3606 .field(
3607 "user_input_handler",
3608 &self.user_input_handler.as_ref().map(|_| "<set>"),
3609 )
3610 .field(
3611 "exit_plan_mode_handler",
3612 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3613 )
3614 .field(
3615 "auto_mode_switch_handler",
3616 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3617 )
3618 .field(
3619 "hooks_handler",
3620 &self.hooks_handler.as_ref().map(|_| "<set>"),
3621 )
3622 .field(
3623 "system_message_transform",
3624 &self.system_message_transform.as_ref().map(|_| "<set>"),
3625 )
3626 .field("suppress_resume_event", &self.suppress_resume_event)
3627 .field("continue_pending_work", &self.continue_pending_work)
3628 .finish()
3629 }
3630}
3631
3632impl ResumeSessionConfig {
3633 pub(crate) fn into_wire(
3641 mut self,
3642 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3643 let permission_active =
3644 self.permission_handler.is_some() || self.permission_policy.is_some();
3645 let request_user_input = self.user_input_handler.is_some();
3646 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3647 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3648 let request_elicitation = self.elicitation_handler.is_some();
3649 let hooks_flag = self.hooks_handler.is_some();
3650
3651 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3652 if let Some(tools) = self.tools.as_mut() {
3653 for tool in tools.iter_mut() {
3654 if let Some(handler) = tool.handler.take()
3655 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3656 {
3657 return Err(crate::Error::with_message(
3658 crate::ErrorKind::InvalidConfig,
3659 format!("duplicate tool handler registered for name {:?}", tool.name),
3660 ));
3661 }
3662 }
3663 }
3664
3665 let wire_commands = self.commands.as_ref().map(|cmds| {
3666 cmds.iter()
3667 .map(|c| crate::wire::CommandWireDefinition {
3668 name: c.name.clone(),
3669 description: c.description.clone().unwrap_or_default(),
3670 })
3671 .collect()
3672 });
3673 let wire_canvases = self.canvases.clone();
3674 let canvas_handler = self.canvas_handler.clone();
3675 let bearer_token_providers =
3676 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3677
3678 let wire = crate::wire::SessionResumeWire {
3679 session_id: self.session_id,
3680 model: self.model,
3681 client_name: self.client_name,
3682 reasoning_effort: self.reasoning_effort,
3683 reasoning_summary: self.reasoning_summary,
3684 context_tier: self.context_tier,
3685 streaming: self.streaming,
3686 system_message: self.system_message,
3687 tools: self.tools,
3688 canvases: wire_canvases,
3689 open_canvases: self.open_canvases,
3690 request_canvas_renderer: self.request_canvas_renderer,
3691 request_extensions: self.request_extensions,
3692 extension_sdk_path: self.extension_sdk_path,
3693 extension_info: self.extension_info,
3694 canvas_provider: self.canvas_provider,
3695 available_tools: self.available_tools,
3696 excluded_tools: self.excluded_tools,
3697 excluded_builtin_agents: self.excluded_builtin_agents,
3698 tool_filter_precedence: "excluded",
3699 mcp_servers: self.mcp_servers,
3700 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3701 embedding_cache_storage: self.embedding_cache_storage,
3702 env_value_mode: "direct",
3703 enable_config_discovery: self.enable_config_discovery,
3704 skip_embedding_retrieval: self.skip_embedding_retrieval,
3705 organization_custom_instructions: self.organization_custom_instructions,
3706 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3707 enable_file_hooks: self.enable_file_hooks,
3708 enable_host_git_operations: self.enable_host_git_operations,
3709 enable_session_store: self.enable_session_store,
3710 enable_skills: self.enable_skills,
3711 request_user_input,
3712 request_permission: permission_active,
3713 request_exit_plan_mode,
3714 request_auto_mode_switch,
3715 request_elicitation,
3716 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3717 github_mcp_tool_config: self.github_mcp_tool_config,
3718 hooks: hooks_flag,
3719 skill_directories: self.skill_directories,
3720 instruction_directories: self.instruction_directories,
3721 plugin_directories: self.plugin_directories,
3722 large_output: self.large_output,
3723 tool_search: self.tool_search,
3724 disabled_skills: self.disabled_skills,
3725 disabled_mcp_servers: self.disabled_mcp_servers,
3726 custom_agents: self.custom_agents,
3727 custom_agents_local_only: self.custom_agents_local_only,
3728 default_agent: self.default_agent,
3729 agent: self.agent,
3730 infinite_sessions: self.infinite_sessions,
3731 provider: self.provider,
3732 capi: self.capi,
3733 providers: self.providers,
3734 models: self.models,
3735 enable_session_telemetry: self.enable_session_telemetry,
3736 enable_citations: self.enable_citations,
3737 enable_file_change_tracking: self.enable_file_change_tracking,
3738 session_limits: self.session_limits,
3739 model_capabilities: self.model_capabilities,
3740 memory: self.memory,
3741 config_dir: self.config_directory,
3742 working_directory: self.working_directory,
3743 additional_directories: self.additional_directories,
3744 github_token: self.github_token,
3745 remote_session: self.remote_session,
3746 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3747 enable_github_telemetry_forwarding: None,
3748 commands: wire_commands,
3749 exp_assignments: self.exp_assignments,
3750 enable_managed_settings: self.enable_managed_settings,
3751 is_experimental_mode: self.enable_experimental_mode,
3752 managed_settings: self.managed_settings,
3753 suppress_resume_event: self.suppress_resume_event,
3754 continue_pending_work: self.continue_pending_work,
3755 };
3756
3757 let runtime = SessionConfigRuntime {
3758 permission_handler: self.permission_handler,
3759 permission_policy: self.permission_policy,
3760 elicitation_handler: self.elicitation_handler,
3761 mcp_auth_handler: self.mcp_auth_handler,
3762 user_input_handler: self.user_input_handler,
3763 exit_plan_mode_handler: self.exit_plan_mode_handler,
3764 auto_mode_switch_handler: self.auto_mode_switch_handler,
3765 hooks_handler: self.hooks_handler,
3766 system_message_transform: self.system_message_transform,
3767 tool_handlers,
3768 canvas_handler,
3769 session_fs_provider: self.session_fs_provider,
3770 bearer_token_providers,
3771 commands: self.commands,
3772 };
3773
3774 Ok((wire, runtime))
3775 }
3776
3777 pub fn new(session_id: SessionId) -> Self {
3782 Self {
3783 session_id,
3784 model: None,
3785 client_name: None,
3786 reasoning_effort: None,
3787 reasoning_summary: None,
3788 context_tier: None,
3789 streaming: None,
3790 system_message: None,
3791 tools: None,
3792 canvases: None,
3793 canvas_handler: None,
3794 open_canvases: None,
3795 request_canvas_renderer: None,
3796 request_extensions: None,
3797 extension_sdk_path: None,
3798 extension_info: None,
3799 canvas_provider: None,
3800 available_tools: None,
3801 excluded_tools: None,
3802 excluded_builtin_agents: None,
3803 included_builtin_skills: None,
3804 mcp_servers: None,
3805 mcp_oauth_token_storage: None,
3806 enable_config_discovery: None,
3807 skip_embedding_retrieval: None,
3808 organization_custom_instructions: None,
3809 enable_on_demand_instruction_discovery: None,
3810 enable_file_hooks: None,
3811 enable_host_git_operations: None,
3812 enable_session_store: None,
3813 enable_skills: None,
3814 embedding_cache_storage: None,
3815 enable_mcp_apps: None,
3816 github_mcp_tool_config: None,
3817 skill_directories: None,
3818 instruction_directories: None,
3819 plugin_directories: None,
3820 large_output: None,
3821 tool_search: None,
3822 disabled_skills: None,
3823 disabled_mcp_servers: None,
3824 hooks: None,
3825 custom_agents: None,
3826 default_agent: None,
3827 agent: None,
3828 infinite_sessions: None,
3829 provider: None,
3830 capi: None,
3831 providers: None,
3832 models: None,
3833 enable_session_telemetry: None,
3834 enable_citations: None,
3835 enable_file_change_tracking: None,
3836 session_limits: None,
3837 model_capabilities: None,
3838 memory: None,
3839 config_directory: None,
3840 working_directory: None,
3841 additional_directories: None,
3842 github_token: None,
3843 remote_session: None,
3844 include_sub_agent_streaming_events: None,
3845 commands: None,
3846 exp_assignments: None,
3847 enable_managed_settings: None,
3848 managed_settings: None,
3849 session_fs_provider: None,
3850 suppress_resume_event: None,
3851 continue_pending_work: None,
3852 permission_handler: None,
3853 elicitation_handler: None,
3854 mcp_auth_handler: None,
3855 user_input_handler: None,
3856 exit_plan_mode_handler: None,
3857 auto_mode_switch_handler: None,
3858 hooks_handler: None,
3859 permission_policy: None,
3860 system_message_transform: None,
3861 skip_custom_instructions: None,
3862 custom_agents_local_only: None,
3863 enable_experimental_mode: None,
3864 coauthor_enabled: None,
3865 manage_schedule_enabled: None,
3866 }
3867 }
3868
3869 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3871 self.permission_handler = Some(handler);
3872 self
3873 }
3874
3875 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3877 self.elicitation_handler = Some(handler);
3878 self
3879 }
3880
3881 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3883 self.mcp_auth_handler = Some(handler);
3884 self
3885 }
3886
3887 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3889 self.user_input_handler = Some(handler);
3890 self
3891 }
3892
3893 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3895 self.exit_plan_mode_handler = Some(handler);
3896 self
3897 }
3898
3899 pub fn with_auto_mode_switch_handler(
3901 mut self,
3902 handler: Arc<dyn AutoModeSwitchHandler>,
3903 ) -> Self {
3904 self.auto_mode_switch_handler = Some(handler);
3905 self
3906 }
3907
3908 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3911 self.hooks_handler = Some(hooks);
3912 self
3913 }
3914
3915 pub fn with_system_message_transform(
3917 mut self,
3918 transform: Arc<dyn SystemMessageTransform>,
3919 ) -> Self {
3920 self.system_message_transform = Some(transform);
3921 self
3922 }
3923
3924 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3928 self.commands = Some(commands);
3929 self
3930 }
3931
3932 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3935 self.session_fs_provider = Some(provider);
3936 self
3937 }
3938
3939 pub fn approve_all_permissions(mut self) -> Self {
3942 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3943 self
3944 }
3945
3946 pub fn deny_all_permissions(mut self) -> Self {
3949 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3950 self
3951 }
3952
3953 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3956 where
3957 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3958 {
3959 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3960 self
3961 }
3962
3963 pub fn with_model(mut self, model: impl Into<String>) -> Self {
3965 self.model = Some(model.into());
3966 self
3967 }
3968
3969 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3971 self.client_name = Some(name.into());
3972 self
3973 }
3974
3975 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3977 self.reasoning_effort = Some(effort.into());
3978 self
3979 }
3980
3981 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3983 self.reasoning_summary = Some(summary);
3984 self
3985 }
3986
3987 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3990 self.context_tier = Some(tier.into());
3991 self
3992 }
3993
3994 pub fn with_streaming(mut self, streaming: bool) -> Self {
3996 self.streaming = Some(streaming);
3997 self
3998 }
3999
4000 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
4003 self.system_message = Some(system_message);
4004 self
4005 }
4006
4007 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
4009 self.tools = Some(tools.into_iter().collect());
4010 self
4011 }
4012
4013 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
4015 self.canvases = Some(canvases.into_iter().collect());
4016 self
4017 }
4018
4019 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
4021 self.canvas_handler = Some(handler);
4022 self
4023 }
4024
4025 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
4027 mut self,
4028 open_canvases: I,
4029 ) -> Self {
4030 self.open_canvases = Some(open_canvases.into_iter().collect());
4031 self
4032 }
4033
4034 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
4036 self.request_canvas_renderer = Some(request);
4037 self
4038 }
4039
4040 pub fn with_request_extensions(mut self, request: bool) -> Self {
4042 self.request_extensions = Some(request);
4043 self
4044 }
4045
4046 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
4050 self.extension_sdk_path = Some(path.into());
4051 self
4052 }
4053
4054 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
4056 self.extension_info = Some(extension_info);
4057 self
4058 }
4059
4060 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
4063 self.canvas_provider = Some(canvas_provider);
4064 self
4065 }
4066
4067 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
4069 where
4070 I: IntoIterator<Item = S>,
4071 S: Into<String>,
4072 {
4073 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
4074 self
4075 }
4076
4077 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
4079 where
4080 I: IntoIterator<Item = S>,
4081 S: Into<String>,
4082 {
4083 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
4084 self
4085 }
4086
4087 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
4089 where
4090 I: IntoIterator<Item = S>,
4091 S: Into<String>,
4092 {
4093 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
4094 self
4095 }
4096
4097 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
4099 self.mcp_servers = Some(servers);
4100 self
4101 }
4102
4103 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
4106 self.mcp_oauth_token_storage = Some(mode.into());
4107 self
4108 }
4109
4110 pub fn with_embedding_cache_storage(
4112 mut self,
4113 embedding_cache_storage: impl Into<String>,
4114 ) -> Self {
4115 self.embedding_cache_storage = Some(embedding_cache_storage.into());
4116 self
4117 }
4118
4119 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
4122 self.enable_config_discovery = Some(enable);
4123 self
4124 }
4125
4126 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
4128 self.skip_embedding_retrieval = Some(value);
4129 self
4130 }
4131
4132 pub fn with_organization_custom_instructions(
4134 mut self,
4135 instructions: impl Into<String>,
4136 ) -> Self {
4137 self.organization_custom_instructions = Some(instructions.into());
4138 self
4139 }
4140
4141 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
4143 self.enable_on_demand_instruction_discovery = Some(value);
4144 self
4145 }
4146
4147 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
4149 self.enable_file_hooks = Some(value);
4150 self
4151 }
4152
4153 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
4155 self.enable_host_git_operations = Some(value);
4156 self
4157 }
4158
4159 pub fn with_enable_session_store(mut self, value: bool) -> Self {
4161 self.enable_session_store = Some(value);
4162 self
4163 }
4164
4165 pub fn with_enable_skills(mut self, value: bool) -> Self {
4167 self.enable_skills = Some(value);
4168 self
4169 }
4170
4171 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
4177 self.enable_mcp_apps = Some(enable);
4178 self
4179 }
4180
4181 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
4183 self.github_mcp_tool_config = Some(config);
4184 self
4185 }
4186
4187 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
4189 where
4190 I: IntoIterator<Item = P>,
4191 P: Into<PathBuf>,
4192 {
4193 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
4194 self
4195 }
4196
4197 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
4199 where
4200 I: IntoIterator<Item = S>,
4201 S: Into<String>,
4202 {
4203 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
4204 self
4205 }
4206
4207 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
4211 where
4212 I: IntoIterator<Item = P>,
4213 P: Into<PathBuf>,
4214 {
4215 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
4216 self
4217 }
4218
4219 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
4221 where
4222 I: IntoIterator<Item = P>,
4223 P: Into<PathBuf>,
4224 {
4225 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
4226 self
4227 }
4228
4229 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
4231 self.large_output = Some(config);
4232 self
4233 }
4234
4235 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
4238 self.tool_search = Some(config);
4239 self
4240 }
4241
4242 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
4244 where
4245 I: IntoIterator<Item = S>,
4246 S: Into<String>,
4247 {
4248 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
4249 self
4250 }
4251
4252 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
4254 where
4255 I: IntoIterator<Item = S>,
4256 S: Into<String>,
4257 {
4258 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
4259 self
4260 }
4261
4262 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
4264 mut self,
4265 agents: I,
4266 ) -> Self {
4267 self.custom_agents = Some(agents.into_iter().collect());
4268 self
4269 }
4270
4271 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
4273 self.default_agent = Some(agent);
4274 self
4275 }
4276
4277 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
4279 self.agent = Some(name.into());
4280 self
4281 }
4282
4283 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
4285 self.infinite_sessions = Some(config);
4286 self
4287 }
4288
4289 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
4291 self.provider = Some(provider);
4292 self
4293 }
4294
4295 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
4297 self.capi = Some(capi);
4298 self
4299 }
4300
4301 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
4307 self.providers = Some(providers);
4308 self
4309 }
4310
4311 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
4317 self.models = Some(models);
4318 self
4319 }
4320
4321 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
4325 self.enable_session_telemetry = Some(enable);
4326 self
4327 }
4328
4329 pub fn with_enable_citations(mut self, enable: bool) -> Self {
4331 self.enable_citations = Some(enable);
4332 self
4333 }
4334
4335 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
4338 self.enable_file_change_tracking = Some(enable);
4339 self
4340 }
4341
4342 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
4344 self.session_limits = Some(limits);
4345 self
4346 }
4347
4348 pub fn with_model_capabilities(
4350 mut self,
4351 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
4352 ) -> Self {
4353 self.model_capabilities = Some(capabilities);
4354 self
4355 }
4356
4357 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
4359 self.memory = Some(memory);
4360 self
4361 }
4362
4363 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4365 self.config_directory = Some(dir.into());
4366 self
4367 }
4368
4369 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4371 self.working_directory = Some(dir.into());
4372 self
4373 }
4374
4375 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
4377 where
4378 I: IntoIterator<Item = P>,
4379 P: Into<PathBuf>,
4380 {
4381 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
4382 self
4383 }
4384
4385 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
4389 self.github_token = Some(token.into());
4390 self
4391 }
4392
4393 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4395 self.include_sub_agent_streaming_events = Some(include);
4396 self
4397 }
4398
4399 pub fn with_remote_session(
4401 mut self,
4402 mode: crate::generated::api_types::RemoteSessionMode,
4403 ) -> Self {
4404 self.remote_session = Some(mode);
4405 self
4406 }
4407
4408 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4411 self.suppress_resume_event = Some(suppress);
4412 self
4413 }
4414
4415 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4421 self.continue_pending_work = Some(continue_pending);
4422 self
4423 }
4424
4425 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4427 self.skip_custom_instructions = Some(value);
4428 self
4429 }
4430
4431 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4433 self.custom_agents_local_only = Some(value);
4434 self
4435 }
4436
4437 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
4439 self.enable_experimental_mode = Some(enable_experimental_mode);
4440 self
4441 }
4442
4443 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4445 self.coauthor_enabled = Some(value);
4446 self
4447 }
4448
4449 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4451 self.manage_schedule_enabled = Some(value);
4452 self
4453 }
4454
4455 #[doc(hidden)]
4459 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4460 self.exp_assignments = Some(assignments);
4461 self
4462 }
4463
4464 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4467 self.enable_managed_settings = Some(enabled);
4468 self
4469 }
4470
4471 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
4475 self.managed_settings = Some(managed_settings);
4476 self
4477 }
4478}
4479
4480#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4486#[serde(rename_all = "camelCase")]
4487#[non_exhaustive]
4488pub struct SystemMessageConfig {
4489 #[serde(skip_serializing_if = "Option::is_none")]
4491 pub mode: Option<String>,
4492 #[serde(skip_serializing_if = "Option::is_none")]
4494 pub content: Option<String>,
4495 #[serde(skip_serializing_if = "Option::is_none")]
4497 pub sections: Option<HashMap<String, SectionOverride>>,
4498}
4499
4500impl SystemMessageConfig {
4501 pub fn new() -> Self {
4504 Self::default()
4505 }
4506
4507 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4510 self.mode = Some(mode.into());
4511 self
4512 }
4513
4514 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4517 self.content = Some(content.into());
4518 self
4519 }
4520
4521 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4523 self.sections = Some(sections);
4524 self
4525 }
4526}
4527
4528#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4534#[serde(rename_all = "camelCase")]
4535pub struct SectionOverride {
4536 #[serde(skip_serializing_if = "Option::is_none")]
4539 pub action: Option<String>,
4540 #[serde(skip_serializing_if = "Option::is_none")]
4542 pub content: Option<String>,
4543}
4544
4545#[derive(Debug, Clone, Serialize, Deserialize)]
4547#[serde(rename_all = "camelCase")]
4548pub struct CreateSessionResult {
4549 pub session_id: SessionId,
4551 #[serde(skip_serializing_if = "Option::is_none")]
4553 pub workspace_path: Option<PathBuf>,
4554 #[serde(default, alias = "remote_url")]
4556 pub remote_url: Option<String>,
4557 #[serde(skip_serializing_if = "Option::is_none")]
4559 pub capabilities: Option<SessionCapabilities>,
4560}
4561
4562#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4564#[serde(rename_all = "camelCase")]
4565pub(crate) struct ResumeSessionResult {
4566 #[serde(default)]
4568 pub session_id: Option<SessionId>,
4569 #[serde(default, skip_serializing_if = "Option::is_none")]
4571 pub workspace_path: Option<PathBuf>,
4572 #[serde(default, alias = "remote_url")]
4574 pub remote_url: Option<String>,
4575 #[serde(default, skip_serializing_if = "Option::is_none")]
4577 pub capabilities: Option<SessionCapabilities>,
4578 #[serde(
4580 default,
4581 alias = "openCanvasInstances",
4582 skip_serializing_if = "Option::is_none"
4583 )]
4584 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4585}
4586
4587#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4589#[serde(rename_all = "lowercase")]
4590pub enum LogLevel {
4591 #[default]
4593 Info,
4594 Warning,
4596 Error,
4598}
4599
4600#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4605#[serde(rename_all = "camelCase")]
4606pub struct LogOptions {
4607 #[serde(skip_serializing_if = "Option::is_none")]
4609 pub level: Option<LogLevel>,
4610 #[serde(skip_serializing_if = "Option::is_none")]
4613 pub ephemeral: Option<bool>,
4614}
4615
4616impl LogOptions {
4617 pub fn with_level(mut self, level: LogLevel) -> Self {
4619 self.level = Some(level);
4620 self
4621 }
4622
4623 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4625 self.ephemeral = Some(ephemeral);
4626 self
4627 }
4628}
4629
4630#[derive(Debug, Clone, Default)]
4634pub struct SetModelOptions {
4635 pub reasoning_effort: Option<String>,
4638 pub reasoning_summary: Option<ReasoningSummary>,
4642 pub context_tier: Option<ContextTier>,
4645 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4649}
4650
4651impl SetModelOptions {
4652 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4654 self.reasoning_effort = Some(effort.into());
4655 self
4656 }
4657
4658 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4660 self.reasoning_summary = Some(summary);
4661 self
4662 }
4663
4664 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4666 self.context_tier = Some(tier);
4667 self
4668 }
4669
4670 pub fn with_model_capabilities(
4672 mut self,
4673 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4674 ) -> Self {
4675 self.model_capabilities = Some(caps);
4676 self
4677 }
4678}
4679
4680#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4687#[serde(rename_all = "camelCase")]
4688pub struct PingResponse {
4689 #[serde(default)]
4691 pub message: String,
4692 #[serde(default)]
4694 pub timestamp: String,
4695 #[serde(skip_serializing_if = "Option::is_none")]
4697 pub protocol_version: Option<u32>,
4698}
4699
4700#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4702#[serde(rename_all = "camelCase")]
4703pub struct AttachmentLineRange {
4704 pub start: u32,
4706 pub end: u32,
4708}
4709
4710#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4712#[serde(rename_all = "camelCase")]
4713pub struct AttachmentSelectionPosition {
4714 pub line: u32,
4716 pub character: u32,
4718}
4719
4720#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4722#[serde(rename_all = "camelCase")]
4723pub struct AttachmentSelectionRange {
4724 pub start: AttachmentSelectionPosition,
4726 pub end: AttachmentSelectionPosition,
4728}
4729
4730#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4732#[serde(rename_all = "snake_case")]
4733#[non_exhaustive]
4734pub enum GitHubReferenceType {
4735 Issue,
4737 Pr,
4739 Discussion,
4741}
4742
4743#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4749#[serde(rename_all = "camelCase")]
4750pub struct GitHubRepoPointer {
4751 #[serde(skip_serializing_if = "Option::is_none")]
4753 pub id: Option<i64>,
4754 pub name: String,
4756 pub owner: String,
4758}
4759
4760#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4762#[serde(rename_all = "camelCase")]
4763pub struct GitHubFileDiffSide {
4764 pub path: String,
4766 pub r#ref: String,
4768 pub repo: GitHubRepoPointer,
4770}
4771
4772#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4774#[serde(rename_all = "camelCase")]
4775pub struct GitHubTreeComparisonSide {
4776 pub repo: GitHubRepoPointer,
4778 pub revision: String,
4780}
4781
4782#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4784#[serde(rename_all = "camelCase")]
4785pub struct GitHubSnippetLineRange {
4786 pub start: i64,
4788 pub end: i64,
4790}
4791
4792#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4794#[serde(
4795 tag = "type",
4796 rename_all = "camelCase",
4797 rename_all_fields = "camelCase"
4798)]
4799#[non_exhaustive]
4800pub enum Attachment {
4801 File {
4803 path: PathBuf,
4805 #[serde(skip_serializing_if = "Option::is_none")]
4807 display_name: Option<String>,
4808 #[serde(skip_serializing_if = "Option::is_none")]
4810 line_range: Option<AttachmentLineRange>,
4811 },
4812 Directory {
4814 path: PathBuf,
4816 #[serde(skip_serializing_if = "Option::is_none")]
4818 display_name: Option<String>,
4819 },
4820 Selection {
4822 file_path: PathBuf,
4824 text: String,
4826 #[serde(skip_serializing_if = "Option::is_none")]
4828 display_name: Option<String>,
4829 selection: AttachmentSelectionRange,
4831 },
4832 Blob {
4834 data: String,
4836 mime_type: String,
4838 #[serde(skip_serializing_if = "Option::is_none")]
4840 display_name: Option<String>,
4841 },
4842 #[serde(rename = "github_reference")]
4844 GitHubReference {
4845 number: u64,
4847 title: String,
4849 reference_type: GitHubReferenceType,
4851 state: String,
4853 url: String,
4855 },
4856 #[serde(rename = "github_commit")]
4858 GitHubCommit {
4859 message: String,
4861 oid: String,
4863 repo: GitHubRepoPointer,
4865 url: String,
4867 },
4868 #[serde(rename = "github_release")]
4870 GitHubRelease {
4871 name: String,
4873 repo: GitHubRepoPointer,
4875 tag_name: String,
4877 url: String,
4879 },
4880 #[serde(rename = "github_actions_job")]
4882 GitHubActionsJob {
4883 #[serde(skip_serializing_if = "Option::is_none")]
4886 conclusion: Option<String>,
4887 job_id: i64,
4889 job_name: String,
4891 repo: GitHubRepoPointer,
4893 url: String,
4895 workflow_name: String,
4897 },
4898 #[serde(rename = "github_repository")]
4900 GitHubRepository {
4901 #[serde(skip_serializing_if = "Option::is_none")]
4903 description: Option<String>,
4904 #[serde(skip_serializing_if = "Option::is_none")]
4907 r#ref: Option<String>,
4908 repo: GitHubRepoPointer,
4910 url: String,
4912 },
4913 #[serde(rename = "github_file_diff")]
4915 GitHubFileDiff {
4916 #[serde(skip_serializing_if = "Option::is_none")]
4918 base: Option<GitHubFileDiffSide>,
4919 #[serde(skip_serializing_if = "Option::is_none")]
4921 head: Option<GitHubFileDiffSide>,
4922 url: String,
4924 },
4925 #[serde(rename = "github_tree_comparison")]
4927 GitHubTreeComparison {
4928 base: GitHubTreeComparisonSide,
4930 head: GitHubTreeComparisonSide,
4932 url: String,
4934 },
4935 #[serde(rename = "github_url")]
4937 GitHubUrl {
4938 url: String,
4940 },
4941 #[serde(rename = "github_file")]
4943 GitHubFile {
4944 path: String,
4946 r#ref: String,
4948 repo: GitHubRepoPointer,
4950 url: String,
4952 },
4953 #[serde(rename = "github_snippet")]
4955 GitHubSnippet {
4956 line_range: GitHubSnippetLineRange,
4958 path: String,
4960 r#ref: String,
4962 repo: GitHubRepoPointer,
4964 url: String,
4966 },
4967}
4968
4969impl Attachment {
4970 pub fn display_name(&self) -> Option<&str> {
4972 match self {
4973 Self::File { display_name, .. }
4974 | Self::Directory { display_name, .. }
4975 | Self::Selection { display_name, .. }
4976 | Self::Blob { display_name, .. } => display_name.as_deref(),
4977 Self::GitHubReference { .. }
4978 | Self::GitHubCommit { .. }
4979 | Self::GitHubRelease { .. }
4980 | Self::GitHubActionsJob { .. }
4981 | Self::GitHubRepository { .. }
4982 | Self::GitHubFileDiff { .. }
4983 | Self::GitHubTreeComparison { .. }
4984 | Self::GitHubUrl { .. }
4985 | Self::GitHubFile { .. }
4986 | Self::GitHubSnippet { .. } => None,
4987 }
4988 }
4989
4990 pub fn label(&self) -> Option<String> {
4992 if let Some(display_name) = self
4993 .display_name()
4994 .map(str::trim)
4995 .filter(|name| !name.is_empty())
4996 {
4997 return Some(display_name.to_string());
4998 }
4999
5000 match self {
5001 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
5002 format!("#{}", number)
5003 } else {
5004 title.trim().to_string()
5005 }),
5006 _ => self.derived_display_name(),
5007 }
5008 }
5009
5010 pub fn ensure_display_name(&mut self) {
5012 if self
5013 .display_name()
5014 .map(str::trim)
5015 .is_some_and(|name| !name.is_empty())
5016 {
5017 return;
5018 }
5019
5020 let Some(derived_display_name) = self.derived_display_name() else {
5021 return;
5022 };
5023
5024 match self {
5025 Self::File { display_name, .. }
5026 | Self::Directory { display_name, .. }
5027 | Self::Selection { display_name, .. }
5028 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
5029 Self::GitHubReference { .. }
5030 | Self::GitHubCommit { .. }
5031 | Self::GitHubRelease { .. }
5032 | Self::GitHubActionsJob { .. }
5033 | Self::GitHubRepository { .. }
5034 | Self::GitHubFileDiff { .. }
5035 | Self::GitHubTreeComparison { .. }
5036 | Self::GitHubUrl { .. }
5037 | Self::GitHubFile { .. }
5038 | Self::GitHubSnippet { .. } => {}
5039 }
5040 }
5041
5042 fn derived_display_name(&self) -> Option<String> {
5043 match self {
5044 Self::File { path, .. } | Self::Directory { path, .. } => {
5045 Some(attachment_name_from_path(path))
5046 }
5047 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
5048 Self::Blob { .. } => Some("attachment".to_string()),
5049 Self::GitHubReference { .. }
5050 | Self::GitHubCommit { .. }
5051 | Self::GitHubRelease { .. }
5052 | Self::GitHubActionsJob { .. }
5053 | Self::GitHubRepository { .. }
5054 | Self::GitHubFileDiff { .. }
5055 | Self::GitHubTreeComparison { .. }
5056 | Self::GitHubUrl { .. }
5057 | Self::GitHubFile { .. }
5058 | Self::GitHubSnippet { .. } => None,
5059 }
5060 }
5061}
5062
5063fn attachment_name_from_path(path: &Path) -> String {
5064 path.file_name()
5065 .map(|name| name.to_string_lossy().into_owned())
5066 .filter(|name| !name.is_empty())
5067 .unwrap_or_else(|| {
5068 let full = path.to_string_lossy();
5069 if full.is_empty() {
5070 "attachment".to_string()
5071 } else {
5072 full.into_owned()
5073 }
5074 })
5075}
5076
5077pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
5079 for attachment in attachments {
5080 attachment.ensure_display_name();
5081 }
5082}
5083
5084#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5089#[serde(rename_all = "lowercase")]
5090#[non_exhaustive]
5091pub enum DeliveryMode {
5092 Enqueue,
5094 Immediate,
5096}
5097
5098#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5103#[serde(rename_all = "lowercase")]
5104#[non_exhaustive]
5105pub enum AgentMode {
5106 Interactive,
5108 Plan,
5110 Autopilot,
5112 Shell,
5114}
5115
5116#[derive(Debug, Clone)]
5145#[non_exhaustive]
5146pub struct MessageOptions {
5147 pub prompt: String,
5149 pub mode: Option<DeliveryMode>,
5155 pub agent_mode: Option<AgentMode>,
5159 pub attachments: Option<Vec<Attachment>>,
5161 pub wait_timeout: Option<Duration>,
5164 pub request_headers: Option<HashMap<String, String>>,
5168 pub traceparent: Option<String>,
5175 pub tracestate: Option<String>,
5179 pub display_prompt: Option<String>,
5181}
5182
5183impl MessageOptions {
5184 pub fn new(prompt: impl Into<String>) -> Self {
5186 Self {
5187 prompt: prompt.into(),
5188 mode: None,
5189 agent_mode: None,
5190 attachments: None,
5191 wait_timeout: None,
5192 request_headers: None,
5193 traceparent: None,
5194 tracestate: None,
5195 display_prompt: None,
5196 }
5197 }
5198
5199 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
5205 self.mode = Some(mode);
5206 self
5207 }
5208
5209 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
5213 self.agent_mode = Some(agent_mode);
5214 self
5215 }
5216
5217 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
5219 self.attachments = Some(attachments);
5220 self
5221 }
5222
5223 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
5225 self.wait_timeout = Some(timeout);
5226 self
5227 }
5228
5229 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
5231 self.request_headers = Some(headers);
5232 self
5233 }
5234
5235 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
5240 self.traceparent = ctx.traceparent;
5241 self.tracestate = ctx.tracestate;
5242 self
5243 }
5244
5245 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5247 self.traceparent = Some(traceparent.into());
5248 self
5249 }
5250
5251 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5253 self.tracestate = Some(tracestate.into());
5254 self
5255 }
5256
5257 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5259 self.display_prompt = Some(display_prompt.into());
5260 self
5261 }
5262}
5263
5264impl From<&str> for MessageOptions {
5265 fn from(prompt: &str) -> Self {
5266 Self::new(prompt)
5267 }
5268}
5269
5270impl From<String> for MessageOptions {
5271 fn from(prompt: String) -> Self {
5272 Self::new(prompt)
5273 }
5274}
5275
5276impl From<&String> for MessageOptions {
5277 fn from(prompt: &String) -> Self {
5278 Self::new(prompt.clone())
5279 }
5280}
5281
5282#[derive(Debug, Clone, Serialize, Deserialize)]
5284#[serde(rename_all = "camelCase")]
5285#[non_exhaustive]
5286pub struct GetStatusResponse {
5287 pub version: String,
5289 pub protocol_version: u32,
5291}
5292
5293#[derive(Debug, Clone, Serialize, Deserialize)]
5295#[serde(rename_all = "camelCase")]
5296#[non_exhaustive]
5297pub struct GetAuthStatusResponse {
5298 pub is_authenticated: bool,
5300 #[serde(skip_serializing_if = "Option::is_none")]
5303 pub auth_type: Option<String>,
5304 #[serde(skip_serializing_if = "Option::is_none")]
5306 pub host: Option<String>,
5307 #[serde(skip_serializing_if = "Option::is_none")]
5309 pub login: Option<String>,
5310 #[serde(skip_serializing_if = "Option::is_none")]
5312 pub status_message: Option<String>,
5313}
5314
5315#[derive(Debug, Clone, Serialize, Deserialize)]
5319#[serde(rename_all = "camelCase")]
5320pub struct SessionEventNotification {
5321 pub session_id: SessionId,
5323 pub event: SessionEvent,
5325}
5326
5327#[derive(Debug, Clone, Serialize, Deserialize)]
5334#[serde(rename_all = "camelCase")]
5335pub struct SessionEvent {
5336 pub id: String,
5338 pub timestamp: String,
5340 pub parent_id: Option<String>,
5342 #[serde(skip_serializing_if = "Option::is_none")]
5344 pub ephemeral: Option<bool>,
5345 #[serde(skip_serializing_if = "Option::is_none")]
5348 pub agent_id: Option<String>,
5349 #[serde(skip_serializing_if = "Option::is_none")]
5351 pub debug_cli_received_at_ms: Option<i64>,
5352 #[serde(skip_serializing_if = "Option::is_none")]
5354 pub debug_ws_forwarded_at_ms: Option<i64>,
5355 #[serde(rename = "type")]
5357 pub event_type: String,
5358 pub data: Value,
5360}
5361
5362impl SessionEvent {
5363 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5368 use serde::de::IntoDeserializer;
5369 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5370 self.event_type.as_str().into_deserializer();
5371 crate::generated::SessionEventType::deserialize(deserializer)
5372 .unwrap_or(crate::generated::SessionEventType::Unknown)
5373 }
5374
5375 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5381 serde_json::from_value(self.data.clone()).ok()
5382 }
5383
5384 pub fn is_transient_error(&self) -> bool {
5388 self.event_type == "session.error"
5389 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5390 }
5391}
5392
5393#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5398#[serde(rename_all = "camelCase")]
5399#[non_exhaustive]
5400pub struct ToolInvocation {
5401 pub session_id: SessionId,
5403 pub tool_call_id: String,
5405 pub tool_name: String,
5407 pub arguments: Value,
5409 #[serde(skip)]
5417 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5418 #[serde(default, skip_serializing_if = "Option::is_none")]
5423 pub traceparent: Option<String>,
5424 #[serde(default, skip_serializing_if = "Option::is_none")]
5427 pub tracestate: Option<String>,
5428}
5429
5430impl ToolInvocation {
5431 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5452 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5453 }
5454
5455 pub fn trace_context(&self) -> TraceContext {
5458 TraceContext {
5459 traceparent: self.traceparent.clone(),
5460 tracestate: self.tracestate.clone(),
5461 }
5462 }
5463}
5464
5465#[derive(Debug, Clone, Serialize, Deserialize)]
5467#[serde(rename_all = "camelCase")]
5468pub struct ToolBinaryResult {
5469 pub data: String,
5471 pub mime_type: String,
5473 pub r#type: String,
5475 #[serde(default, skip_serializing_if = "Option::is_none")]
5477 pub description: Option<String>,
5478}
5479
5480#[derive(Debug, Clone, Serialize, Deserialize)]
5487#[serde(rename_all = "camelCase")]
5488#[non_exhaustive]
5489pub struct ToolResultExpanded {
5490 pub text_result_for_llm: String,
5492 pub result_type: String,
5494 #[serde(default, skip_serializing_if = "Option::is_none")]
5496 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5497 #[serde(skip_serializing_if = "Option::is_none")]
5499 pub session_log: Option<String>,
5500 #[serde(skip_serializing_if = "Option::is_none")]
5502 pub error: Option<String>,
5503 #[serde(default, skip_serializing_if = "Option::is_none")]
5505 pub tool_telemetry: Option<HashMap<String, Value>>,
5506 #[serde(default, skip_serializing_if = "Option::is_none")]
5508 pub tool_references: Option<Vec<String>>,
5509}
5510
5511impl ToolResultExpanded {
5512 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5516 Self {
5517 text_result_for_llm: text_result_for_llm.into(),
5518 result_type: result_type.into(),
5519 binary_results_for_llm: None,
5520 session_log: None,
5521 error: None,
5522 tool_telemetry: None,
5523 tool_references: None,
5524 }
5525 }
5526
5527 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5529 self.binary_results_for_llm = Some(results);
5530 self
5531 }
5532
5533 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5535 self.session_log = Some(session_log.into());
5536 self
5537 }
5538
5539 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5541 self.error = Some(error.into());
5542 self
5543 }
5544
5545 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5547 self.tool_telemetry = Some(telemetry);
5548 self
5549 }
5550
5551 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5553 where
5554 I: IntoIterator<Item = S>,
5555 S: Into<String>,
5556 {
5557 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5558 self
5559 }
5560}
5561
5562#[derive(Debug, Clone, Serialize, Deserialize)]
5564#[serde(untagged)]
5565#[non_exhaustive]
5566pub enum ToolResult {
5567 Text(String),
5569 Expanded(ToolResultExpanded),
5571}
5572
5573#[derive(Debug, Clone, Serialize, Deserialize)]
5575#[serde(rename_all = "camelCase")]
5576pub struct ToolResultResponse {
5577 pub result: ToolResult,
5579}
5580
5581#[derive(Debug, Clone, Serialize, Deserialize)]
5583#[serde(rename_all = "camelCase")]
5584pub struct SessionMetadata {
5585 pub session_id: SessionId,
5587 pub start_time: String,
5589 pub modified_time: String,
5591 #[serde(skip_serializing_if = "Option::is_none")]
5593 pub summary: Option<String>,
5594 pub is_remote: bool,
5596}
5597
5598#[derive(Debug, Clone, Serialize, Deserialize)]
5600#[serde(rename_all = "camelCase")]
5601pub struct ListSessionsResponse {
5602 pub sessions: Vec<SessionMetadata>,
5604}
5605
5606#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5610#[serde(rename_all = "camelCase")]
5611pub struct SessionListFilter {
5612 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5614 pub working_directory: Option<String>,
5615 #[serde(default, skip_serializing_if = "Option::is_none")]
5617 pub git_root: Option<String>,
5618 #[serde(default, skip_serializing_if = "Option::is_none")]
5620 pub repository: Option<String>,
5621 #[serde(default, skip_serializing_if = "Option::is_none")]
5623 pub branch: Option<String>,
5624}
5625
5626#[derive(Debug, Clone, Serialize, Deserialize)]
5628#[serde(rename_all = "camelCase")]
5629pub struct GetSessionMetadataResponse {
5630 #[serde(skip_serializing_if = "Option::is_none")]
5632 pub session: Option<SessionMetadata>,
5633}
5634
5635#[derive(Debug, Clone, Serialize, Deserialize)]
5637#[serde(rename_all = "camelCase")]
5638pub struct GetLastSessionIdResponse {
5639 #[serde(skip_serializing_if = "Option::is_none")]
5641 pub session_id: Option<SessionId>,
5642}
5643
5644#[derive(Debug, Clone, Serialize, Deserialize)]
5646#[serde(rename_all = "camelCase")]
5647pub struct GetForegroundSessionResponse {
5648 #[serde(skip_serializing_if = "Option::is_none")]
5650 pub session_id: Option<SessionId>,
5651}
5652
5653#[derive(Debug, Clone, Serialize, Deserialize)]
5655#[serde(rename_all = "camelCase")]
5656pub struct GetMessagesResponse {
5657 pub events: Vec<SessionEvent>,
5659}
5660
5661#[derive(Debug, Clone, Serialize, Deserialize)]
5663#[serde(rename_all = "camelCase")]
5664pub struct ElicitationResult {
5665 pub action: String,
5667 #[serde(skip_serializing_if = "Option::is_none")]
5669 pub content: Option<Value>,
5670}
5671
5672#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5678#[serde(rename_all = "camelCase")]
5679#[non_exhaustive]
5680pub enum ElicitationMode {
5681 Form,
5683 Url,
5685 #[serde(other)]
5687 Unknown,
5688}
5689
5690#[derive(Debug, Clone, Serialize, Deserialize)]
5697#[serde(rename_all = "camelCase")]
5698pub struct ElicitationRequest {
5699 pub message: String,
5701 #[serde(skip_serializing_if = "Option::is_none")]
5703 pub requested_schema: Option<Value>,
5704 #[serde(skip_serializing_if = "Option::is_none")]
5706 pub mode: Option<ElicitationMode>,
5707 #[serde(skip_serializing_if = "Option::is_none")]
5709 pub elicitation_source: Option<String>,
5710 #[serde(skip_serializing_if = "Option::is_none")]
5712 pub url: Option<String>,
5713}
5714
5715#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5720#[serde(rename_all = "camelCase")]
5721pub struct SessionCapabilities {
5722 #[serde(skip_serializing_if = "Option::is_none")]
5724 pub ui: Option<UiCapabilities>,
5725}
5726
5727#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5729#[serde(rename_all = "camelCase")]
5730pub struct UiCapabilities {
5731 #[serde(skip_serializing_if = "Option::is_none")]
5733 pub elicitation: Option<bool>,
5734 #[serde(skip_serializing_if = "Option::is_none")]
5745 pub mcp_apps: Option<bool>,
5746 #[serde(skip_serializing_if = "Option::is_none")]
5748 pub canvases: Option<bool>,
5749}
5750
5751#[derive(Debug, Clone, Default)]
5753pub struct UiInputOptions<'a> {
5754 pub title: Option<&'a str>,
5756 pub description: Option<&'a str>,
5758 pub min_length: Option<u64>,
5760 pub max_length: Option<u64>,
5762 pub format: Option<InputFormat>,
5764 pub default: Option<&'a str>,
5766}
5767
5768#[derive(Debug, Clone, Copy)]
5770#[non_exhaustive]
5771pub enum InputFormat {
5772 Email,
5774 Uri,
5776 Date,
5778 DateTime,
5780}
5781
5782impl InputFormat {
5783 pub fn as_str(&self) -> &'static str {
5785 match self {
5786 Self::Email => "email",
5787 Self::Uri => "uri",
5788 Self::Date => "date",
5789 Self::DateTime => "date-time",
5790 }
5791 }
5792}
5793
5794pub use crate::generated::api_types::{
5799 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5800 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5801 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5802 PermissionDecisionApproveOnce, PermissionDecisionContext, PermissionDecisionOutcome,
5803 PermissionDecisionReject, PermissionDecisionSource, PermissionDecisionSurface,
5804 PermissionDecisionUserNotAvailable,
5805};
5806
5807#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5813#[serde(rename_all = "kebab-case")]
5814#[non_exhaustive]
5815pub enum PermissionRequestKind {
5816 Shell,
5818 Write,
5820 Read,
5822 Url,
5824 Mcp,
5826 CustomTool,
5828 Memory,
5830 Hook,
5832 #[serde(other)]
5835 Unknown,
5836}
5837
5838#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5844#[serde(rename_all = "camelCase")]
5845pub struct PermissionRequestData {
5846 #[serde(default, skip_serializing_if = "Option::is_none")]
5850 pub kind: Option<PermissionRequestKind>,
5851 #[serde(default, skip_serializing_if = "Option::is_none")]
5854 pub tool_call_id: Option<String>,
5855 #[serde(default, skip_serializing_if = "Option::is_none")]
5857 pub managed_approval_required: Option<bool>,
5858 #[serde(default, skip_serializing_if = "is_false")]
5860 pub managed_settings_enabled: bool,
5861 #[serde(flatten)]
5865 pub extra: Value,
5866}
5867
5868#[derive(Debug, Clone, Serialize, Deserialize)]
5870#[serde(rename_all = "camelCase")]
5871pub struct ExitPlanModeData {
5872 #[serde(default)]
5874 pub summary: String,
5875 #[serde(default, skip_serializing_if = "Option::is_none")]
5877 pub plan_content: Option<String>,
5878 #[serde(default)]
5880 pub actions: Vec<String>,
5881 #[serde(default = "default_recommended_action")]
5883 pub recommended_action: String,
5884}
5885
5886fn default_recommended_action() -> String {
5887 "autopilot".to_string()
5888}
5889
5890impl Default for ExitPlanModeData {
5891 fn default() -> Self {
5892 Self {
5893 summary: String::new(),
5894 plan_content: None,
5895 actions: Vec::new(),
5896 recommended_action: default_recommended_action(),
5897 }
5898 }
5899}
5900
5901#[cfg(test)]
5902mod tests {
5903 use std::collections::HashMap;
5904 use std::path::PathBuf;
5905
5906 use serde_json::json;
5907
5908 use super::{
5909 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5910 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5911 CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry,
5912 ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
5913 InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
5914 MemoryConfiguration, NamedProviderConfig, ProviderConfig, ProviderModelConfig,
5915 ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, SessionId,
5916 SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
5917 ToolResultResponse, ensure_attachment_display_names,
5918 };
5919 use crate::generated::session_events::TypedSessionEvent;
5920
5921 #[test]
5922 fn tool_builder_composes() {
5923 let tool = Tool::new("greet")
5924 .with_description("Say hello")
5925 .with_namespaced_name("hello/greet")
5926 .with_instructions("Pass the user's name")
5927 .with_parameters(json!({
5928 "type": "object",
5929 "properties": { "name": { "type": "string" } },
5930 "required": ["name"]
5931 }))
5932 .with_overrides_built_in_tool(true)
5933 .with_skip_permission(true);
5934 assert_eq!(tool.name, "greet");
5935 assert_eq!(tool.description, "Say hello");
5936 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5937 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5938 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5939 assert!(tool.overrides_built_in_tool);
5940 assert!(tool.skip_permission);
5941 }
5942
5943 #[test]
5944 fn tool_defer_serialization() {
5945 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5946 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5947 let value = serde_json::to_value(&tool).unwrap();
5948 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5949
5950 let plain = Tool::new("plain");
5951 let value = serde_json::to_value(&plain).unwrap();
5952 assert!(value.get("defer").is_none());
5953 }
5954
5955 #[test]
5956 fn tool_metadata_serialization() {
5957 use indexmap::IndexMap;
5958
5959 let mut metadata = IndexMap::new();
5960 metadata.insert(
5961 "github.com/copilot:safeForTelemetry".to_string(),
5962 json!({ "name": true, "inputsNames": false }),
5963 );
5964 let tool = Tool::new("lookup").with_metadata(metadata);
5965 let value = serde_json::to_value(&tool).unwrap();
5966 assert_eq!(
5967 value
5968 .get("metadata")
5969 .unwrap()
5970 .get("github.com/copilot:safeForTelemetry")
5971 .unwrap(),
5972 &json!({ "name": true, "inputsNames": false })
5973 );
5974
5975 let plain = Tool::new("plain");
5977 let value = serde_json::to_value(&plain).unwrap();
5978 assert!(value.get("metadata").is_none());
5979 }
5980
5981 #[test]
5982 fn custom_agent_config_builder_with_model() {
5983 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5984 .with_model("claude-haiku-4.5")
5985 .with_display_name("My Agent");
5986 assert_eq!(agent.name, "my-agent");
5987 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5988 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5989 }
5990
5991 #[test]
5992 fn custom_agent_config_serializes_model() {
5993 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5994 let wire = serde_json::to_value(&agent).unwrap();
5995 assert_eq!(wire["model"], "claude-haiku-4.5");
5996 assert_eq!(wire["name"], "model-agent");
5997 }
5998
5999 #[test]
6000 fn custom_agent_config_omits_model_when_none() {
6001 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
6002 let wire = serde_json::to_value(&agent).unwrap();
6003 assert!(wire.get("model").is_none());
6004 }
6005
6006 #[test]
6007 fn custom_agent_config_builder_with_reasoning_effort() {
6008 let agent =
6009 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6010 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
6011 }
6012
6013 #[test]
6014 fn custom_agent_config_serializes_reasoning_effort() {
6015 let agent =
6016 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6017 let wire = serde_json::to_value(&agent).unwrap();
6018 assert_eq!(wire["reasoningEffort"], "high");
6019 }
6020
6021 #[test]
6022 fn custom_agent_config_omits_reasoning_effort_when_none() {
6023 let agent = CustomAgentConfig::new("default-agent", "prompt");
6024 let wire = serde_json::to_value(&agent).unwrap();
6025 assert!(wire.get("reasoningEffort").is_none());
6026 }
6027
6028 #[test]
6029 #[should_panic(expected = "tool parameter schema must be a JSON object")]
6030 fn tool_with_parameters_panics_on_non_object_value() {
6031 let _ = Tool::new("noop").with_parameters(json!(null));
6032 }
6033
6034 #[test]
6035 fn tool_result_expanded_serializes_binary_results_for_llm() {
6036 let response = ToolResultResponse {
6037 result: ToolResult::Expanded(ToolResultExpanded {
6038 text_result_for_llm: "rendered chart".to_string(),
6039 result_type: "success".to_string(),
6040 binary_results_for_llm: Some(vec![ToolBinaryResult {
6041 data: "aW1n".to_string(),
6042 mime_type: "image/png".to_string(),
6043 r#type: "image".to_string(),
6044 description: Some("chart preview".to_string()),
6045 }]),
6046 session_log: None,
6047 error: None,
6048 tool_telemetry: None,
6049 tool_references: None,
6050 }),
6051 };
6052
6053 let wire = serde_json::to_value(&response).unwrap();
6054
6055 assert_eq!(
6056 wire,
6057 json!({
6058 "result": {
6059 "textResultForLlm": "rendered chart",
6060 "resultType": "success",
6061 "binaryResultsForLlm": [
6062 {
6063 "data": "aW1n",
6064 "mimeType": "image/png",
6065 "type": "image",
6066 "description": "chart preview"
6067 }
6068 ]
6069 }
6070 })
6071 );
6072 }
6073
6074 #[test]
6075 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
6076 let response = ToolResultResponse {
6077 result: ToolResult::Expanded(ToolResultExpanded {
6078 text_result_for_llm: "ok".to_string(),
6079 result_type: "success".to_string(),
6080 binary_results_for_llm: None,
6081 session_log: None,
6082 error: None,
6083 tool_telemetry: None,
6084 tool_references: None,
6085 }),
6086 };
6087
6088 let wire = serde_json::to_value(&response).unwrap();
6089
6090 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6091 assert!(wire["result"].get("binaryResultsForLlm").is_none());
6092 }
6093
6094 #[test]
6095 fn tool_result_expanded_serializes_tool_references() {
6096 let response = ToolResultResponse {
6097 result: ToolResult::Expanded(
6098 ToolResultExpanded::new("found 2 tools", "success")
6099 .with_tool_references(["get_weather", "check_status"]),
6100 ),
6101 };
6102
6103 let wire = serde_json::to_value(&response).unwrap();
6104
6105 assert_eq!(
6106 wire,
6107 json!({
6108 "result": {
6109 "textResultForLlm": "found 2 tools",
6110 "resultType": "success",
6111 "toolReferences": ["get_weather", "check_status"]
6112 }
6113 })
6114 );
6115 }
6116
6117 #[test]
6118 fn tool_result_expanded_omits_tool_references_when_none() {
6119 let response = ToolResultResponse {
6120 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
6121 };
6122
6123 let wire = serde_json::to_value(&response).unwrap();
6124
6125 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6126 assert!(wire["result"].get("toolReferences").is_none());
6127 }
6128
6129 #[test]
6130 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
6131 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
6134 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
6135
6136 assert_eq!(
6137 expanded.tool_references.as_deref(),
6138 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6139 );
6140 }
6141
6142 #[test]
6143 fn tool_result_expanded_deserializes_tool_references() {
6144 let wire = json!({
6145 "textResultForLlm": "found tools",
6146 "resultType": "success",
6147 "toolReferences": ["alpha", "beta"]
6148 });
6149
6150 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
6151
6152 assert_eq!(
6153 expanded.tool_references.as_deref(),
6154 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6155 );
6156 }
6157
6158 #[test]
6159 fn session_config_default_wire_flags_off_without_handlers() {
6160 let cfg = SessionConfig::default();
6161 assert_eq!(cfg.mcp_oauth_token_storage, None);
6162 let (wire, _runtime) = cfg
6166 .into_wire(Some(SessionId::from("default-flags")))
6167 .expect("default config has no duplicate handlers");
6168 assert!(!wire.request_user_input);
6169 assert!(!wire.request_permission);
6170 assert!(!wire.request_elicitation);
6171 assert!(!wire.request_exit_plan_mode);
6172 assert!(!wire.request_auto_mode_switch);
6173 assert!(!wire.hooks);
6174 assert!(!wire.request_mcp_apps);
6175 }
6176
6177 #[test]
6178 fn resume_session_config_new_wire_flags_off_without_handlers() {
6179 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
6180 assert_eq!(cfg.mcp_oauth_token_storage, None);
6181 let (wire, _runtime) = cfg
6182 .into_wire()
6183 .expect("default resume config has no duplicate handlers");
6184 assert!(!wire.request_user_input);
6185 assert!(!wire.request_permission);
6186 assert!(!wire.request_elicitation);
6187 assert!(!wire.request_exit_plan_mode);
6188 assert!(!wire.request_auto_mode_switch);
6189 assert!(!wire.hooks);
6190 assert!(!wire.request_mcp_apps);
6191 }
6192
6193 #[test]
6194 fn custom_agents_local_only_serializes_on_create_and_resume() {
6195 let (create_wire, _) = SessionConfig::default()
6196 .with_custom_agents_local_only(false)
6197 .into_wire(Some(SessionId::from("create-locality")))
6198 .expect("create config has no duplicate handlers");
6199 let create_json = serde_json::to_value(&create_wire).unwrap();
6200 assert_eq!(create_json["customAgentsLocalOnly"], false);
6201
6202 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
6203 .with_custom_agents_local_only(false)
6204 .into_wire()
6205 .expect("resume config has no duplicate handlers");
6206 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6207 assert_eq!(resume_json["customAgentsLocalOnly"], false);
6208
6209 let (unset_create_wire, _) = SessionConfig::default()
6210 .into_wire(Some(SessionId::from("create-unset")))
6211 .expect("create config has no duplicate handlers");
6212 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6213 assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
6214
6215 let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
6216 .into_wire()
6217 .expect("resume config has no duplicate handlers");
6218 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6219 assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
6220 }
6221
6222 #[test]
6223 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6224 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
6225 assert_eq!(cfg.enable_mcp_apps, Some(true));
6226
6227 let (wire, _runtime) = cfg
6228 .into_wire(Some(SessionId::from("enable-mcp-apps")))
6229 .expect("enable_mcp_apps config has no duplicate handlers");
6230 assert!(wire.request_mcp_apps);
6231
6232 let json = serde_json::to_value(&wire).unwrap();
6233 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6234 }
6235
6236 #[test]
6237 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6238 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
6239 .with_enable_mcp_apps(true);
6240 assert_eq!(cfg.enable_mcp_apps, Some(true));
6241
6242 let (wire, _runtime) = cfg
6243 .into_wire()
6244 .expect("resume enable_mcp_apps config has no duplicate handlers");
6245 assert!(wire.request_mcp_apps);
6246
6247 let json = serde_json::to_value(&wire).unwrap();
6248 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6249 }
6250
6251 #[test]
6252 fn github_mcp_tool_config_serializes_for_create_and_resume() {
6253 let github_config = GitHubMcpToolConfig::new()
6254 .with_enable_all_tools(true)
6255 .with_additional_toolsets(["repos"])
6256 .with_additional_tools(["get_issue"])
6257 .with_enable_insiders_mode(true)
6258 .with_disable_form_deferral(true);
6259
6260 let (create_wire, _) = SessionConfig::default()
6261 .with_github_mcp_tool_config(github_config.clone())
6262 .into_wire(Some(SessionId::from("github-mcp")))
6263 .expect("create config has no duplicate handlers");
6264 assert_eq!(
6265 serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6266 serde_json::json!({
6267 "enableAllTools": true,
6268 "additionalToolsets": ["repos"],
6269 "additionalTools": ["get_issue"],
6270 "enableInsidersMode": true,
6271 "disableFormDeferral": true,
6272 })
6273 );
6274
6275 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6276 .with_github_mcp_tool_config(github_config)
6277 .into_wire()
6278 .expect("resume config has no duplicate handlers");
6279 assert!(resume_wire.github_mcp_tool_config.is_some());
6280
6281 let (unset_wire, _) = SessionConfig::default()
6282 .into_wire(Some(SessionId::from("github-mcp-unset")))
6283 .expect("default config has no duplicate handlers");
6284 assert!(
6285 serde_json::to_value(&unset_wire)
6286 .unwrap()
6287 .get("githubMcpToolConfig")
6288 .is_none()
6289 );
6290 }
6291
6292 #[test]
6293 fn memory_configuration_constructors_and_serde() {
6294 assert!(MemoryConfiguration::enabled().enabled);
6295 assert!(!MemoryConfiguration::disabled().enabled);
6296 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6297
6298 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6299 assert_eq!(json, serde_json::json!({ "enabled": true }));
6300 }
6301
6302 #[test]
6303 fn session_config_with_memory_serializes() {
6304 let (wire, _runtime) = SessionConfig::default()
6305 .with_memory(MemoryConfiguration::enabled())
6306 .into_wire(Some(SessionId::from("memory-on")))
6307 .expect("no duplicate handlers");
6308 let json = serde_json::to_value(&wire).unwrap();
6309 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6310
6311 let (wire_off, _) = SessionConfig::default()
6312 .with_memory(MemoryConfiguration::disabled())
6313 .into_wire(Some(SessionId::from("memory-off")))
6314 .expect("no duplicate handlers");
6315 let json_off = serde_json::to_value(&wire_off).unwrap();
6316 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6317
6318 let (empty_wire, _) = SessionConfig::default()
6320 .into_wire(Some(SessionId::from("memory-unset")))
6321 .expect("no duplicate handlers");
6322 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6323 assert!(empty_json.get("memory").is_none());
6324 }
6325
6326 #[test]
6327 fn resume_session_config_with_memory_serializes() {
6328 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6329 .with_memory(MemoryConfiguration::enabled())
6330 .into_wire()
6331 .expect("no duplicate handlers");
6332 let json = serde_json::to_value(&wire).unwrap();
6333 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6334
6335 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6337 .into_wire()
6338 .expect("no duplicate handlers");
6339 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6340 assert!(empty_json.get("memory").is_none());
6341 }
6342
6343 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6344 CopilotExpAssignmentResponse {
6345 features: vec!["copilot_exp_flag".to_string()],
6346 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6347 configs: vec![ExpConfigEntry {
6348 id: "cfg-1".to_string(),
6349 parameters: HashMap::from([
6350 ("threshold".to_string(), ExpFlagValue::Integer(5)),
6351 ("enabled".to_string(), ExpFlagValue::Bool(true)),
6352 ]),
6353 }],
6354 assignment_context: context.to_string(),
6355 ..Default::default()
6356 }
6357 }
6358
6359 #[test]
6360 fn exp_flag_value_round_trips_all_variants() {
6361 let values = serde_json::json!({
6362 "s": "text",
6363 "i": 7,
6364 "f": 1.5,
6365 "b": true,
6366 "n": null,
6367 });
6368 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6369 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6370 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6371 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6372 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6373 assert_eq!(parsed["n"], ExpFlagValue::Null);
6374 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6375 }
6376
6377 #[test]
6378 fn session_config_with_exp_assignments_serializes() {
6379 let assignments = sample_exp_assignments("ctx-123");
6380 let expected = serde_json::to_value(&assignments).unwrap();
6381 let (wire, _runtime) = SessionConfig::default()
6382 .with_exp_assignments(assignments)
6383 .into_wire(Some(SessionId::from("exp-on")))
6384 .expect("no duplicate handlers");
6385 let json = serde_json::to_value(&wire).unwrap();
6386 assert_eq!(json["expAssignments"], expected);
6387 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6388 assert_eq!(
6389 json["expAssignments"]["Flights"]["copilot_exp_flag"],
6390 "treatment"
6391 );
6392
6393 let (empty_wire, _) = SessionConfig::default()
6395 .into_wire(Some(SessionId::from("exp-unset")))
6396 .expect("no duplicate handlers");
6397 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6398 assert!(empty_json.get("expAssignments").is_none());
6399 }
6400
6401 #[test]
6402 fn resume_session_config_with_exp_assignments_serializes() {
6403 let assignments = sample_exp_assignments("ctx-456");
6404 let expected = serde_json::to_value(&assignments).unwrap();
6405 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6406 .with_exp_assignments(assignments)
6407 .into_wire()
6408 .expect("no duplicate handlers");
6409 let json = serde_json::to_value(&wire).unwrap();
6410 assert_eq!(json["expAssignments"], expected);
6411
6412 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6414 .into_wire()
6415 .expect("no duplicate handlers");
6416 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6417 assert!(empty_json.get("expAssignments").is_none());
6418 }
6419
6420 #[test]
6421 fn session_config_clone_preserves_exp_assignments() {
6422 let assignments = sample_exp_assignments("ctx-clone");
6423 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6424 let cloned = config.clone();
6425
6426 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6427
6428 let (wire, _runtime) = cloned
6429 .into_wire(Some(SessionId::from("exp-clone")))
6430 .expect("no duplicate handlers");
6431 let json = serde_json::to_value(&wire).unwrap();
6432 assert_eq!(
6433 json["expAssignments"],
6434 serde_json::to_value(&assignments).unwrap()
6435 );
6436 }
6437
6438 #[test]
6439 fn resume_session_config_clone_preserves_exp_assignments() {
6440 let assignments = sample_exp_assignments("ctx-clone-resume");
6441 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6442 .with_exp_assignments(assignments.clone());
6443 let cloned = config.clone();
6444
6445 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6446
6447 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6448 let json = serde_json::to_value(&wire).unwrap();
6449 assert_eq!(
6450 json["expAssignments"],
6451 serde_json::to_value(&assignments).unwrap()
6452 );
6453 }
6454
6455 #[test]
6456 #[allow(clippy::field_reassign_with_default)]
6457 fn session_config_into_wire_serializes_bucket_b_fields() {
6458 use std::path::PathBuf;
6459
6460 use super::{CloudSessionOptions, CloudSessionRepository};
6461
6462 let mut cfg = SessionConfig::default();
6463 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6464 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6465 cfg.github_token = Some("ghs_secret".to_string());
6466 cfg.include_sub_agent_streaming_events = Some(false);
6467 cfg.enable_session_telemetry = Some(false);
6468 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
6469 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
6470 cfg.enable_on_demand_instruction_discovery = Some(false);
6471 cfg.cloud = Some(CloudSessionOptions::with_repository(
6472 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
6473 ));
6474
6475 let (wire, _runtime) = cfg
6476 .into_wire(Some(SessionId::from("custom-id")))
6477 .expect("no duplicate handlers");
6478 let wire_json = serde_json::to_value(&wire).unwrap();
6479 assert_eq!(wire_json["sessionId"], "custom-id");
6480 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6481 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6482 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6483 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6484 assert_eq!(wire_json["enableSessionTelemetry"], false);
6485 assert_eq!(wire_json["reasoningSummary"], "concise");
6486 assert_eq!(wire_json["remoteSession"], "export");
6487 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6488 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6489 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6490 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6491
6492 let (empty_wire, _) = SessionConfig::default()
6494 .into_wire(Some(SessionId::from("empty")))
6495 .expect("default has no duplicate handlers");
6496 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6497 assert!(empty_json.get("gitHubToken").is_none());
6498 assert!(empty_json.get("enableSessionTelemetry").is_none());
6499 assert!(empty_json.get("reasoningSummary").is_none());
6500 assert!(empty_json.get("remoteSession").is_none());
6501 assert!(
6502 empty_json
6503 .get("enableOnDemandInstructionDiscovery")
6504 .is_none()
6505 );
6506 assert!(empty_json.get("cloud").is_none());
6507 }
6508
6509 #[test]
6510 fn session_config_into_wire_serializes_named_providers_and_models() {
6511 let cfg = SessionConfig::default()
6512 .with_providers(vec![
6513 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6514 .with_provider_type("openai")
6515 .with_wire_api("responses")
6516 .with_api_key("sk-test"),
6517 ])
6518 .with_models(vec![
6519 ProviderModelConfig::new("gpt-x", "my-openai")
6520 .with_wire_model("gpt-x-2025")
6521 .with_max_output_tokens(2048),
6522 ]);
6523
6524 let (wire, _) = cfg
6525 .into_wire(Some(SessionId::from("sess-providers")))
6526 .expect("no duplicate handlers");
6527 let wire_json = serde_json::to_value(&wire).unwrap();
6528 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6529 assert_eq!(
6530 wire_json["providers"][0]["baseUrl"],
6531 "https://api.example.com/v1"
6532 );
6533 assert_eq!(wire_json["providers"][0]["type"], "openai");
6534 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6535 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6536 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
6537 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
6538 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
6539 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
6540
6541 let (empty_wire, _) = SessionConfig::default()
6542 .into_wire(Some(SessionId::from("empty")))
6543 .expect("default has no duplicate handlers");
6544 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6545 assert!(empty_json.get("providers").is_none());
6546 assert!(empty_json.get("models").is_none());
6547 }
6548
6549 #[test]
6550 fn resume_config_into_wire_serializes_named_providers_and_models() {
6551 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
6552 .with_providers(vec![
6553 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
6554 .with_provider_type("azure")
6555 .with_azure(AzureProviderOptions {
6556 api_version: Some("2024-10-21".to_string()),
6557 }),
6558 ])
6559 .with_models(vec![
6560 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
6561 ]);
6562
6563 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6564 let wire_json = serde_json::to_value(&wire).unwrap();
6565 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
6566 assert_eq!(wire_json["providers"][0]["type"], "azure");
6567 assert_eq!(
6568 wire_json["providers"][0]["azure"]["apiVersion"],
6569 "2024-10-21"
6570 );
6571 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
6572 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
6573 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
6574
6575 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
6576 .into_wire()
6577 .expect("default has no duplicate handlers");
6578 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6579 assert!(empty_json.get("providers").is_none());
6580 assert!(empty_json.get("models").is_none());
6581 }
6582
6583 #[test]
6584 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
6585 use std::path::PathBuf;
6586
6587 let cfg = SessionConfig {
6588 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
6589 disabled_mcp_servers: Some(vec![
6590 "local-files".to_string(),
6591 "remote-github".to_string(),
6592 ]),
6593 large_output: Some(
6594 LargeToolOutputConfig::new()
6595 .with_enabled(true)
6596 .with_max_size_bytes(1024)
6597 .with_output_directory(PathBuf::from("/tmp/large-output")),
6598 ),
6599 ..Default::default()
6600 };
6601
6602 let (wire, _) = cfg
6603 .into_wire(Some(SessionId::from("sess-1")))
6604 .expect("no duplicate handlers");
6605 let wire_json = serde_json::to_value(&wire).unwrap();
6606 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
6607 assert_eq!(
6608 wire_json["disabledMcpServers"],
6609 serde_json::json!(["local-files", "remote-github"])
6610 );
6611 assert_eq!(wire_json["largeOutput"]["enabled"], true);
6612 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
6613 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
6614
6615 let (empty_wire, _) = SessionConfig::default()
6616 .into_wire(Some(SessionId::from("empty")))
6617 .expect("default has no duplicate handlers");
6618 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6619 assert!(empty_json.get("pluginDirectories").is_none());
6620 assert!(empty_json.get("disabledMcpServers").is_none());
6621 assert!(empty_json.get("largeOutput").is_none());
6622 }
6623
6624 #[test]
6625 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
6626 use std::path::PathBuf;
6627
6628 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6629 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6630 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6631 cfg.github_token = Some("ghs_secret".to_string());
6632 cfg.include_sub_agent_streaming_events = Some(true);
6633 cfg.enable_session_telemetry = Some(false);
6634 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
6635 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
6636 cfg.enable_on_demand_instruction_discovery = Some(false);
6637
6638 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6639 let wire_json = serde_json::to_value(&wire).unwrap();
6640 assert_eq!(wire_json["sessionId"], "sess-1");
6641 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6642 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6643 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6644 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
6645 assert_eq!(wire_json["enableSessionTelemetry"], false);
6646 assert_eq!(wire_json["reasoningSummary"], "detailed");
6647 assert_eq!(wire_json["remoteSession"], "on");
6648 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6649
6650 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6652 .into_wire()
6653 .expect("default resume has no duplicate handlers");
6654 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6655 assert!(empty_json.get("reasoningSummary").is_none());
6656 assert!(empty_json.get("remoteSession").is_none());
6657 assert!(
6658 empty_json
6659 .get("enableOnDemandInstructionDiscovery")
6660 .is_none()
6661 );
6662 }
6663
6664 #[test]
6665 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
6666 use std::path::PathBuf;
6667
6668 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6669 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6670 cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]);
6671 cfg.large_output = Some(
6672 LargeToolOutputConfig::new()
6673 .with_enabled(false)
6674 .with_max_size_bytes(2048)
6675 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6676 );
6677
6678 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6679 let wire_json = serde_json::to_value(&wire).unwrap();
6680 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6681 assert_eq!(
6682 wire_json["disabledMcpServers"],
6683 serde_json::json!(["local-files-r"])
6684 );
6685 assert_eq!(wire_json["largeOutput"]["enabled"], false);
6686 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6687 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6688
6689 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6690 .into_wire()
6691 .expect("default resume has no duplicate handlers");
6692 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6693 assert!(empty_json.get("pluginDirectories").is_none());
6694 assert!(empty_json.get("disabledMcpServers").is_none());
6695 assert!(empty_json.get("largeOutput").is_none());
6696 }
6697
6698 #[test]
6699 fn session_config_clones_disabled_mcp_servers() {
6700 let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]);
6701 let mut create_clone = create.clone();
6702 create_clone
6703 .disabled_mcp_servers
6704 .as_mut()
6705 .expect("configured disabled MCP servers")
6706 .push("remote-github".to_string());
6707 assert_eq!(
6708 create.disabled_mcp_servers.as_deref(),
6709 Some(&["local-files".to_string()][..])
6710 );
6711
6712 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6713 .with_disabled_mcp_servers(["local-files"]);
6714 let mut resume_clone = resume.clone();
6715 resume_clone
6716 .disabled_mcp_servers
6717 .as_mut()
6718 .expect("configured disabled MCP servers")
6719 .push("remote-github".to_string());
6720 assert_eq!(
6721 resume.disabled_mcp_servers.as_deref(),
6722 Some(&["local-files".to_string()][..])
6723 );
6724 }
6725
6726 #[test]
6727 fn session_config_builder_composes() {
6728 use indexmap::IndexMap;
6729
6730 let cfg = SessionConfig::default()
6731 .with_session_id(SessionId::from("sess-1"))
6732 .with_model("claude-sonnet-4")
6733 .with_client_name("test-app")
6734 .with_reasoning_effort("medium")
6735 .with_reasoning_summary(ReasoningSummary::Concise)
6736 .with_context_tier("long_context")
6737 .with_streaming(true)
6738 .with_tools([Tool::new("greet")])
6739 .with_available_tools(["bash", "view"])
6740 .with_excluded_tools(["dangerous"])
6741 .with_mcp_servers(IndexMap::new())
6742 .with_mcp_oauth_token_storage("persistent")
6743 .with_enable_config_discovery(true)
6744 .with_enable_on_demand_instruction_discovery(true)
6745 .with_skill_directories([PathBuf::from("/tmp/skills")])
6746 .with_disabled_skills(["broken-skill"])
6747 .with_disabled_mcp_servers(["local-files"])
6748 .with_agent("researcher")
6749 .with_config_directory(PathBuf::from("/tmp/config"))
6750 .with_working_directory(PathBuf::from("/tmp/work"))
6751 .with_additional_directories([PathBuf::from("/tmp/shared")])
6752 .with_github_token("ghp_test")
6753 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6754 .with_enable_session_telemetry(false)
6755 .with_include_sub_agent_streaming_events(false)
6756 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6757
6758 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
6759 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
6760 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6761 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
6762 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
6763 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
6764 assert_eq!(cfg.streaming, Some(true));
6765 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6766 assert_eq!(
6767 cfg.available_tools.as_deref(),
6768 Some(&["bash".to_string(), "view".to_string()][..])
6769 );
6770 assert_eq!(
6771 cfg.excluded_tools.as_deref(),
6772 Some(&["dangerous".to_string()][..])
6773 );
6774 assert!(cfg.mcp_servers.is_some());
6775 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6776 assert_eq!(cfg.enable_config_discovery, Some(true));
6777 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
6778 assert_eq!(
6779 cfg.skill_directories.as_deref(),
6780 Some(&[PathBuf::from("/tmp/skills")][..])
6781 );
6782 assert_eq!(
6783 cfg.disabled_skills.as_deref(),
6784 Some(&["broken-skill".to_string()][..])
6785 );
6786 assert_eq!(
6787 cfg.disabled_mcp_servers.as_deref(),
6788 Some(&["local-files".to_string()][..])
6789 );
6790 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6791 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6792 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6793 assert_eq!(
6794 cfg.additional_directories.as_deref(),
6795 Some(&[PathBuf::from("/tmp/shared")][..])
6796 );
6797 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6798 assert_eq!(
6799 cfg.capi,
6800 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6801 );
6802 assert_eq!(cfg.enable_session_telemetry, Some(false));
6803 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
6804 assert_eq!(
6805 cfg.extension_info,
6806 Some(ExtensionInfo::new("github-app", "counter"))
6807 );
6808 }
6809
6810 #[test]
6811 fn resume_session_config_builder_composes() {
6812 use indexmap::IndexMap;
6813
6814 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
6815 .with_client_name("test-app")
6816 .with_reasoning_summary(ReasoningSummary::None)
6817 .with_context_tier("default")
6818 .with_streaming(true)
6819 .with_tools([Tool::new("greet")])
6820 .with_available_tools(["bash", "view"])
6821 .with_excluded_tools(["dangerous"])
6822 .with_mcp_servers(IndexMap::new())
6823 .with_mcp_oauth_token_storage("persistent")
6824 .with_enable_config_discovery(true)
6825 .with_enable_on_demand_instruction_discovery(false)
6826 .with_skill_directories([PathBuf::from("/tmp/skills")])
6827 .with_disabled_skills(["broken-skill"])
6828 .with_disabled_mcp_servers(["local-files"])
6829 .with_agent("researcher")
6830 .with_config_directory(PathBuf::from("/tmp/config"))
6831 .with_working_directory(PathBuf::from("/tmp/work"))
6832 .with_additional_directories([PathBuf::from("/tmp/shared")])
6833 .with_github_token("ghp_test")
6834 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6835 .with_enable_session_telemetry(false)
6836 .with_include_sub_agent_streaming_events(true)
6837 .with_suppress_resume_event(true)
6838 .with_continue_pending_work(true)
6839 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6840
6841 assert_eq!(cfg.session_id.as_str(), "sess-2");
6842 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6843 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
6844 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
6845 assert_eq!(cfg.streaming, Some(true));
6846 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6847 assert_eq!(
6848 cfg.available_tools.as_deref(),
6849 Some(&["bash".to_string(), "view".to_string()][..])
6850 );
6851 assert_eq!(
6852 cfg.excluded_tools.as_deref(),
6853 Some(&["dangerous".to_string()][..])
6854 );
6855 assert!(cfg.mcp_servers.is_some());
6856 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6857 assert_eq!(cfg.enable_config_discovery, Some(true));
6858 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
6859 assert_eq!(
6860 cfg.skill_directories.as_deref(),
6861 Some(&[PathBuf::from("/tmp/skills")][..])
6862 );
6863 assert_eq!(
6864 cfg.disabled_skills.as_deref(),
6865 Some(&["broken-skill".to_string()][..])
6866 );
6867 assert_eq!(
6868 cfg.disabled_mcp_servers.as_deref(),
6869 Some(&["local-files".to_string()][..])
6870 );
6871 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6872 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6873 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6874 assert_eq!(
6875 cfg.additional_directories.as_deref(),
6876 Some(&[PathBuf::from("/tmp/shared")][..])
6877 );
6878 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6879 assert_eq!(
6880 cfg.capi,
6881 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6882 );
6883 assert_eq!(cfg.enable_session_telemetry, Some(false));
6884 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
6885 assert_eq!(cfg.suppress_resume_event, Some(true));
6886 assert_eq!(cfg.continue_pending_work, Some(true));
6887 assert_eq!(
6888 cfg.extension_info,
6889 Some(ExtensionInfo::new("github-app", "counter"))
6890 );
6891 }
6892
6893 #[test]
6897 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
6898 let cfg =
6899 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
6900 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6901 let json = serde_json::to_value(&wire).unwrap();
6902 assert_eq!(json["continuePendingWork"], true);
6903
6904 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6906 .into_wire()
6907 .expect("no duplicate handlers");
6908 let json = serde_json::to_value(&wire).unwrap();
6909 assert!(json.get("continuePendingWork").is_none());
6910 }
6911
6912 #[test]
6913 fn session_configs_serialize_additional_directories() {
6914 let create = SessionConfig::default().with_additional_directories([
6915 PathBuf::from("/tmp/shared"),
6916 PathBuf::from("/tmp/generated"),
6917 ]);
6918 let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
6919 let create_json = serde_json::to_value(&create_wire).unwrap();
6920 assert_eq!(
6921 create_json["additionalDirectories"],
6922 serde_json::json!(["/tmp/shared", "/tmp/generated"])
6923 );
6924
6925 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6926 .with_additional_directories([PathBuf::from("/tmp/resumed")]);
6927 let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
6928 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6929 assert_eq!(
6930 resume_json["additionalDirectories"],
6931 serde_json::json!(["/tmp/resumed"])
6932 );
6933 }
6934
6935 #[test]
6939 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
6940 let cfg =
6941 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
6942 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6943 let json = serde_json::to_value(&wire).unwrap();
6944 assert_eq!(json["disableResume"], true);
6945 assert!(json.get("suppressResumeEvent").is_none());
6946 }
6947
6948 #[test]
6951 fn session_config_serializes_instruction_directories_to_camel_case() {
6952 let cfg =
6953 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
6954 let (wire, _) = cfg
6955 .into_wire(Some(SessionId::from("instr-on")))
6956 .expect("no duplicate handlers");
6957 let json = serde_json::to_value(&wire).unwrap();
6958 assert_eq!(
6959 json["instructionDirectories"],
6960 serde_json::json!(["/tmp/instr"])
6961 );
6962
6963 let (wire, _) = SessionConfig::default()
6965 .into_wire(Some(SessionId::from("instr-off")))
6966 .expect("no duplicate handlers");
6967 let json = serde_json::to_value(&wire).unwrap();
6968 assert!(json.get("instructionDirectories").is_none());
6969 }
6970
6971 #[test]
6974 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
6975 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
6976 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
6977 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6978 let json = serde_json::to_value(&wire).unwrap();
6979 assert_eq!(
6980 json["instructionDirectories"],
6981 serde_json::json!(["/tmp/instr"])
6982 );
6983
6984 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6985 .into_wire()
6986 .expect("no duplicate handlers");
6987 let json = serde_json::to_value(&wire).unwrap();
6988 assert!(json.get("instructionDirectories").is_none());
6989 }
6990
6991 #[test]
6992 fn custom_agent_config_builder_composes() {
6993 use indexmap::IndexMap;
6994
6995 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
6996 .with_display_name("Research Assistant")
6997 .with_description("Investigates technical questions.")
6998 .with_tools(["bash", "view"])
6999 .with_mcp_servers(IndexMap::new())
7000 .with_infer(true)
7001 .with_skills(["rust-coding-skill"]);
7002
7003 assert_eq!(cfg.name, "researcher");
7004 assert_eq!(cfg.prompt, "You are a research assistant.");
7005 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
7006 assert_eq!(
7007 cfg.description.as_deref(),
7008 Some("Investigates technical questions.")
7009 );
7010 assert_eq!(
7011 cfg.tools.as_deref(),
7012 Some(&["bash".to_string(), "view".to_string()][..])
7013 );
7014 assert!(cfg.mcp_servers.is_some());
7015 assert_eq!(cfg.infer, Some(true));
7016 assert_eq!(
7017 cfg.skills.as_deref(),
7018 Some(&["rust-coding-skill".to_string()][..])
7019 );
7020 }
7021
7022 #[test]
7023 fn mcp_servers_serialize_in_insertion_order() {
7024 use indexmap::IndexMap;
7025
7026 let order = [
7032 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
7033 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
7034 ];
7035 let mut servers = IndexMap::new();
7036 for name in order {
7037 servers.insert(
7038 name.to_string(),
7039 McpServerConfig::Stdio(McpStdioServerConfig {
7040 command: "run".to_string(),
7041 ..Default::default()
7042 }),
7043 );
7044 }
7045
7046 let (wire, _runtime) = SessionConfig::default()
7047 .with_mcp_servers(servers)
7048 .into_wire(None)
7049 .expect("into_wire should succeed");
7050 let json = serde_json::to_string(&wire).expect("serialize wire");
7051
7052 let positions: Vec<usize> = order
7053 .iter()
7054 .map(|name| {
7055 json.find(&format!("\"{name}\""))
7056 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
7057 })
7058 .collect();
7059 let mut ascending = positions.clone();
7060 ascending.sort_unstable();
7061 assert_eq!(
7062 positions, ascending,
7063 "mcp server keys must serialize in insertion order: {json}"
7064 );
7065 }
7066
7067 #[test]
7068 fn infinite_session_config_builder_composes() {
7069 let cfg = InfiniteSessionConfig::new()
7070 .with_enabled(true)
7071 .with_background_compaction_threshold(0.75)
7072 .with_buffer_exhaustion_threshold(0.92);
7073
7074 assert_eq!(cfg.enabled, Some(true));
7075 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
7076 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
7077 }
7078
7079 #[test]
7080 fn provider_config_builder_composes() {
7081 use std::collections::HashMap;
7082
7083 let mut headers = HashMap::new();
7084 headers.insert("X-Custom".to_string(), "value".to_string());
7085
7086 let cfg = ProviderConfig::new("https://api.example.com")
7087 .with_provider_type("openai")
7088 .with_wire_api("completions")
7089 .with_transport("websockets")
7090 .with_api_key("sk-test")
7091 .with_bearer_token("bearer-test")
7092 .with_headers(headers)
7093 .with_model_id("gpt-4")
7094 .with_wire_model("azure-gpt-4-deployment")
7095 .with_max_prompt_tokens(8192)
7096 .with_max_output_tokens(2048);
7097
7098 assert_eq!(cfg.base_url, "https://api.example.com");
7099 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
7100 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
7101 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
7102 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
7103 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
7104 assert_eq!(
7105 cfg.headers
7106 .as_ref()
7107 .and_then(|h| h.get("X-Custom"))
7108 .map(String::as_str),
7109 Some("value"),
7110 );
7111 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
7112 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
7113 assert_eq!(cfg.max_prompt_tokens, Some(8192));
7114 assert_eq!(cfg.max_output_tokens, Some(2048));
7115
7116 let wire = serde_json::to_value(&cfg).unwrap();
7118 assert_eq!(wire["modelId"], "gpt-4");
7119 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
7120 assert_eq!(wire["maxPromptTokens"], 8192);
7121 assert_eq!(wire["maxOutputTokens"], 2048);
7122
7123 let unset = ProviderConfig::new("https://api.example.com");
7124 let wire_unset = serde_json::to_value(&unset).unwrap();
7125 assert!(wire_unset.get("modelId").is_none());
7126 assert!(wire_unset.get("wireModel").is_none());
7127 assert!(wire_unset.get("maxPromptTokens").is_none());
7128 assert!(wire_unset.get("maxOutputTokens").is_none());
7129 }
7130
7131 #[test]
7132 fn capi_session_options_builder_composes_and_serializes() {
7133 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
7134
7135 assert_eq!(cfg.enable_web_socket_responses, Some(false));
7136
7137 let wire = serde_json::to_value(&cfg).unwrap();
7138 assert_eq!(
7139 wire,
7140 serde_json::json!({ "enableWebSocketResponses": false })
7141 );
7142
7143 let unset = CapiSessionOptions::new();
7144 let wire_unset = serde_json::to_value(&unset).unwrap();
7145 assert!(wire_unset.get("enableWebSocketResponses").is_none());
7146 }
7147
7148 #[test]
7149 fn session_config_with_capi_serializes() {
7150 let (wire, _) = SessionConfig::default()
7151 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7152 .into_wire(Some(SessionId::from("capi-create")))
7153 .expect("no duplicate handlers");
7154 let json = serde_json::to_value(&wire).unwrap();
7155 assert_eq!(
7156 json["capi"],
7157 serde_json::json!({ "enableWebSocketResponses": false })
7158 );
7159
7160 let (empty_wire, _) = SessionConfig::default()
7161 .into_wire(Some(SessionId::from("capi-create-unset")))
7162 .expect("no duplicate handlers");
7163 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7164 assert!(empty_json.get("capi").is_none());
7165 }
7166
7167 #[test]
7168 fn resume_session_config_with_capi_serializes() {
7169 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7170 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7171 .into_wire()
7172 .expect("no duplicate handlers");
7173 let json = serde_json::to_value(&wire).unwrap();
7174 assert_eq!(
7175 json["capi"],
7176 serde_json::json!({ "enableWebSocketResponses": false })
7177 );
7178
7179 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
7180 .into_wire()
7181 .expect("no duplicate handlers");
7182 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7183 assert!(empty_json.get("capi").is_none());
7184 }
7185
7186 #[test]
7187 fn system_message_config_builder_composes() {
7188 use std::collections::HashMap;
7189
7190 let cfg = SystemMessageConfig::new()
7191 .with_mode("replace")
7192 .with_content("Custom system message.")
7193 .with_sections(HashMap::new());
7194
7195 assert_eq!(cfg.mode.as_deref(), Some("replace"));
7196 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
7197 assert!(cfg.sections.is_some());
7198 }
7199
7200 #[test]
7201 fn delivery_mode_serializes_to_kebab_case_strings() {
7202 assert_eq!(
7203 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
7204 "\"enqueue\""
7205 );
7206 assert_eq!(
7207 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
7208 "\"immediate\""
7209 );
7210 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
7211 assert_eq!(parsed, DeliveryMode::Immediate);
7212 }
7213
7214 #[test]
7215 fn agent_mode_serializes_to_kebab_case_strings() {
7216 assert_eq!(
7217 serde_json::to_string(&AgentMode::Interactive).unwrap(),
7218 "\"interactive\""
7219 );
7220 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
7221 assert_eq!(
7222 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
7223 "\"autopilot\""
7224 );
7225 assert_eq!(
7226 serde_json::to_string(&AgentMode::Shell).unwrap(),
7227 "\"shell\""
7228 );
7229 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
7230 assert_eq!(parsed, AgentMode::Plan);
7231 }
7232
7233 #[test]
7234 fn connection_state_distinguishes_variants() {
7235 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
7238 }
7239
7240 #[test]
7246 fn session_event_round_trips_agent_id_on_envelope() {
7247 let wire = json!({
7248 "id": "evt-1",
7249 "timestamp": "2026-04-30T12:00:00Z",
7250 "parentId": null,
7251 "agentId": "sub-agent-42",
7252 "type": "assistant.message",
7253 "data": { "message": "hi" }
7254 });
7255
7256 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
7257 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7258
7259 let roundtripped = serde_json::to_value(&event).unwrap();
7261 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7262
7263 let main_agent_event: SessionEvent = serde_json::from_value(json!({
7265 "id": "evt-2",
7266 "timestamp": "2026-04-30T12:00:01Z",
7267 "parentId": null,
7268 "type": "session.idle",
7269 "data": {}
7270 }))
7271 .unwrap();
7272 assert!(main_agent_event.agent_id.is_none());
7273 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
7274 assert!(roundtripped.get("agentId").is_none());
7275 }
7276
7277 #[test]
7279 fn typed_session_event_round_trips_agent_id_on_envelope() {
7280 let wire = json!({
7281 "id": "evt-1",
7282 "timestamp": "2026-04-30T12:00:00Z",
7283 "parentId": null,
7284 "agentId": "sub-agent-42",
7285 "type": "session.idle",
7286 "data": {}
7287 });
7288
7289 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
7290 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7291
7292 let roundtripped = serde_json::to_value(&event).unwrap();
7293 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7294 }
7295
7296 #[test]
7297 fn connection_state_variants_compile() {
7298 let _ = ConnectionState::Disconnected;
7302 let _ = ConnectionState::Connecting;
7303 let _ = ConnectionState::Connected;
7304 let _ = ConnectionState::Error;
7305 }
7306
7307 #[test]
7308 fn deserializes_runtime_attachment_variants() {
7309 let attachments: Vec<Attachment> = serde_json::from_value(json!([
7310 {
7311 "type": "file",
7312 "path": "/tmp/file.rs",
7313 "displayName": "file.rs",
7314 "lineRange": { "start": 7, "end": 12 }
7315 },
7316 {
7317 "type": "directory",
7318 "path": "/tmp/project",
7319 "displayName": "project"
7320 },
7321 {
7322 "type": "selection",
7323 "filePath": "/tmp/lib.rs",
7324 "displayName": "lib.rs",
7325 "text": "fn main() {}",
7326 "selection": {
7327 "start": { "line": 1, "character": 2 },
7328 "end": { "line": 3, "character": 4 }
7329 }
7330 },
7331 {
7332 "type": "blob",
7333 "data": "Zm9v",
7334 "mimeType": "image/png",
7335 "displayName": "image.png"
7336 },
7337 {
7338 "type": "github_reference",
7339 "number": 42,
7340 "title": "Fix rendering",
7341 "referenceType": "issue",
7342 "state": "open",
7343 "url": "https://github.com/example/repo/issues/42"
7344 }
7345 ]))
7346 .expect("attachments should deserialize");
7347
7348 assert_eq!(attachments.len(), 5);
7349 assert!(matches!(
7350 &attachments[0],
7351 Attachment::File {
7352 path,
7353 display_name,
7354 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7355 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7356 ));
7357 assert!(matches!(
7358 &attachments[1],
7359 Attachment::Directory { path, display_name }
7360 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7361 ));
7362 assert!(matches!(
7363 &attachments[2],
7364 Attachment::Selection {
7365 file_path,
7366 display_name,
7367 selection:
7368 AttachmentSelectionRange {
7369 start: AttachmentSelectionPosition { line: 1, character: 2 },
7370 end: AttachmentSelectionPosition { line: 3, character: 4 },
7371 },
7372 ..
7373 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
7374 ));
7375 assert!(matches!(
7376 &attachments[3],
7377 Attachment::Blob {
7378 data,
7379 mime_type,
7380 display_name,
7381 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
7382 ));
7383 assert!(matches!(
7384 &attachments[4],
7385 Attachment::GitHubReference {
7386 number: 42,
7387 title,
7388 reference_type: GitHubReferenceType::Issue,
7389 state,
7390 url,
7391 } if title == "Fix rendering"
7392 && state == "open"
7393 && url == "https://github.com/example/repo/issues/42"
7394 ));
7395 }
7396
7397 #[test]
7398 fn ensures_display_names_for_variants_that_support_them() {
7399 let mut attachments = vec![
7400 Attachment::File {
7401 path: PathBuf::from("/tmp/file.rs"),
7402 display_name: None,
7403 line_range: None,
7404 },
7405 Attachment::Selection {
7406 file_path: PathBuf::from("/tmp/src/lib.rs"),
7407 display_name: None,
7408 text: "fn main() {}".to_string(),
7409 selection: AttachmentSelectionRange {
7410 start: AttachmentSelectionPosition {
7411 line: 0,
7412 character: 0,
7413 },
7414 end: AttachmentSelectionPosition {
7415 line: 0,
7416 character: 10,
7417 },
7418 },
7419 },
7420 Attachment::Blob {
7421 data: "Zm9v".to_string(),
7422 mime_type: "image/png".to_string(),
7423 display_name: None,
7424 },
7425 Attachment::GitHubReference {
7426 number: 7,
7427 title: "Track regressions".to_string(),
7428 reference_type: GitHubReferenceType::Issue,
7429 state: "open".to_string(),
7430 url: "https://example.com/issues/7".to_string(),
7431 },
7432 ];
7433
7434 ensure_attachment_display_names(&mut attachments);
7435
7436 assert_eq!(attachments[0].display_name(), Some("file.rs"));
7437 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
7438 assert_eq!(attachments[2].display_name(), Some("attachment"));
7439 assert_eq!(attachments[3].display_name(), None);
7440 assert_eq!(
7441 attachments[3].label(),
7442 Some("Track regressions".to_string())
7443 );
7444 }
7445
7446 #[test]
7447 fn github_anchored_attachment_variants_round_trip() {
7448 let cases = vec![
7449 (
7450 "github_commit",
7451 json!({
7452 "type": "github_commit",
7453 "message": "Fix the thing",
7454 "oid": "abc123",
7455 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
7456 "url": "https://github.com/octocat/repo/commit/abc123"
7457 }),
7458 ),
7459 (
7460 "github_release",
7461 json!({
7462 "type": "github_release",
7463 "name": "v1.2.3",
7464 "repo": { "name": "repo", "owner": "octocat" },
7465 "tagName": "v1.2.3",
7466 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
7467 }),
7468 ),
7469 (
7470 "github_actions_job",
7471 json!({
7472 "type": "github_actions_job",
7473 "conclusion": "failure",
7474 "jobId": 99,
7475 "jobName": "build",
7476 "repo": { "name": "repo", "owner": "octocat" },
7477 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
7478 "workflowName": "CI"
7479 }),
7480 ),
7481 (
7482 "github_repository",
7483 json!({
7484 "type": "github_repository",
7485 "description": "An example repository",
7486 "ref": "main",
7487 "repo": { "name": "repo", "owner": "octocat" },
7488 "url": "https://github.com/octocat/repo"
7489 }),
7490 ),
7491 (
7492 "github_file_diff",
7493 json!({
7494 "type": "github_file_diff",
7495 "base": {
7496 "path": "src/lib.rs",
7497 "ref": "main",
7498 "repo": { "name": "repo", "owner": "octocat" }
7499 },
7500 "head": {
7501 "path": "src/lib.rs",
7502 "ref": "feature",
7503 "repo": { "name": "repo", "owner": "octocat" }
7504 },
7505 "url": "https://github.com/octocat/repo/compare/main...feature"
7506 }),
7507 ),
7508 (
7509 "github_tree_comparison",
7510 json!({
7511 "type": "github_tree_comparison",
7512 "base": {
7513 "repo": { "name": "repo", "owner": "octocat" },
7514 "revision": "main"
7515 },
7516 "head": {
7517 "repo": { "name": "repo", "owner": "octocat" },
7518 "revision": "feature"
7519 },
7520 "url": "https://github.com/octocat/repo/compare/main...feature"
7521 }),
7522 ),
7523 (
7524 "github_url",
7525 json!({
7526 "type": "github_url",
7527 "url": "https://github.com/octocat/repo/wiki"
7528 }),
7529 ),
7530 (
7531 "github_file",
7532 json!({
7533 "type": "github_file",
7534 "path": "src/main.rs",
7535 "ref": "main",
7536 "repo": { "name": "repo", "owner": "octocat" },
7537 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
7538 }),
7539 ),
7540 (
7541 "github_snippet",
7542 json!({
7543 "type": "github_snippet",
7544 "lineRange": { "start": 10, "end": 20 },
7545 "path": "src/main.rs",
7546 "ref": "main",
7547 "repo": { "name": "repo", "owner": "octocat" },
7548 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
7549 }),
7550 ),
7551 ];
7552
7553 for (expected_type, input) in cases {
7554 let attachment: Attachment = serde_json::from_value(input.clone())
7555 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
7556
7557 let serialized_string = serde_json::to_string(&attachment)
7562 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
7563
7564 assert_eq!(
7566 serialized_string.matches("\"type\":").count(),
7567 1,
7568 "{expected_type} must serialize a single `type` key"
7569 );
7570
7571 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
7572 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
7573 assert_eq!(
7574 serialized.get("type").and_then(|value| value.as_str()),
7575 Some(expected_type),
7576 "{expected_type} must serialize the correct discriminator"
7577 );
7578
7579 assert_eq!(
7581 serialized, input,
7582 "{expected_type} should round-trip without data loss"
7583 );
7584 let reparsed: Attachment = serde_json::from_value(serialized)
7585 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
7586 assert_eq!(
7587 reparsed, attachment,
7588 "{expected_type} should re-deserialize to the same value"
7589 );
7590 }
7591 }
7592}
7593
7594#[cfg(test)]
7595mod permission_builder_tests {
7596 use std::sync::Arc;
7597
7598 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
7599 use crate::permission;
7600 use crate::types::{
7601 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
7602 SessionId,
7603 };
7604
7605 fn data() -> PermissionRequestData {
7606 PermissionRequestData {
7607 extra: serde_json::json!({"tool": "shell"}),
7608 ..Default::default()
7609 }
7610 }
7611
7612 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7615 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7616 }
7617
7618 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7619 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7620 }
7621
7622 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
7623 handler
7624 .handle(SessionId::from("s1"), RequestId::new("1"), data())
7625 .await
7626 }
7627
7628 #[tokio::test]
7629 async fn approve_all_with_handler_present_approves() {
7630 let cfg = SessionConfig::default()
7631 .with_permission_handler(Arc::new(ApproveAllHandler))
7632 .approve_all_permissions();
7633 let h = resolve_create(cfg).expect("policy + handler yields handler");
7634 assert!(matches!(
7635 dispatch(&h).await,
7636 PermissionResult::Decision {
7637 decision: PermissionDecision::ApproveOnce(_),
7638 ..
7639 }
7640 ));
7641 }
7642
7643 #[tokio::test]
7644 async fn approve_all_standalone_produces_handler() {
7645 let cfg = SessionConfig::default().approve_all_permissions();
7646 let h = resolve_create(cfg).expect("policy alone yields handler");
7647 assert!(matches!(
7648 dispatch(&h).await,
7649 PermissionResult::Decision {
7650 decision: PermissionDecision::ApproveOnce(_),
7651 ..
7652 }
7653 ));
7654 }
7655
7656 #[tokio::test]
7659 async fn approve_all_is_order_independent() {
7660 let a = SessionConfig::default()
7661 .with_permission_handler(Arc::new(ApproveAllHandler))
7662 .approve_all_permissions();
7663 let b = SessionConfig::default()
7664 .approve_all_permissions()
7665 .with_permission_handler(Arc::new(ApproveAllHandler));
7666 let ha = resolve_create(a).unwrap();
7667 let hb = resolve_create(b).unwrap();
7668 assert!(matches!(
7669 dispatch(&ha).await,
7670 PermissionResult::Decision {
7671 decision: PermissionDecision::ApproveOnce(_),
7672 ..
7673 }
7674 ));
7675 assert!(matches!(
7676 dispatch(&hb).await,
7677 PermissionResult::Decision {
7678 decision: PermissionDecision::ApproveOnce(_),
7679 ..
7680 }
7681 ));
7682 }
7683
7684 #[tokio::test]
7685 async fn deny_all_is_order_independent() {
7686 let a = SessionConfig::default()
7687 .with_permission_handler(Arc::new(ApproveAllHandler))
7688 .deny_all_permissions();
7689 let b = SessionConfig::default()
7690 .deny_all_permissions()
7691 .with_permission_handler(Arc::new(ApproveAllHandler));
7692 let ha = resolve_create(a).unwrap();
7693 let hb = resolve_create(b).unwrap();
7694 assert!(matches!(
7695 dispatch(&ha).await,
7696 PermissionResult::Decision {
7697 decision: PermissionDecision::Reject(_),
7698 ..
7699 }
7700 ));
7701 assert!(matches!(
7702 dispatch(&hb).await,
7703 PermissionResult::Decision {
7704 decision: PermissionDecision::Reject(_),
7705 ..
7706 }
7707 ));
7708 }
7709
7710 #[tokio::test]
7711 async fn approve_permissions_if_consults_predicate() {
7712 let cfg = SessionConfig::default().approve_permissions_if(|d| {
7713 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7714 });
7715 let h = resolve_create(cfg).unwrap();
7716 assert!(matches!(
7717 dispatch(&h).await,
7718 PermissionResult::Decision {
7719 decision: PermissionDecision::Reject(_),
7720 ..
7721 }
7722 ));
7723 }
7724
7725 #[tokio::test]
7726 async fn approve_permissions_if_is_order_independent() {
7727 let predicate = |d: &PermissionRequestData| {
7728 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7729 };
7730 let a = SessionConfig::default()
7731 .with_permission_handler(Arc::new(ApproveAllHandler))
7732 .approve_permissions_if(predicate);
7733 let b = SessionConfig::default()
7734 .approve_permissions_if(predicate)
7735 .with_permission_handler(Arc::new(ApproveAllHandler));
7736 let ha = resolve_create(a).unwrap();
7737 let hb = resolve_create(b).unwrap();
7738 assert!(matches!(
7739 dispatch(&ha).await,
7740 PermissionResult::Decision {
7741 decision: PermissionDecision::Reject(_),
7742 ..
7743 }
7744 ));
7745 assert!(matches!(
7746 dispatch(&hb).await,
7747 PermissionResult::Decision {
7748 decision: PermissionDecision::Reject(_),
7749 ..
7750 }
7751 ));
7752 }
7753
7754 #[tokio::test]
7755 async fn resume_session_config_approve_all_works() {
7756 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
7757 .with_permission_handler(Arc::new(ApproveAllHandler))
7758 .approve_all_permissions();
7759 let h = resolve_resume(cfg).unwrap();
7760 assert!(matches!(
7761 dispatch(&h).await,
7762 PermissionResult::Decision {
7763 decision: PermissionDecision::ApproveOnce(_),
7764 ..
7765 }
7766 ));
7767 }
7768
7769 #[tokio::test]
7770 async fn resume_session_config_approve_all_is_order_independent() {
7771 let a = ResumeSessionConfig::new(SessionId::from("s1"))
7772 .with_permission_handler(Arc::new(ApproveAllHandler))
7773 .approve_all_permissions();
7774 let b = ResumeSessionConfig::new(SessionId::from("s1"))
7775 .approve_all_permissions()
7776 .with_permission_handler(Arc::new(ApproveAllHandler));
7777 let ha = resolve_resume(a).unwrap();
7778 let hb = resolve_resume(b).unwrap();
7779 assert!(matches!(
7780 dispatch(&ha).await,
7781 PermissionResult::Decision {
7782 decision: PermissionDecision::ApproveOnce(_),
7783 ..
7784 }
7785 ));
7786 assert!(matches!(
7787 dispatch(&hb).await,
7788 PermissionResult::Decision {
7789 decision: PermissionDecision::ApproveOnce(_),
7790 ..
7791 }
7792 ));
7793 }
7794
7795 #[test]
7796 fn session_config_enable_experimental_mode_serializes_when_set() {
7797 let cfg = SessionConfig::default().with_enable_experimental_mode(false);
7798 assert_eq!(cfg.enable_experimental_mode, Some(false));
7799
7800 let (wire, _runtime) = cfg
7801 .into_wire(Some(SessionId::from("experimental-mode")))
7802 .expect("enable_experimental_mode config has no duplicate handlers");
7803 assert_eq!(wire.is_experimental_mode, Some(false));
7804
7805 let json = serde_json::to_value(&wire).unwrap();
7806 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7807 }
7808
7809 #[test]
7810 fn session_config_enable_experimental_mode_omitted_when_none() {
7811 let cfg = SessionConfig::default();
7812 assert_eq!(cfg.enable_experimental_mode, None);
7813
7814 let (wire, _runtime) = cfg
7815 .into_wire(Some(SessionId::from("no-experimental-mode")))
7816 .expect("default config has no duplicate handlers");
7817 assert_eq!(wire.is_experimental_mode, None);
7818
7819 let json = serde_json::to_value(&wire).unwrap();
7820 assert!(json.get("isExperimentalMode").is_none());
7821 }
7822
7823 #[test]
7824 fn resume_session_config_enable_experimental_mode_serializes_when_set() {
7825 let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
7826 .with_enable_experimental_mode(false);
7827 assert_eq!(cfg.enable_experimental_mode, Some(false));
7828
7829 let (wire, _runtime) = cfg
7830 .into_wire()
7831 .expect("resume enable_experimental_mode config has no duplicate handlers");
7832 assert_eq!(wire.is_experimental_mode, Some(false));
7833
7834 let json = serde_json::to_value(&wire).unwrap();
7835 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7836 }
7837
7838 #[test]
7839 fn resume_session_config_enable_experimental_mode_omitted_when_none() {
7840 let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
7841 assert_eq!(cfg.enable_experimental_mode, None);
7842
7843 let (wire, _runtime) = cfg
7844 .into_wire()
7845 .expect("default resume config has no duplicate handlers");
7846 assert_eq!(wire.is_experimental_mode, None);
7847
7848 let json = serde_json::to_value(&wire).unwrap();
7849 assert!(json.get("isExperimentalMode").is_none());
7850 }
7851}
7852
7853#[cfg(test)]
7854mod is_terminal_tests {
7855 use super::Tool;
7856
7857 #[test]
7858 fn is_terminal_serializes_as_camel_case_when_set() {
7859 let tool = Tool {
7860 name: "clear_context".to_owned(),
7861 is_terminal: true,
7862 ..Default::default()
7863 };
7864 let value = serde_json::to_value(&tool).expect("tool serializes");
7865 assert_eq!(
7866 value.get("isTerminal"),
7867 Some(&serde_json::Value::Bool(true))
7868 );
7869 }
7870
7871 #[test]
7872 fn is_terminal_is_omitted_when_false() {
7873 let tool = Tool {
7874 name: "plain".to_owned(),
7875 ..Default::default()
7876 };
7877 let value = serde_json::to_value(&tool).expect("tool serializes");
7878 assert!(value.get("isTerminal").is_none());
7879 }
7880
7881 #[test]
7884 fn is_terminal_appears_in_debug_output() {
7885 let terminal = Tool {
7886 name: "clear_context".to_owned(),
7887 is_terminal: true,
7888 ..Default::default()
7889 };
7890 assert!(format!("{terminal:?}").contains("is_terminal: true"));
7891
7892 let plain = Tool {
7893 name: "plain".to_owned(),
7894 ..Default::default()
7895 };
7896 assert!(format!("{plain:?}").contains("is_terminal: false"));
7897 }
7898}