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
1763#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1765#[serde(rename_all = "lowercase")]
1766#[non_exhaustive]
1767pub enum DisableBypassPermissionsMode {
1768 Disable,
1770}
1771
1772#[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<DisableBypassPermissionsMode>,
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(
1804 mut self,
1805 value: DisableBypassPermissionsMode,
1806 ) -> Self {
1807 self.disable_bypass_permissions_mode = Some(value);
1808 self
1809 }
1810
1811 pub fn with_deny(mut self, rules: Vec<String>) -> Self {
1813 self.deny = Some(rules);
1814 self
1815 }
1816
1817 pub fn with_ask(mut self, rules: Vec<String>) -> Self {
1819 self.ask = Some(rules);
1820 self
1821 }
1822
1823 pub fn with_allow(mut self, rules: Vec<String>) -> Self {
1825 self.allow = Some(rules);
1826 self
1827 }
1828}
1829
1830#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1840#[serde(rename_all = "camelCase")]
1841#[non_exhaustive]
1842pub struct ManagedSettings {
1843 #[serde(default, skip_serializing_if = "Option::is_none")]
1845 pub permissions: Option<ManagedSettingsPermissions>,
1846}
1847
1848impl ManagedSettings {
1849 pub fn with_permissions(mut self, permissions: ManagedSettingsPermissions) -> Self {
1851 self.permissions = Some(permissions);
1852 self
1853 }
1854}
1855
1856#[derive(Clone)]
1908#[non_exhaustive]
1909pub struct SessionConfig {
1910 pub session_id: Option<SessionId>,
1912 pub model: Option<String>,
1914 pub client_name: Option<String>,
1916 pub reasoning_effort: Option<String>,
1918 pub reasoning_summary: Option<ReasoningSummary>,
1922 pub context_tier: Option<String>,
1925 pub streaming: Option<bool>,
1927 pub system_message: Option<SystemMessageConfig>,
1929 pub tools: Option<Vec<Tool>>,
1931 pub canvases: Option<Vec<CanvasDeclaration>>,
1933 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1938 pub request_canvas_renderer: Option<bool>,
1940 pub request_extensions: Option<bool>,
1942 pub extension_sdk_path: Option<String>,
1946 pub extension_info: Option<ExtensionInfo>,
1948 pub canvas_provider: Option<CanvasProviderIdentity>,
1951 pub available_tools: Option<Vec<String>>,
1953 pub excluded_tools: Option<Vec<String>>,
1955 pub excluded_builtin_agents: Option<Vec<String>>,
1961 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
1963 pub mcp_oauth_token_storage: Option<String>,
1972 pub enable_config_discovery: Option<bool>,
1975 pub skip_embedding_retrieval: Option<bool>,
1977 pub embedding_cache_storage: Option<String>,
1980 pub organization_custom_instructions: Option<String>,
1982 pub enable_on_demand_instruction_discovery: Option<bool>,
1984 pub enable_file_hooks: Option<bool>,
1986 pub enable_host_git_operations: Option<bool>,
1988 pub enable_session_store: Option<bool>,
1990 pub enable_skills: Option<bool>,
1992 pub enable_mcp_apps: Option<bool>,
2019 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
2024 pub skill_directories: Option<Vec<PathBuf>>,
2026 pub instruction_directories: Option<Vec<PathBuf>>,
2029 pub plugin_directories: Option<Vec<PathBuf>>,
2031 pub large_output: Option<LargeToolOutputConfig>,
2033 pub tool_search: Option<ToolSearchConfig>,
2037 pub disabled_skills: Option<Vec<String>>,
2040 pub disabled_mcp_servers: Option<Vec<String>>,
2044 pub hooks: Option<bool>,
2048 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2050 pub default_agent: Option<DefaultAgentConfig>,
2054 pub agent: Option<String>,
2057 pub infinite_sessions: Option<InfiniteSessionConfig>,
2060 pub provider: Option<ProviderConfig>,
2064 pub capi: Option<CapiSessionOptions>,
2070 pub providers: Option<Vec<NamedProviderConfig>>,
2077 pub models: Option<Vec<ProviderModelConfig>>,
2083 pub enable_session_telemetry: Option<bool>,
2091 pub enable_citations: Option<bool>,
2093 pub enable_file_change_tracking: Option<bool>,
2096 pub session_limits: Option<SessionLimitsConfig>,
2098 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2101 pub memory: Option<MemoryConfiguration>,
2103 pub config_directory: Option<PathBuf>,
2106 pub working_directory: Option<PathBuf>,
2109 pub additional_directories: Option<Vec<PathBuf>>,
2113 pub github_token: Option<String>,
2119 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2125 pub cloud: Option<CloudSessionOptions>,
2128 pub include_sub_agent_streaming_events: Option<bool>,
2132 pub commands: Option<Vec<CommandDefinition>>,
2136 #[doc(hidden)]
2143 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
2144 pub enable_managed_settings: Option<bool>,
2151 pub managed_settings: Option<ManagedSettings>,
2160 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2165 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2169 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2172 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2175 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2179 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2182 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2185 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2189 pub(crate) permission_policy: Option<crate::permission::Policy>,
2193 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2198 pub skip_custom_instructions: Option<bool>,
2202 pub custom_agents_local_only: Option<bool>,
2206 pub enable_experimental_mode: Option<bool>,
2211 pub coauthor_enabled: Option<bool>,
2215 pub manage_schedule_enabled: Option<bool>,
2219}
2220
2221impl std::fmt::Debug for SessionConfig {
2222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2223 f.debug_struct("SessionConfig")
2224 .field("session_id", &self.session_id)
2225 .field("model", &self.model)
2226 .field("client_name", &self.client_name)
2227 .field("reasoning_effort", &self.reasoning_effort)
2228 .field("reasoning_summary", &self.reasoning_summary)
2229 .field("context_tier", &self.context_tier)
2230 .field("streaming", &self.streaming)
2231 .field("system_message", &self.system_message)
2232 .field("tools", &self.tools)
2233 .field("canvases", &self.canvases)
2234 .field(
2235 "canvas_handler",
2236 &self.canvas_handler.as_ref().map(|_| "<set>"),
2237 )
2238 .field("request_canvas_renderer", &self.request_canvas_renderer)
2239 .field("request_extensions", &self.request_extensions)
2240 .field("extension_sdk_path", &self.extension_sdk_path)
2241 .field("extension_info", &self.extension_info)
2242 .field("canvas_provider", &self.canvas_provider)
2243 .field("available_tools", &self.available_tools)
2244 .field("excluded_tools", &self.excluded_tools)
2245 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2246 .field("mcp_servers", &self.mcp_servers)
2247 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2248 .field("embedding_cache_storage", &self.embedding_cache_storage)
2249 .field("enable_config_discovery", &self.enable_config_discovery)
2250 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2251 .field(
2252 "organization_custom_instructions",
2253 &self
2254 .organization_custom_instructions
2255 .as_ref()
2256 .map(|_| "<redacted>"),
2257 )
2258 .field(
2259 "enable_on_demand_instruction_discovery",
2260 &self.enable_on_demand_instruction_discovery,
2261 )
2262 .field("enable_file_hooks", &self.enable_file_hooks)
2263 .field(
2264 "enable_host_git_operations",
2265 &self.enable_host_git_operations,
2266 )
2267 .field("enable_session_store", &self.enable_session_store)
2268 .field("enable_skills", &self.enable_skills)
2269 .field("enable_mcp_apps", &self.enable_mcp_apps)
2270 .field("skill_directories", &self.skill_directories)
2271 .field("instruction_directories", &self.instruction_directories)
2272 .field("plugin_directories", &self.plugin_directories)
2273 .field("large_output", &self.large_output)
2274 .field("tool_search", &self.tool_search)
2275 .field("disabled_skills", &self.disabled_skills)
2276 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
2277 .field("hooks", &self.hooks)
2278 .field("custom_agents", &self.custom_agents)
2279 .field("default_agent", &self.default_agent)
2280 .field("agent", &self.agent)
2281 .field("infinite_sessions", &self.infinite_sessions)
2282 .field("provider", &self.provider)
2283 .field("capi", &self.capi)
2284 .field("enable_session_telemetry", &self.enable_session_telemetry)
2285 .field("enable_citations", &self.enable_citations)
2286 .field(
2287 "enable_file_change_tracking",
2288 &self.enable_file_change_tracking,
2289 )
2290 .field("session_limits", &self.session_limits)
2291 .field("model_capabilities", &self.model_capabilities)
2292 .field("memory", &self.memory)
2293 .field("config_directory", &self.config_directory)
2294 .field("working_directory", &self.working_directory)
2295 .field("additional_directories", &self.additional_directories)
2296 .field(
2297 "github_token",
2298 &self.github_token.as_ref().map(|_| "<redacted>"),
2299 )
2300 .field("remote_session", &self.remote_session)
2301 .field("cloud", &self.cloud)
2302 .field(
2303 "include_sub_agent_streaming_events",
2304 &self.include_sub_agent_streaming_events,
2305 )
2306 .field("commands", &self.commands)
2307 .field("exp_assignments", &self.exp_assignments)
2308 .field("enable_managed_settings", &self.enable_managed_settings)
2309 .field("enable_experimental_mode", &self.enable_experimental_mode)
2310 .field("managed_settings", &self.managed_settings)
2311 .field(
2312 "session_fs_provider",
2313 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2314 )
2315 .field(
2316 "permission_handler",
2317 &self.permission_handler.as_ref().map(|_| "<set>"),
2318 )
2319 .field(
2320 "elicitation_handler",
2321 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2322 )
2323 .field(
2324 "mcp_auth_handler",
2325 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2326 )
2327 .field(
2328 "user_input_handler",
2329 &self.user_input_handler.as_ref().map(|_| "<set>"),
2330 )
2331 .field(
2332 "exit_plan_mode_handler",
2333 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2334 )
2335 .field(
2336 "auto_mode_switch_handler",
2337 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2338 )
2339 .field(
2340 "hooks_handler",
2341 &self.hooks_handler.as_ref().map(|_| "<set>"),
2342 )
2343 .field(
2344 "system_message_transform",
2345 &self.system_message_transform.as_ref().map(|_| "<set>"),
2346 )
2347 .finish()
2348 }
2349}
2350
2351impl Default for SessionConfig {
2352 fn default() -> Self {
2358 Self {
2359 session_id: None,
2360 model: None,
2361 client_name: None,
2362 reasoning_effort: None,
2363 reasoning_summary: None,
2364 context_tier: None,
2365 streaming: None,
2366 system_message: None,
2367 tools: None,
2368 canvases: None,
2369 canvas_handler: None,
2370 request_canvas_renderer: None,
2371 request_extensions: None,
2372 extension_sdk_path: None,
2373 extension_info: None,
2374 canvas_provider: None,
2375 available_tools: None,
2376 excluded_tools: None,
2377 excluded_builtin_agents: None,
2378 mcp_servers: None,
2379 mcp_oauth_token_storage: None,
2380 enable_config_discovery: None,
2381 skip_embedding_retrieval: None,
2382 organization_custom_instructions: None,
2383 enable_on_demand_instruction_discovery: None,
2384 enable_file_hooks: None,
2385 enable_host_git_operations: None,
2386 enable_session_store: None,
2387 enable_skills: None,
2388 embedding_cache_storage: None,
2389 enable_mcp_apps: None,
2390 github_mcp_tool_config: None,
2391 skill_directories: None,
2392 instruction_directories: None,
2393 plugin_directories: None,
2394 large_output: None,
2395 tool_search: None,
2396 disabled_skills: None,
2397 disabled_mcp_servers: None,
2398 hooks: None,
2399 custom_agents: None,
2400 default_agent: None,
2401 agent: None,
2402 infinite_sessions: None,
2403 provider: None,
2404 capi: None,
2405 providers: None,
2406 models: None,
2407 enable_session_telemetry: None,
2408 enable_citations: None,
2409 enable_file_change_tracking: None,
2410 session_limits: None,
2411 model_capabilities: None,
2412 memory: None,
2413 config_directory: None,
2414 working_directory: None,
2415 additional_directories: None,
2416 github_token: None,
2417 remote_session: None,
2418 cloud: None,
2419 include_sub_agent_streaming_events: None,
2420 commands: None,
2421 exp_assignments: None,
2422 enable_managed_settings: None,
2423 managed_settings: None,
2424 session_fs_provider: None,
2425 permission_handler: None,
2426 elicitation_handler: None,
2427 mcp_auth_handler: None,
2428 user_input_handler: None,
2429 exit_plan_mode_handler: None,
2430 auto_mode_switch_handler: None,
2431 hooks_handler: None,
2432 permission_policy: None,
2433 system_message_transform: None,
2434 skip_custom_instructions: None,
2435 custom_agents_local_only: None,
2436 enable_experimental_mode: None,
2437 coauthor_enabled: None,
2438 manage_schedule_enabled: None,
2439 }
2440 }
2441}
2442
2443pub(crate) struct SessionConfigRuntime {
2449 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2450 pub permission_policy: Option<crate::permission::Policy>,
2451 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2452 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2453 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2454 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2455 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2456 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2457 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2458 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2459 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2460 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2461 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2462 pub commands: Option<Vec<CommandDefinition>>,
2463}
2464
2465impl SessionConfig {
2466 pub(crate) fn into_wire(
2478 mut self,
2479 session_id: Option<SessionId>,
2480 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2481 let permission_active =
2482 self.permission_handler.is_some() || self.permission_policy.is_some();
2483 let request_user_input = self.user_input_handler.is_some();
2484 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2485 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2486 let request_elicitation = self.elicitation_handler.is_some();
2487 let hooks_flag = self.hooks_handler.is_some();
2488
2489 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2490 if let Some(tools) = self.tools.as_mut() {
2491 for tool in tools.iter_mut() {
2492 if let Some(handler) = tool.handler.take()
2493 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2494 {
2495 return Err(crate::Error::with_message(
2496 crate::ErrorKind::InvalidConfig,
2497 format!("duplicate tool handler registered for name {:?}", tool.name),
2498 ));
2499 }
2500 }
2501 }
2502
2503 let wire_commands = self.commands.as_ref().map(|cmds| {
2504 cmds.iter()
2505 .map(|c| crate::wire::CommandWireDefinition {
2506 name: c.name.clone(),
2507 description: c.description.clone().unwrap_or_default(),
2508 })
2509 .collect()
2510 });
2511 let wire_canvases = self.canvases.clone();
2512 let canvas_handler = self.canvas_handler.clone();
2513 let bearer_token_providers =
2514 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2515
2516 let wire = crate::wire::SessionCreateWire {
2517 session_id,
2518 model: self.model,
2519 client_name: self.client_name,
2520 reasoning_effort: self.reasoning_effort,
2521 reasoning_summary: self.reasoning_summary,
2522 context_tier: self.context_tier,
2523 streaming: self.streaming,
2524 system_message: self.system_message,
2525 tools: self.tools,
2526 canvases: wire_canvases,
2527 request_canvas_renderer: self.request_canvas_renderer,
2528 request_extensions: self.request_extensions,
2529 extension_sdk_path: self.extension_sdk_path,
2530 extension_info: self.extension_info,
2531 canvas_provider: self.canvas_provider,
2532 available_tools: self.available_tools,
2533 excluded_tools: self.excluded_tools,
2534 excluded_builtin_agents: self.excluded_builtin_agents,
2535 tool_filter_precedence: "excluded",
2536 mcp_servers: self.mcp_servers,
2537 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2538 embedding_cache_storage: self.embedding_cache_storage,
2539 env_value_mode: "direct",
2540 enable_config_discovery: self.enable_config_discovery,
2541 skip_embedding_retrieval: self.skip_embedding_retrieval,
2542 organization_custom_instructions: self.organization_custom_instructions,
2543 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2544 enable_file_hooks: self.enable_file_hooks,
2545 enable_host_git_operations: self.enable_host_git_operations,
2546 enable_session_store: self.enable_session_store,
2547 enable_skills: self.enable_skills,
2548 request_user_input,
2549 request_permission: permission_active,
2550 request_exit_plan_mode,
2551 request_auto_mode_switch,
2552 request_elicitation,
2553 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2554 github_mcp_tool_config: self.github_mcp_tool_config,
2555 hooks: hooks_flag,
2556 skill_directories: self.skill_directories,
2557 instruction_directories: self.instruction_directories,
2558 plugin_directories: self.plugin_directories,
2559 large_output: self.large_output,
2560 tool_search: self.tool_search,
2561 disabled_skills: self.disabled_skills,
2562 disabled_mcp_servers: self.disabled_mcp_servers,
2563 custom_agents: self.custom_agents,
2564 custom_agents_local_only: self.custom_agents_local_only,
2565 default_agent: self.default_agent,
2566 agent: self.agent,
2567 infinite_sessions: self.infinite_sessions,
2568 provider: self.provider,
2569 capi: self.capi,
2570 providers: self.providers,
2571 models: self.models,
2572 enable_session_telemetry: self.enable_session_telemetry,
2573 enable_citations: self.enable_citations,
2574 enable_file_change_tracking: self.enable_file_change_tracking,
2575 session_limits: self.session_limits,
2576 model_capabilities: self.model_capabilities,
2577 memory: self.memory,
2578 config_dir: self.config_directory,
2579 working_directory: self.working_directory,
2580 additional_directories: self.additional_directories,
2581 github_token: self.github_token,
2582 remote_session: self.remote_session,
2583 cloud: self.cloud,
2584 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2585 enable_github_telemetry_forwarding: None,
2586 commands: wire_commands,
2587 exp_assignments: self.exp_assignments,
2588 enable_managed_settings: self.enable_managed_settings,
2589 is_experimental_mode: self.enable_experimental_mode,
2590 managed_settings: self.managed_settings,
2591 };
2592
2593 let runtime = SessionConfigRuntime {
2594 permission_handler: self.permission_handler,
2595 permission_policy: self.permission_policy,
2596 elicitation_handler: self.elicitation_handler,
2597 mcp_auth_handler: self.mcp_auth_handler,
2598 user_input_handler: self.user_input_handler,
2599 exit_plan_mode_handler: self.exit_plan_mode_handler,
2600 auto_mode_switch_handler: self.auto_mode_switch_handler,
2601 hooks_handler: self.hooks_handler,
2602 system_message_transform: self.system_message_transform,
2603 tool_handlers,
2604 canvas_handler,
2605 session_fs_provider: self.session_fs_provider,
2606 bearer_token_providers,
2607 commands: self.commands,
2608 };
2609
2610 Ok((wire, runtime))
2611 }
2612
2613 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2617 self.permission_handler = Some(handler);
2618 self
2619 }
2620
2621 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2624 self.elicitation_handler = Some(handler);
2625 self
2626 }
2627
2628 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2630 self.mcp_auth_handler = Some(handler);
2631 self
2632 }
2633
2634 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2637 self.user_input_handler = Some(handler);
2638 self
2639 }
2640
2641 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2643 self.exit_plan_mode_handler = Some(handler);
2644 self
2645 }
2646
2647 pub fn with_auto_mode_switch_handler(
2649 mut self,
2650 handler: Arc<dyn AutoModeSwitchHandler>,
2651 ) -> Self {
2652 self.auto_mode_switch_handler = Some(handler);
2653 self
2654 }
2655
2656 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2661 self.commands = Some(commands);
2662 self
2663 }
2664
2665 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2669 self.session_fs_provider = Some(provider);
2670 self
2671 }
2672
2673 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2676 self.hooks_handler = Some(hooks);
2677 self
2678 }
2679
2680 pub fn with_system_message_transform(
2684 mut self,
2685 transform: Arc<dyn SystemMessageTransform>,
2686 ) -> Self {
2687 self.system_message_transform = Some(transform);
2688 self
2689 }
2690
2691 pub fn approve_all_permissions(mut self) -> Self {
2697 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2698 self
2699 }
2700
2701 pub fn deny_all_permissions(mut self) -> Self {
2704 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2705 self
2706 }
2707
2708 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2713 where
2714 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2715 {
2716 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2717 self
2718 }
2719
2720 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2722 self.session_id = Some(id.into());
2723 self
2724 }
2725
2726 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2728 self.model = Some(model.into());
2729 self
2730 }
2731
2732 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2734 self.client_name = Some(name.into());
2735 self
2736 }
2737
2738 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2740 self.reasoning_effort = Some(effort.into());
2741 self
2742 }
2743
2744 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2746 self.reasoning_summary = Some(summary);
2747 self
2748 }
2749
2750 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2752 self.context_tier = Some(tier.into());
2753 self
2754 }
2755
2756 pub fn with_streaming(mut self, streaming: bool) -> Self {
2758 self.streaming = Some(streaming);
2759 self
2760 }
2761
2762 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2764 self.system_message = Some(system_message);
2765 self
2766 }
2767
2768 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2770 self.tools = Some(tools.into_iter().collect());
2771 self
2772 }
2773
2774 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2779 self.canvases = Some(canvases.into_iter().collect());
2780 self
2781 }
2782
2783 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2785 self.canvas_handler = Some(handler);
2786 self
2787 }
2788
2789 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2791 self.request_canvas_renderer = Some(request);
2792 self
2793 }
2794
2795 pub fn with_request_extensions(mut self, request: bool) -> Self {
2797 self.request_extensions = Some(request);
2798 self
2799 }
2800
2801 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2805 self.extension_sdk_path = Some(path.into());
2806 self
2807 }
2808
2809 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2811 self.extension_info = Some(extension_info);
2812 self
2813 }
2814
2815 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2818 self.canvas_provider = Some(canvas_provider);
2819 self
2820 }
2821
2822 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2824 where
2825 I: IntoIterator<Item = S>,
2826 S: Into<String>,
2827 {
2828 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2829 self
2830 }
2831
2832 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2834 where
2835 I: IntoIterator<Item = S>,
2836 S: Into<String>,
2837 {
2838 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2839 self
2840 }
2841
2842 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2844 where
2845 I: IntoIterator<Item = S>,
2846 S: Into<String>,
2847 {
2848 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2849 self
2850 }
2851
2852 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2854 self.mcp_servers = Some(servers);
2855 self
2856 }
2857
2858 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2866 self.mcp_oauth_token_storage = Some(mode.into());
2867 self
2868 }
2869
2870 pub fn with_embedding_cache_storage(
2872 mut self,
2873 embedding_cache_storage: impl Into<String>,
2874 ) -> Self {
2875 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2876 self
2877 }
2878
2879 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2882 self.enable_config_discovery = Some(enable);
2883 self
2884 }
2885
2886 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2888 self.skip_embedding_retrieval = Some(value);
2889 self
2890 }
2891
2892 pub fn with_organization_custom_instructions(
2894 mut self,
2895 instructions: impl Into<String>,
2896 ) -> Self {
2897 self.organization_custom_instructions = Some(instructions.into());
2898 self
2899 }
2900
2901 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2903 self.enable_on_demand_instruction_discovery = Some(value);
2904 self
2905 }
2906
2907 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2909 self.enable_file_hooks = Some(value);
2910 self
2911 }
2912
2913 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2915 self.enable_host_git_operations = Some(value);
2916 self
2917 }
2918
2919 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2921 self.enable_session_store = Some(value);
2922 self
2923 }
2924
2925 pub fn with_enable_skills(mut self, value: bool) -> Self {
2927 self.enable_skills = Some(value);
2928 self
2929 }
2930
2931 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2937 self.enable_mcp_apps = Some(enable);
2938 self
2939 }
2940
2941 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
2943 self.github_mcp_tool_config = Some(config);
2944 self
2945 }
2946
2947 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2949 where
2950 I: IntoIterator<Item = P>,
2951 P: Into<PathBuf>,
2952 {
2953 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2954 self
2955 }
2956
2957 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2961 where
2962 I: IntoIterator<Item = P>,
2963 P: Into<PathBuf>,
2964 {
2965 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2966 self
2967 }
2968
2969 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2971 where
2972 I: IntoIterator<Item = P>,
2973 P: Into<PathBuf>,
2974 {
2975 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2976 self
2977 }
2978
2979 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2981 self.large_output = Some(config);
2982 self
2983 }
2984
2985 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
2988 self.tool_search = Some(config);
2989 self
2990 }
2991
2992 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2994 where
2995 I: IntoIterator<Item = S>,
2996 S: Into<String>,
2997 {
2998 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2999 self
3000 }
3001
3002 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
3004 where
3005 I: IntoIterator<Item = S>,
3006 S: Into<String>,
3007 {
3008 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
3009 self
3010 }
3011
3012 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3014 mut self,
3015 agents: I,
3016 ) -> Self {
3017 self.custom_agents = Some(agents.into_iter().collect());
3018 self
3019 }
3020
3021 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3023 self.default_agent = Some(agent);
3024 self
3025 }
3026
3027 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3030 self.agent = Some(name.into());
3031 self
3032 }
3033
3034 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3037 self.infinite_sessions = Some(config);
3038 self
3039 }
3040
3041 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3043 self.provider = Some(provider);
3044 self
3045 }
3046
3047 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3049 self.capi = Some(capi);
3050 self
3051 }
3052
3053 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3059 self.providers = Some(providers);
3060 self
3061 }
3062
3063 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3069 self.models = Some(models);
3070 self
3071 }
3072
3073 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3077 self.enable_session_telemetry = Some(enable);
3078 self
3079 }
3080
3081 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3083 self.enable_citations = Some(enable);
3084 self
3085 }
3086
3087 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
3090 self.enable_file_change_tracking = Some(enable);
3091 self
3092 }
3093
3094 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3096 self.session_limits = Some(limits);
3097 self
3098 }
3099
3100 pub fn with_model_capabilities(
3102 mut self,
3103 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3104 ) -> Self {
3105 self.model_capabilities = Some(capabilities);
3106 self
3107 }
3108
3109 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3111 self.memory = Some(memory);
3112 self
3113 }
3114
3115 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3117 self.config_directory = Some(dir.into());
3118 self
3119 }
3120
3121 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3124 self.working_directory = Some(dir.into());
3125 self
3126 }
3127
3128 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
3130 where
3131 I: IntoIterator<Item = P>,
3132 P: Into<PathBuf>,
3133 {
3134 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
3135 self
3136 }
3137
3138 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3143 self.github_token = Some(token.into());
3144 self
3145 }
3146
3147 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3150 self.include_sub_agent_streaming_events = Some(include);
3151 self
3152 }
3153
3154 pub fn with_remote_session(
3156 mut self,
3157 mode: crate::generated::api_types::RemoteSessionMode,
3158 ) -> Self {
3159 self.remote_session = Some(mode);
3160 self
3161 }
3162
3163 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
3165 self.cloud = Some(cloud);
3166 self
3167 }
3168
3169 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3171 self.skip_custom_instructions = Some(value);
3172 self
3173 }
3174
3175 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3177 self.custom_agents_local_only = Some(value);
3178 self
3179 }
3180
3181 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
3183 self.enable_experimental_mode = Some(enable_experimental_mode);
3184 self
3185 }
3186
3187 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3189 self.coauthor_enabled = Some(value);
3190 self
3191 }
3192
3193 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3195 self.manage_schedule_enabled = Some(value);
3196 self
3197 }
3198
3199 #[doc(hidden)]
3207 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
3208 self.exp_assignments = Some(assignments);
3209 self
3210 }
3211
3212 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3218 self.enable_managed_settings = Some(enabled);
3219 self
3220 }
3221
3222 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
3227 self.managed_settings = Some(managed_settings);
3228 self
3229 }
3230}
3231#[derive(Clone)]
3238#[non_exhaustive]
3239pub struct ResumeSessionConfig {
3240 pub session_id: SessionId,
3242 pub model: Option<String>,
3245 pub client_name: Option<String>,
3247 pub reasoning_effort: Option<String>,
3249 pub reasoning_summary: Option<ReasoningSummary>,
3253 pub context_tier: Option<String>,
3256 pub streaming: Option<bool>,
3258 pub system_message: Option<SystemMessageConfig>,
3261 pub tools: Option<Vec<Tool>>,
3263 pub canvases: Option<Vec<CanvasDeclaration>>,
3265 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
3268 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3270 pub request_canvas_renderer: Option<bool>,
3272 pub request_extensions: Option<bool>,
3274 pub extension_sdk_path: Option<String>,
3278 pub extension_info: Option<ExtensionInfo>,
3280 pub canvas_provider: Option<CanvasProviderIdentity>,
3283 pub available_tools: Option<Vec<String>>,
3285 pub excluded_tools: Option<Vec<String>>,
3287 pub excluded_builtin_agents: Option<Vec<String>>,
3293 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3295 pub mcp_oauth_token_storage: Option<String>,
3298 pub enable_config_discovery: Option<bool>,
3301 pub skip_embedding_retrieval: Option<bool>,
3303 pub embedding_cache_storage: Option<String>,
3305 pub organization_custom_instructions: Option<String>,
3307 pub enable_on_demand_instruction_discovery: Option<bool>,
3309 pub enable_file_hooks: Option<bool>,
3311 pub enable_host_git_operations: Option<bool>,
3313 pub enable_session_store: Option<bool>,
3315 pub enable_skills: Option<bool>,
3317 pub enable_mcp_apps: Option<bool>,
3323 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
3328 pub skill_directories: Option<Vec<PathBuf>>,
3330 pub instruction_directories: Option<Vec<PathBuf>>,
3333 pub plugin_directories: Option<Vec<PathBuf>>,
3335 pub large_output: Option<LargeToolOutputConfig>,
3337 pub tool_search: Option<ToolSearchConfig>,
3340 pub disabled_skills: Option<Vec<String>>,
3342 pub disabled_mcp_servers: Option<Vec<String>>,
3345 pub hooks: Option<bool>,
3347 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3349 pub default_agent: Option<DefaultAgentConfig>,
3351 pub agent: Option<String>,
3353 pub infinite_sessions: Option<InfiniteSessionConfig>,
3355 pub provider: Option<ProviderConfig>,
3357 pub capi: Option<CapiSessionOptions>,
3363 pub providers: Option<Vec<NamedProviderConfig>>,
3369 pub models: Option<Vec<ProviderModelConfig>>,
3375 pub enable_session_telemetry: Option<bool>,
3383 pub enable_citations: Option<bool>,
3385 pub enable_file_change_tracking: Option<bool>,
3389 pub session_limits: Option<SessionLimitsConfig>,
3391 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3393 pub memory: Option<MemoryConfiguration>,
3395 pub config_directory: Option<PathBuf>,
3397 pub working_directory: Option<PathBuf>,
3399 pub additional_directories: Option<Vec<PathBuf>>,
3402 pub github_token: Option<String>,
3405 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3408 pub include_sub_agent_streaming_events: Option<bool>,
3410 pub commands: Option<Vec<CommandDefinition>>,
3414 #[doc(hidden)]
3419 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3420 pub enable_managed_settings: Option<bool>,
3426 pub managed_settings: Option<ManagedSettings>,
3432 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3437 pub suppress_resume_event: Option<bool>,
3440 pub continue_pending_work: Option<bool>,
3448 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3451 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3454 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3456 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3459 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3462 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3465 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3467 pub(crate) permission_policy: Option<crate::permission::Policy>,
3469 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3471 pub skip_custom_instructions: Option<bool>,
3473 pub custom_agents_local_only: Option<bool>,
3475 pub enable_experimental_mode: Option<bool>,
3480 pub coauthor_enabled: Option<bool>,
3482 pub manage_schedule_enabled: Option<bool>,
3484}
3485
3486impl std::fmt::Debug for ResumeSessionConfig {
3487 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3488 f.debug_struct("ResumeSessionConfig")
3489 .field("session_id", &self.session_id)
3490 .field("model", &self.model)
3491 .field("client_name", &self.client_name)
3492 .field("reasoning_effort", &self.reasoning_effort)
3493 .field("reasoning_summary", &self.reasoning_summary)
3494 .field("context_tier", &self.context_tier)
3495 .field("streaming", &self.streaming)
3496 .field("system_message", &self.system_message)
3497 .field("tools", &self.tools)
3498 .field("canvases", &self.canvases)
3499 .field(
3500 "canvas_handler",
3501 &self.canvas_handler.as_ref().map(|_| "<set>"),
3502 )
3503 .field("open_canvases", &self.open_canvases)
3504 .field("request_canvas_renderer", &self.request_canvas_renderer)
3505 .field("request_extensions", &self.request_extensions)
3506 .field("extension_sdk_path", &self.extension_sdk_path)
3507 .field("extension_info", &self.extension_info)
3508 .field("canvas_provider", &self.canvas_provider)
3509 .field("available_tools", &self.available_tools)
3510 .field("excluded_tools", &self.excluded_tools)
3511 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3512 .field("mcp_servers", &self.mcp_servers)
3513 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3514 .field("embedding_cache_storage", &self.embedding_cache_storage)
3515 .field("enable_config_discovery", &self.enable_config_discovery)
3516 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3517 .field(
3518 "organization_custom_instructions",
3519 &self
3520 .organization_custom_instructions
3521 .as_ref()
3522 .map(|_| "<redacted>"),
3523 )
3524 .field(
3525 "enable_on_demand_instruction_discovery",
3526 &self.enable_on_demand_instruction_discovery,
3527 )
3528 .field("enable_file_hooks", &self.enable_file_hooks)
3529 .field(
3530 "enable_host_git_operations",
3531 &self.enable_host_git_operations,
3532 )
3533 .field("enable_session_store", &self.enable_session_store)
3534 .field("enable_skills", &self.enable_skills)
3535 .field("enable_mcp_apps", &self.enable_mcp_apps)
3536 .field("skill_directories", &self.skill_directories)
3537 .field("instruction_directories", &self.instruction_directories)
3538 .field("plugin_directories", &self.plugin_directories)
3539 .field("large_output", &self.large_output)
3540 .field("tool_search", &self.tool_search)
3541 .field("disabled_skills", &self.disabled_skills)
3542 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
3543 .field("hooks", &self.hooks)
3544 .field("custom_agents", &self.custom_agents)
3545 .field("default_agent", &self.default_agent)
3546 .field("agent", &self.agent)
3547 .field("infinite_sessions", &self.infinite_sessions)
3548 .field("provider", &self.provider)
3549 .field("capi", &self.capi)
3550 .field("enable_session_telemetry", &self.enable_session_telemetry)
3551 .field("enable_citations", &self.enable_citations)
3552 .field(
3553 "enable_file_change_tracking",
3554 &self.enable_file_change_tracking,
3555 )
3556 .field("session_limits", &self.session_limits)
3557 .field("model_capabilities", &self.model_capabilities)
3558 .field("memory", &self.memory)
3559 .field("config_directory", &self.config_directory)
3560 .field("working_directory", &self.working_directory)
3561 .field("additional_directories", &self.additional_directories)
3562 .field(
3563 "github_token",
3564 &self.github_token.as_ref().map(|_| "<redacted>"),
3565 )
3566 .field("remote_session", &self.remote_session)
3567 .field(
3568 "include_sub_agent_streaming_events",
3569 &self.include_sub_agent_streaming_events,
3570 )
3571 .field("commands", &self.commands)
3572 .field("exp_assignments", &self.exp_assignments)
3573 .field("enable_managed_settings", &self.enable_managed_settings)
3574 .field("enable_experimental_mode", &self.enable_experimental_mode)
3575 .field("managed_settings", &self.managed_settings)
3576 .field(
3577 "session_fs_provider",
3578 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3579 )
3580 .field(
3581 "permission_handler",
3582 &self.permission_handler.as_ref().map(|_| "<set>"),
3583 )
3584 .field(
3585 "elicitation_handler",
3586 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3587 )
3588 .field(
3589 "user_input_handler",
3590 &self.user_input_handler.as_ref().map(|_| "<set>"),
3591 )
3592 .field(
3593 "exit_plan_mode_handler",
3594 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3595 )
3596 .field(
3597 "auto_mode_switch_handler",
3598 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3599 )
3600 .field(
3601 "hooks_handler",
3602 &self.hooks_handler.as_ref().map(|_| "<set>"),
3603 )
3604 .field(
3605 "system_message_transform",
3606 &self.system_message_transform.as_ref().map(|_| "<set>"),
3607 )
3608 .field("suppress_resume_event", &self.suppress_resume_event)
3609 .field("continue_pending_work", &self.continue_pending_work)
3610 .finish()
3611 }
3612}
3613
3614impl ResumeSessionConfig {
3615 pub(crate) fn into_wire(
3623 mut self,
3624 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3625 let permission_active =
3626 self.permission_handler.is_some() || self.permission_policy.is_some();
3627 let request_user_input = self.user_input_handler.is_some();
3628 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3629 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3630 let request_elicitation = self.elicitation_handler.is_some();
3631 let hooks_flag = self.hooks_handler.is_some();
3632
3633 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3634 if let Some(tools) = self.tools.as_mut() {
3635 for tool in tools.iter_mut() {
3636 if let Some(handler) = tool.handler.take()
3637 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3638 {
3639 return Err(crate::Error::with_message(
3640 crate::ErrorKind::InvalidConfig,
3641 format!("duplicate tool handler registered for name {:?}", tool.name),
3642 ));
3643 }
3644 }
3645 }
3646
3647 let wire_commands = self.commands.as_ref().map(|cmds| {
3648 cmds.iter()
3649 .map(|c| crate::wire::CommandWireDefinition {
3650 name: c.name.clone(),
3651 description: c.description.clone().unwrap_or_default(),
3652 })
3653 .collect()
3654 });
3655 let wire_canvases = self.canvases.clone();
3656 let canvas_handler = self.canvas_handler.clone();
3657 let bearer_token_providers =
3658 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3659
3660 let wire = crate::wire::SessionResumeWire {
3661 session_id: self.session_id,
3662 model: self.model,
3663 client_name: self.client_name,
3664 reasoning_effort: self.reasoning_effort,
3665 reasoning_summary: self.reasoning_summary,
3666 context_tier: self.context_tier,
3667 streaming: self.streaming,
3668 system_message: self.system_message,
3669 tools: self.tools,
3670 canvases: wire_canvases,
3671 open_canvases: self.open_canvases,
3672 request_canvas_renderer: self.request_canvas_renderer,
3673 request_extensions: self.request_extensions,
3674 extension_sdk_path: self.extension_sdk_path,
3675 extension_info: self.extension_info,
3676 canvas_provider: self.canvas_provider,
3677 available_tools: self.available_tools,
3678 excluded_tools: self.excluded_tools,
3679 excluded_builtin_agents: self.excluded_builtin_agents,
3680 tool_filter_precedence: "excluded",
3681 mcp_servers: self.mcp_servers,
3682 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3683 embedding_cache_storage: self.embedding_cache_storage,
3684 env_value_mode: "direct",
3685 enable_config_discovery: self.enable_config_discovery,
3686 skip_embedding_retrieval: self.skip_embedding_retrieval,
3687 organization_custom_instructions: self.organization_custom_instructions,
3688 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3689 enable_file_hooks: self.enable_file_hooks,
3690 enable_host_git_operations: self.enable_host_git_operations,
3691 enable_session_store: self.enable_session_store,
3692 enable_skills: self.enable_skills,
3693 request_user_input,
3694 request_permission: permission_active,
3695 request_exit_plan_mode,
3696 request_auto_mode_switch,
3697 request_elicitation,
3698 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3699 github_mcp_tool_config: self.github_mcp_tool_config,
3700 hooks: hooks_flag,
3701 skill_directories: self.skill_directories,
3702 instruction_directories: self.instruction_directories,
3703 plugin_directories: self.plugin_directories,
3704 large_output: self.large_output,
3705 tool_search: self.tool_search,
3706 disabled_skills: self.disabled_skills,
3707 disabled_mcp_servers: self.disabled_mcp_servers,
3708 custom_agents: self.custom_agents,
3709 custom_agents_local_only: self.custom_agents_local_only,
3710 default_agent: self.default_agent,
3711 agent: self.agent,
3712 infinite_sessions: self.infinite_sessions,
3713 provider: self.provider,
3714 capi: self.capi,
3715 providers: self.providers,
3716 models: self.models,
3717 enable_session_telemetry: self.enable_session_telemetry,
3718 enable_citations: self.enable_citations,
3719 enable_file_change_tracking: self.enable_file_change_tracking,
3720 session_limits: self.session_limits,
3721 model_capabilities: self.model_capabilities,
3722 memory: self.memory,
3723 config_dir: self.config_directory,
3724 working_directory: self.working_directory,
3725 additional_directories: self.additional_directories,
3726 github_token: self.github_token,
3727 remote_session: self.remote_session,
3728 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3729 enable_github_telemetry_forwarding: None,
3730 commands: wire_commands,
3731 exp_assignments: self.exp_assignments,
3732 enable_managed_settings: self.enable_managed_settings,
3733 is_experimental_mode: self.enable_experimental_mode,
3734 managed_settings: self.managed_settings,
3735 suppress_resume_event: self.suppress_resume_event,
3736 continue_pending_work: self.continue_pending_work,
3737 };
3738
3739 let runtime = SessionConfigRuntime {
3740 permission_handler: self.permission_handler,
3741 permission_policy: self.permission_policy,
3742 elicitation_handler: self.elicitation_handler,
3743 mcp_auth_handler: self.mcp_auth_handler,
3744 user_input_handler: self.user_input_handler,
3745 exit_plan_mode_handler: self.exit_plan_mode_handler,
3746 auto_mode_switch_handler: self.auto_mode_switch_handler,
3747 hooks_handler: self.hooks_handler,
3748 system_message_transform: self.system_message_transform,
3749 tool_handlers,
3750 canvas_handler,
3751 session_fs_provider: self.session_fs_provider,
3752 bearer_token_providers,
3753 commands: self.commands,
3754 };
3755
3756 Ok((wire, runtime))
3757 }
3758
3759 pub fn new(session_id: SessionId) -> Self {
3764 Self {
3765 session_id,
3766 model: None,
3767 client_name: None,
3768 reasoning_effort: None,
3769 reasoning_summary: None,
3770 context_tier: None,
3771 streaming: None,
3772 system_message: None,
3773 tools: None,
3774 canvases: None,
3775 canvas_handler: None,
3776 open_canvases: None,
3777 request_canvas_renderer: None,
3778 request_extensions: None,
3779 extension_sdk_path: None,
3780 extension_info: None,
3781 canvas_provider: None,
3782 available_tools: None,
3783 excluded_tools: None,
3784 excluded_builtin_agents: None,
3785 mcp_servers: None,
3786 mcp_oauth_token_storage: None,
3787 enable_config_discovery: None,
3788 skip_embedding_retrieval: None,
3789 organization_custom_instructions: None,
3790 enable_on_demand_instruction_discovery: None,
3791 enable_file_hooks: None,
3792 enable_host_git_operations: None,
3793 enable_session_store: None,
3794 enable_skills: None,
3795 embedding_cache_storage: None,
3796 enable_mcp_apps: None,
3797 github_mcp_tool_config: None,
3798 skill_directories: None,
3799 instruction_directories: None,
3800 plugin_directories: None,
3801 large_output: None,
3802 tool_search: None,
3803 disabled_skills: None,
3804 disabled_mcp_servers: None,
3805 hooks: None,
3806 custom_agents: None,
3807 default_agent: None,
3808 agent: None,
3809 infinite_sessions: None,
3810 provider: None,
3811 capi: None,
3812 providers: None,
3813 models: None,
3814 enable_session_telemetry: None,
3815 enable_citations: None,
3816 enable_file_change_tracking: None,
3817 session_limits: None,
3818 model_capabilities: None,
3819 memory: None,
3820 config_directory: None,
3821 working_directory: None,
3822 additional_directories: None,
3823 github_token: None,
3824 remote_session: None,
3825 include_sub_agent_streaming_events: None,
3826 commands: None,
3827 exp_assignments: None,
3828 enable_managed_settings: None,
3829 managed_settings: None,
3830 session_fs_provider: None,
3831 suppress_resume_event: None,
3832 continue_pending_work: None,
3833 permission_handler: None,
3834 elicitation_handler: None,
3835 mcp_auth_handler: None,
3836 user_input_handler: None,
3837 exit_plan_mode_handler: None,
3838 auto_mode_switch_handler: None,
3839 hooks_handler: None,
3840 permission_policy: None,
3841 system_message_transform: None,
3842 skip_custom_instructions: None,
3843 custom_agents_local_only: None,
3844 enable_experimental_mode: None,
3845 coauthor_enabled: None,
3846 manage_schedule_enabled: None,
3847 }
3848 }
3849
3850 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3852 self.permission_handler = Some(handler);
3853 self
3854 }
3855
3856 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3858 self.elicitation_handler = Some(handler);
3859 self
3860 }
3861
3862 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3864 self.mcp_auth_handler = Some(handler);
3865 self
3866 }
3867
3868 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3870 self.user_input_handler = Some(handler);
3871 self
3872 }
3873
3874 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3876 self.exit_plan_mode_handler = Some(handler);
3877 self
3878 }
3879
3880 pub fn with_auto_mode_switch_handler(
3882 mut self,
3883 handler: Arc<dyn AutoModeSwitchHandler>,
3884 ) -> Self {
3885 self.auto_mode_switch_handler = Some(handler);
3886 self
3887 }
3888
3889 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3892 self.hooks_handler = Some(hooks);
3893 self
3894 }
3895
3896 pub fn with_system_message_transform(
3898 mut self,
3899 transform: Arc<dyn SystemMessageTransform>,
3900 ) -> Self {
3901 self.system_message_transform = Some(transform);
3902 self
3903 }
3904
3905 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3909 self.commands = Some(commands);
3910 self
3911 }
3912
3913 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3916 self.session_fs_provider = Some(provider);
3917 self
3918 }
3919
3920 pub fn approve_all_permissions(mut self) -> Self {
3923 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3924 self
3925 }
3926
3927 pub fn deny_all_permissions(mut self) -> Self {
3930 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3931 self
3932 }
3933
3934 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3937 where
3938 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3939 {
3940 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3941 self
3942 }
3943
3944 pub fn with_model(mut self, model: impl Into<String>) -> Self {
3946 self.model = Some(model.into());
3947 self
3948 }
3949
3950 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3952 self.client_name = Some(name.into());
3953 self
3954 }
3955
3956 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3958 self.reasoning_effort = Some(effort.into());
3959 self
3960 }
3961
3962 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3964 self.reasoning_summary = Some(summary);
3965 self
3966 }
3967
3968 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3971 self.context_tier = Some(tier.into());
3972 self
3973 }
3974
3975 pub fn with_streaming(mut self, streaming: bool) -> Self {
3977 self.streaming = Some(streaming);
3978 self
3979 }
3980
3981 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3984 self.system_message = Some(system_message);
3985 self
3986 }
3987
3988 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3990 self.tools = Some(tools.into_iter().collect());
3991 self
3992 }
3993
3994 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3996 self.canvases = Some(canvases.into_iter().collect());
3997 self
3998 }
3999
4000 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
4002 self.canvas_handler = Some(handler);
4003 self
4004 }
4005
4006 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
4008 mut self,
4009 open_canvases: I,
4010 ) -> Self {
4011 self.open_canvases = Some(open_canvases.into_iter().collect());
4012 self
4013 }
4014
4015 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
4017 self.request_canvas_renderer = Some(request);
4018 self
4019 }
4020
4021 pub fn with_request_extensions(mut self, request: bool) -> Self {
4023 self.request_extensions = Some(request);
4024 self
4025 }
4026
4027 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
4031 self.extension_sdk_path = Some(path.into());
4032 self
4033 }
4034
4035 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
4037 self.extension_info = Some(extension_info);
4038 self
4039 }
4040
4041 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
4044 self.canvas_provider = Some(canvas_provider);
4045 self
4046 }
4047
4048 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
4050 where
4051 I: IntoIterator<Item = S>,
4052 S: Into<String>,
4053 {
4054 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
4055 self
4056 }
4057
4058 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
4060 where
4061 I: IntoIterator<Item = S>,
4062 S: Into<String>,
4063 {
4064 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
4065 self
4066 }
4067
4068 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
4070 where
4071 I: IntoIterator<Item = S>,
4072 S: Into<String>,
4073 {
4074 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
4075 self
4076 }
4077
4078 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
4080 self.mcp_servers = Some(servers);
4081 self
4082 }
4083
4084 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
4087 self.mcp_oauth_token_storage = Some(mode.into());
4088 self
4089 }
4090
4091 pub fn with_embedding_cache_storage(
4093 mut self,
4094 embedding_cache_storage: impl Into<String>,
4095 ) -> Self {
4096 self.embedding_cache_storage = Some(embedding_cache_storage.into());
4097 self
4098 }
4099
4100 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
4103 self.enable_config_discovery = Some(enable);
4104 self
4105 }
4106
4107 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
4109 self.skip_embedding_retrieval = Some(value);
4110 self
4111 }
4112
4113 pub fn with_organization_custom_instructions(
4115 mut self,
4116 instructions: impl Into<String>,
4117 ) -> Self {
4118 self.organization_custom_instructions = Some(instructions.into());
4119 self
4120 }
4121
4122 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
4124 self.enable_on_demand_instruction_discovery = Some(value);
4125 self
4126 }
4127
4128 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
4130 self.enable_file_hooks = Some(value);
4131 self
4132 }
4133
4134 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
4136 self.enable_host_git_operations = Some(value);
4137 self
4138 }
4139
4140 pub fn with_enable_session_store(mut self, value: bool) -> Self {
4142 self.enable_session_store = Some(value);
4143 self
4144 }
4145
4146 pub fn with_enable_skills(mut self, value: bool) -> Self {
4148 self.enable_skills = Some(value);
4149 self
4150 }
4151
4152 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
4158 self.enable_mcp_apps = Some(enable);
4159 self
4160 }
4161
4162 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
4164 self.github_mcp_tool_config = Some(config);
4165 self
4166 }
4167
4168 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
4170 where
4171 I: IntoIterator<Item = P>,
4172 P: Into<PathBuf>,
4173 {
4174 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
4175 self
4176 }
4177
4178 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
4182 where
4183 I: IntoIterator<Item = P>,
4184 P: Into<PathBuf>,
4185 {
4186 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
4187 self
4188 }
4189
4190 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
4192 where
4193 I: IntoIterator<Item = P>,
4194 P: Into<PathBuf>,
4195 {
4196 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
4197 self
4198 }
4199
4200 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
4202 self.large_output = Some(config);
4203 self
4204 }
4205
4206 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
4209 self.tool_search = Some(config);
4210 self
4211 }
4212
4213 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
4215 where
4216 I: IntoIterator<Item = S>,
4217 S: Into<String>,
4218 {
4219 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
4220 self
4221 }
4222
4223 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
4225 where
4226 I: IntoIterator<Item = S>,
4227 S: Into<String>,
4228 {
4229 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
4230 self
4231 }
4232
4233 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
4235 mut self,
4236 agents: I,
4237 ) -> Self {
4238 self.custom_agents = Some(agents.into_iter().collect());
4239 self
4240 }
4241
4242 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
4244 self.default_agent = Some(agent);
4245 self
4246 }
4247
4248 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
4250 self.agent = Some(name.into());
4251 self
4252 }
4253
4254 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
4256 self.infinite_sessions = Some(config);
4257 self
4258 }
4259
4260 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
4262 self.provider = Some(provider);
4263 self
4264 }
4265
4266 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
4268 self.capi = Some(capi);
4269 self
4270 }
4271
4272 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
4278 self.providers = Some(providers);
4279 self
4280 }
4281
4282 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
4288 self.models = Some(models);
4289 self
4290 }
4291
4292 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
4296 self.enable_session_telemetry = Some(enable);
4297 self
4298 }
4299
4300 pub fn with_enable_citations(mut self, enable: bool) -> Self {
4302 self.enable_citations = Some(enable);
4303 self
4304 }
4305
4306 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
4309 self.enable_file_change_tracking = Some(enable);
4310 self
4311 }
4312
4313 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
4315 self.session_limits = Some(limits);
4316 self
4317 }
4318
4319 pub fn with_model_capabilities(
4321 mut self,
4322 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
4323 ) -> Self {
4324 self.model_capabilities = Some(capabilities);
4325 self
4326 }
4327
4328 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
4330 self.memory = Some(memory);
4331 self
4332 }
4333
4334 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4336 self.config_directory = Some(dir.into());
4337 self
4338 }
4339
4340 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4342 self.working_directory = Some(dir.into());
4343 self
4344 }
4345
4346 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
4348 where
4349 I: IntoIterator<Item = P>,
4350 P: Into<PathBuf>,
4351 {
4352 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
4353 self
4354 }
4355
4356 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
4360 self.github_token = Some(token.into());
4361 self
4362 }
4363
4364 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4366 self.include_sub_agent_streaming_events = Some(include);
4367 self
4368 }
4369
4370 pub fn with_remote_session(
4372 mut self,
4373 mode: crate::generated::api_types::RemoteSessionMode,
4374 ) -> Self {
4375 self.remote_session = Some(mode);
4376 self
4377 }
4378
4379 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4382 self.suppress_resume_event = Some(suppress);
4383 self
4384 }
4385
4386 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4392 self.continue_pending_work = Some(continue_pending);
4393 self
4394 }
4395
4396 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4398 self.skip_custom_instructions = Some(value);
4399 self
4400 }
4401
4402 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4404 self.custom_agents_local_only = Some(value);
4405 self
4406 }
4407
4408 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
4410 self.enable_experimental_mode = Some(enable_experimental_mode);
4411 self
4412 }
4413
4414 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4416 self.coauthor_enabled = Some(value);
4417 self
4418 }
4419
4420 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4422 self.manage_schedule_enabled = Some(value);
4423 self
4424 }
4425
4426 #[doc(hidden)]
4430 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4431 self.exp_assignments = Some(assignments);
4432 self
4433 }
4434
4435 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4438 self.enable_managed_settings = Some(enabled);
4439 self
4440 }
4441
4442 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
4446 self.managed_settings = Some(managed_settings);
4447 self
4448 }
4449}
4450
4451#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4457#[serde(rename_all = "camelCase")]
4458#[non_exhaustive]
4459pub struct SystemMessageConfig {
4460 #[serde(skip_serializing_if = "Option::is_none")]
4462 pub mode: Option<String>,
4463 #[serde(skip_serializing_if = "Option::is_none")]
4465 pub content: Option<String>,
4466 #[serde(skip_serializing_if = "Option::is_none")]
4468 pub sections: Option<HashMap<String, SectionOverride>>,
4469}
4470
4471impl SystemMessageConfig {
4472 pub fn new() -> Self {
4475 Self::default()
4476 }
4477
4478 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4481 self.mode = Some(mode.into());
4482 self
4483 }
4484
4485 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4488 self.content = Some(content.into());
4489 self
4490 }
4491
4492 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4494 self.sections = Some(sections);
4495 self
4496 }
4497}
4498
4499#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4505#[serde(rename_all = "camelCase")]
4506pub struct SectionOverride {
4507 #[serde(skip_serializing_if = "Option::is_none")]
4510 pub action: Option<String>,
4511 #[serde(skip_serializing_if = "Option::is_none")]
4513 pub content: Option<String>,
4514}
4515
4516#[derive(Debug, Clone, Serialize, Deserialize)]
4518#[serde(rename_all = "camelCase")]
4519pub struct CreateSessionResult {
4520 pub session_id: SessionId,
4522 #[serde(skip_serializing_if = "Option::is_none")]
4524 pub workspace_path: Option<PathBuf>,
4525 #[serde(default, alias = "remote_url")]
4527 pub remote_url: Option<String>,
4528 #[serde(skip_serializing_if = "Option::is_none")]
4530 pub capabilities: Option<SessionCapabilities>,
4531}
4532
4533#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4535#[serde(rename_all = "camelCase")]
4536pub(crate) struct ResumeSessionResult {
4537 #[serde(default)]
4539 pub session_id: Option<SessionId>,
4540 #[serde(default, skip_serializing_if = "Option::is_none")]
4542 pub workspace_path: Option<PathBuf>,
4543 #[serde(default, alias = "remote_url")]
4545 pub remote_url: Option<String>,
4546 #[serde(default, skip_serializing_if = "Option::is_none")]
4548 pub capabilities: Option<SessionCapabilities>,
4549 #[serde(
4551 default,
4552 alias = "openCanvasInstances",
4553 skip_serializing_if = "Option::is_none"
4554 )]
4555 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4556}
4557
4558#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4560#[serde(rename_all = "lowercase")]
4561pub enum LogLevel {
4562 #[default]
4564 Info,
4565 Warning,
4567 Error,
4569}
4570
4571#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4576#[serde(rename_all = "camelCase")]
4577pub struct LogOptions {
4578 #[serde(skip_serializing_if = "Option::is_none")]
4580 pub level: Option<LogLevel>,
4581 #[serde(skip_serializing_if = "Option::is_none")]
4584 pub ephemeral: Option<bool>,
4585}
4586
4587impl LogOptions {
4588 pub fn with_level(mut self, level: LogLevel) -> Self {
4590 self.level = Some(level);
4591 self
4592 }
4593
4594 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4596 self.ephemeral = Some(ephemeral);
4597 self
4598 }
4599}
4600
4601#[derive(Debug, Clone, Default)]
4605pub struct SetModelOptions {
4606 pub reasoning_effort: Option<String>,
4609 pub reasoning_summary: Option<ReasoningSummary>,
4613 pub context_tier: Option<ContextTier>,
4616 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4620}
4621
4622impl SetModelOptions {
4623 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4625 self.reasoning_effort = Some(effort.into());
4626 self
4627 }
4628
4629 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4631 self.reasoning_summary = Some(summary);
4632 self
4633 }
4634
4635 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4637 self.context_tier = Some(tier);
4638 self
4639 }
4640
4641 pub fn with_model_capabilities(
4643 mut self,
4644 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4645 ) -> Self {
4646 self.model_capabilities = Some(caps);
4647 self
4648 }
4649}
4650
4651#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4658#[serde(rename_all = "camelCase")]
4659pub struct PingResponse {
4660 #[serde(default)]
4662 pub message: String,
4663 #[serde(default)]
4665 pub timestamp: String,
4666 #[serde(skip_serializing_if = "Option::is_none")]
4668 pub protocol_version: Option<u32>,
4669}
4670
4671#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4673#[serde(rename_all = "camelCase")]
4674pub struct AttachmentLineRange {
4675 pub start: u32,
4677 pub end: u32,
4679}
4680
4681#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4683#[serde(rename_all = "camelCase")]
4684pub struct AttachmentSelectionPosition {
4685 pub line: u32,
4687 pub character: u32,
4689}
4690
4691#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4693#[serde(rename_all = "camelCase")]
4694pub struct AttachmentSelectionRange {
4695 pub start: AttachmentSelectionPosition,
4697 pub end: AttachmentSelectionPosition,
4699}
4700
4701#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4703#[serde(rename_all = "snake_case")]
4704#[non_exhaustive]
4705pub enum GitHubReferenceType {
4706 Issue,
4708 Pr,
4710 Discussion,
4712}
4713
4714#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4720#[serde(rename_all = "camelCase")]
4721pub struct GitHubRepoPointer {
4722 #[serde(skip_serializing_if = "Option::is_none")]
4724 pub id: Option<i64>,
4725 pub name: String,
4727 pub owner: String,
4729}
4730
4731#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4733#[serde(rename_all = "camelCase")]
4734pub struct GitHubFileDiffSide {
4735 pub path: String,
4737 pub r#ref: String,
4739 pub repo: GitHubRepoPointer,
4741}
4742
4743#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4745#[serde(rename_all = "camelCase")]
4746pub struct GitHubTreeComparisonSide {
4747 pub repo: GitHubRepoPointer,
4749 pub revision: String,
4751}
4752
4753#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4755#[serde(rename_all = "camelCase")]
4756pub struct GitHubSnippetLineRange {
4757 pub start: i64,
4759 pub end: i64,
4761}
4762
4763#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4765#[serde(
4766 tag = "type",
4767 rename_all = "camelCase",
4768 rename_all_fields = "camelCase"
4769)]
4770#[non_exhaustive]
4771pub enum Attachment {
4772 File {
4774 path: PathBuf,
4776 #[serde(skip_serializing_if = "Option::is_none")]
4778 display_name: Option<String>,
4779 #[serde(skip_serializing_if = "Option::is_none")]
4781 line_range: Option<AttachmentLineRange>,
4782 },
4783 Directory {
4785 path: PathBuf,
4787 #[serde(skip_serializing_if = "Option::is_none")]
4789 display_name: Option<String>,
4790 },
4791 Selection {
4793 file_path: PathBuf,
4795 text: String,
4797 #[serde(skip_serializing_if = "Option::is_none")]
4799 display_name: Option<String>,
4800 selection: AttachmentSelectionRange,
4802 },
4803 Blob {
4805 data: String,
4807 mime_type: String,
4809 #[serde(skip_serializing_if = "Option::is_none")]
4811 display_name: Option<String>,
4812 },
4813 #[serde(rename = "github_reference")]
4815 GitHubReference {
4816 number: u64,
4818 title: String,
4820 reference_type: GitHubReferenceType,
4822 state: String,
4824 url: String,
4826 },
4827 #[serde(rename = "github_commit")]
4829 GitHubCommit {
4830 message: String,
4832 oid: String,
4834 repo: GitHubRepoPointer,
4836 url: String,
4838 },
4839 #[serde(rename = "github_release")]
4841 GitHubRelease {
4842 name: String,
4844 repo: GitHubRepoPointer,
4846 tag_name: String,
4848 url: String,
4850 },
4851 #[serde(rename = "github_actions_job")]
4853 GitHubActionsJob {
4854 #[serde(skip_serializing_if = "Option::is_none")]
4857 conclusion: Option<String>,
4858 job_id: i64,
4860 job_name: String,
4862 repo: GitHubRepoPointer,
4864 url: String,
4866 workflow_name: String,
4868 },
4869 #[serde(rename = "github_repository")]
4871 GitHubRepository {
4872 #[serde(skip_serializing_if = "Option::is_none")]
4874 description: Option<String>,
4875 #[serde(skip_serializing_if = "Option::is_none")]
4878 r#ref: Option<String>,
4879 repo: GitHubRepoPointer,
4881 url: String,
4883 },
4884 #[serde(rename = "github_file_diff")]
4886 GitHubFileDiff {
4887 #[serde(skip_serializing_if = "Option::is_none")]
4889 base: Option<GitHubFileDiffSide>,
4890 #[serde(skip_serializing_if = "Option::is_none")]
4892 head: Option<GitHubFileDiffSide>,
4893 url: String,
4895 },
4896 #[serde(rename = "github_tree_comparison")]
4898 GitHubTreeComparison {
4899 base: GitHubTreeComparisonSide,
4901 head: GitHubTreeComparisonSide,
4903 url: String,
4905 },
4906 #[serde(rename = "github_url")]
4908 GitHubUrl {
4909 url: String,
4911 },
4912 #[serde(rename = "github_file")]
4914 GitHubFile {
4915 path: String,
4917 r#ref: String,
4919 repo: GitHubRepoPointer,
4921 url: String,
4923 },
4924 #[serde(rename = "github_snippet")]
4926 GitHubSnippet {
4927 line_range: GitHubSnippetLineRange,
4929 path: String,
4931 r#ref: String,
4933 repo: GitHubRepoPointer,
4935 url: String,
4937 },
4938}
4939
4940impl Attachment {
4941 pub fn display_name(&self) -> Option<&str> {
4943 match self {
4944 Self::File { display_name, .. }
4945 | Self::Directory { display_name, .. }
4946 | Self::Selection { display_name, .. }
4947 | Self::Blob { display_name, .. } => display_name.as_deref(),
4948 Self::GitHubReference { .. }
4949 | Self::GitHubCommit { .. }
4950 | Self::GitHubRelease { .. }
4951 | Self::GitHubActionsJob { .. }
4952 | Self::GitHubRepository { .. }
4953 | Self::GitHubFileDiff { .. }
4954 | Self::GitHubTreeComparison { .. }
4955 | Self::GitHubUrl { .. }
4956 | Self::GitHubFile { .. }
4957 | Self::GitHubSnippet { .. } => None,
4958 }
4959 }
4960
4961 pub fn label(&self) -> Option<String> {
4963 if let Some(display_name) = self
4964 .display_name()
4965 .map(str::trim)
4966 .filter(|name| !name.is_empty())
4967 {
4968 return Some(display_name.to_string());
4969 }
4970
4971 match self {
4972 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4973 format!("#{}", number)
4974 } else {
4975 title.trim().to_string()
4976 }),
4977 _ => self.derived_display_name(),
4978 }
4979 }
4980
4981 pub fn ensure_display_name(&mut self) {
4983 if self
4984 .display_name()
4985 .map(str::trim)
4986 .is_some_and(|name| !name.is_empty())
4987 {
4988 return;
4989 }
4990
4991 let Some(derived_display_name) = self.derived_display_name() else {
4992 return;
4993 };
4994
4995 match self {
4996 Self::File { display_name, .. }
4997 | Self::Directory { display_name, .. }
4998 | Self::Selection { display_name, .. }
4999 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
5000 Self::GitHubReference { .. }
5001 | Self::GitHubCommit { .. }
5002 | Self::GitHubRelease { .. }
5003 | Self::GitHubActionsJob { .. }
5004 | Self::GitHubRepository { .. }
5005 | Self::GitHubFileDiff { .. }
5006 | Self::GitHubTreeComparison { .. }
5007 | Self::GitHubUrl { .. }
5008 | Self::GitHubFile { .. }
5009 | Self::GitHubSnippet { .. } => {}
5010 }
5011 }
5012
5013 fn derived_display_name(&self) -> Option<String> {
5014 match self {
5015 Self::File { path, .. } | Self::Directory { path, .. } => {
5016 Some(attachment_name_from_path(path))
5017 }
5018 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
5019 Self::Blob { .. } => Some("attachment".to_string()),
5020 Self::GitHubReference { .. }
5021 | Self::GitHubCommit { .. }
5022 | Self::GitHubRelease { .. }
5023 | Self::GitHubActionsJob { .. }
5024 | Self::GitHubRepository { .. }
5025 | Self::GitHubFileDiff { .. }
5026 | Self::GitHubTreeComparison { .. }
5027 | Self::GitHubUrl { .. }
5028 | Self::GitHubFile { .. }
5029 | Self::GitHubSnippet { .. } => None,
5030 }
5031 }
5032}
5033
5034fn attachment_name_from_path(path: &Path) -> String {
5035 path.file_name()
5036 .map(|name| name.to_string_lossy().into_owned())
5037 .filter(|name| !name.is_empty())
5038 .unwrap_or_else(|| {
5039 let full = path.to_string_lossy();
5040 if full.is_empty() {
5041 "attachment".to_string()
5042 } else {
5043 full.into_owned()
5044 }
5045 })
5046}
5047
5048pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
5050 for attachment in attachments {
5051 attachment.ensure_display_name();
5052 }
5053}
5054
5055#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5060#[serde(rename_all = "lowercase")]
5061#[non_exhaustive]
5062pub enum DeliveryMode {
5063 Enqueue,
5065 Immediate,
5067}
5068
5069#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5074#[serde(rename_all = "lowercase")]
5075#[non_exhaustive]
5076pub enum AgentMode {
5077 Interactive,
5079 Plan,
5081 Autopilot,
5083 Shell,
5085}
5086
5087#[derive(Debug, Clone)]
5116#[non_exhaustive]
5117pub struct MessageOptions {
5118 pub prompt: String,
5120 pub mode: Option<DeliveryMode>,
5126 pub agent_mode: Option<AgentMode>,
5130 pub attachments: Option<Vec<Attachment>>,
5132 pub wait_timeout: Option<Duration>,
5135 pub request_headers: Option<HashMap<String, String>>,
5139 pub traceparent: Option<String>,
5146 pub tracestate: Option<String>,
5150 pub display_prompt: Option<String>,
5152}
5153
5154impl MessageOptions {
5155 pub fn new(prompt: impl Into<String>) -> Self {
5157 Self {
5158 prompt: prompt.into(),
5159 mode: None,
5160 agent_mode: None,
5161 attachments: None,
5162 wait_timeout: None,
5163 request_headers: None,
5164 traceparent: None,
5165 tracestate: None,
5166 display_prompt: None,
5167 }
5168 }
5169
5170 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
5176 self.mode = Some(mode);
5177 self
5178 }
5179
5180 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
5184 self.agent_mode = Some(agent_mode);
5185 self
5186 }
5187
5188 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
5190 self.attachments = Some(attachments);
5191 self
5192 }
5193
5194 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
5196 self.wait_timeout = Some(timeout);
5197 self
5198 }
5199
5200 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
5202 self.request_headers = Some(headers);
5203 self
5204 }
5205
5206 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
5211 self.traceparent = ctx.traceparent;
5212 self.tracestate = ctx.tracestate;
5213 self
5214 }
5215
5216 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5218 self.traceparent = Some(traceparent.into());
5219 self
5220 }
5221
5222 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5224 self.tracestate = Some(tracestate.into());
5225 self
5226 }
5227
5228 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5230 self.display_prompt = Some(display_prompt.into());
5231 self
5232 }
5233}
5234
5235impl From<&str> for MessageOptions {
5236 fn from(prompt: &str) -> Self {
5237 Self::new(prompt)
5238 }
5239}
5240
5241impl From<String> for MessageOptions {
5242 fn from(prompt: String) -> Self {
5243 Self::new(prompt)
5244 }
5245}
5246
5247impl From<&String> for MessageOptions {
5248 fn from(prompt: &String) -> Self {
5249 Self::new(prompt.clone())
5250 }
5251}
5252
5253#[derive(Debug, Clone, Serialize, Deserialize)]
5255#[serde(rename_all = "camelCase")]
5256#[non_exhaustive]
5257pub struct GetStatusResponse {
5258 pub version: String,
5260 pub protocol_version: u32,
5262}
5263
5264#[derive(Debug, Clone, Serialize, Deserialize)]
5266#[serde(rename_all = "camelCase")]
5267#[non_exhaustive]
5268pub struct GetAuthStatusResponse {
5269 pub is_authenticated: bool,
5271 #[serde(skip_serializing_if = "Option::is_none")]
5274 pub auth_type: Option<String>,
5275 #[serde(skip_serializing_if = "Option::is_none")]
5277 pub host: Option<String>,
5278 #[serde(skip_serializing_if = "Option::is_none")]
5280 pub login: Option<String>,
5281 #[serde(skip_serializing_if = "Option::is_none")]
5283 pub status_message: Option<String>,
5284}
5285
5286#[derive(Debug, Clone, Serialize, Deserialize)]
5290#[serde(rename_all = "camelCase")]
5291pub struct SessionEventNotification {
5292 pub session_id: SessionId,
5294 pub event: SessionEvent,
5296}
5297
5298#[derive(Debug, Clone, Serialize, Deserialize)]
5305#[serde(rename_all = "camelCase")]
5306pub struct SessionEvent {
5307 pub id: String,
5309 pub timestamp: String,
5311 pub parent_id: Option<String>,
5313 #[serde(skip_serializing_if = "Option::is_none")]
5315 pub ephemeral: Option<bool>,
5316 #[serde(skip_serializing_if = "Option::is_none")]
5319 pub agent_id: Option<String>,
5320 #[serde(skip_serializing_if = "Option::is_none")]
5322 pub debug_cli_received_at_ms: Option<i64>,
5323 #[serde(skip_serializing_if = "Option::is_none")]
5325 pub debug_ws_forwarded_at_ms: Option<i64>,
5326 #[serde(rename = "type")]
5328 pub event_type: String,
5329 pub data: Value,
5331}
5332
5333impl SessionEvent {
5334 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5339 use serde::de::IntoDeserializer;
5340 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5341 self.event_type.as_str().into_deserializer();
5342 crate::generated::SessionEventType::deserialize(deserializer)
5343 .unwrap_or(crate::generated::SessionEventType::Unknown)
5344 }
5345
5346 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5352 serde_json::from_value(self.data.clone()).ok()
5353 }
5354
5355 pub fn is_transient_error(&self) -> bool {
5359 self.event_type == "session.error"
5360 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5361 }
5362}
5363
5364#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5369#[serde(rename_all = "camelCase")]
5370#[non_exhaustive]
5371pub struct ToolInvocation {
5372 pub session_id: SessionId,
5374 pub tool_call_id: String,
5376 pub tool_name: String,
5378 pub arguments: Value,
5380 #[serde(skip)]
5388 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5389 #[serde(default, skip_serializing_if = "Option::is_none")]
5394 pub traceparent: Option<String>,
5395 #[serde(default, skip_serializing_if = "Option::is_none")]
5398 pub tracestate: Option<String>,
5399}
5400
5401impl ToolInvocation {
5402 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5423 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5424 }
5425
5426 pub fn trace_context(&self) -> TraceContext {
5429 TraceContext {
5430 traceparent: self.traceparent.clone(),
5431 tracestate: self.tracestate.clone(),
5432 }
5433 }
5434}
5435
5436#[derive(Debug, Clone, Serialize, Deserialize)]
5438#[serde(rename_all = "camelCase")]
5439pub struct ToolBinaryResult {
5440 pub data: String,
5442 pub mime_type: String,
5444 pub r#type: String,
5446 #[serde(default, skip_serializing_if = "Option::is_none")]
5448 pub description: Option<String>,
5449}
5450
5451#[derive(Debug, Clone, Serialize, Deserialize)]
5458#[serde(rename_all = "camelCase")]
5459#[non_exhaustive]
5460pub struct ToolResultExpanded {
5461 pub text_result_for_llm: String,
5463 pub result_type: String,
5465 #[serde(default, skip_serializing_if = "Option::is_none")]
5467 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5468 #[serde(skip_serializing_if = "Option::is_none")]
5470 pub session_log: Option<String>,
5471 #[serde(skip_serializing_if = "Option::is_none")]
5473 pub error: Option<String>,
5474 #[serde(default, skip_serializing_if = "Option::is_none")]
5476 pub tool_telemetry: Option<HashMap<String, Value>>,
5477 #[serde(default, skip_serializing_if = "Option::is_none")]
5479 pub tool_references: Option<Vec<String>>,
5480}
5481
5482impl ToolResultExpanded {
5483 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5487 Self {
5488 text_result_for_llm: text_result_for_llm.into(),
5489 result_type: result_type.into(),
5490 binary_results_for_llm: None,
5491 session_log: None,
5492 error: None,
5493 tool_telemetry: None,
5494 tool_references: None,
5495 }
5496 }
5497
5498 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5500 self.binary_results_for_llm = Some(results);
5501 self
5502 }
5503
5504 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5506 self.session_log = Some(session_log.into());
5507 self
5508 }
5509
5510 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5512 self.error = Some(error.into());
5513 self
5514 }
5515
5516 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5518 self.tool_telemetry = Some(telemetry);
5519 self
5520 }
5521
5522 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5524 where
5525 I: IntoIterator<Item = S>,
5526 S: Into<String>,
5527 {
5528 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5529 self
5530 }
5531}
5532
5533#[derive(Debug, Clone, Serialize, Deserialize)]
5535#[serde(untagged)]
5536#[non_exhaustive]
5537pub enum ToolResult {
5538 Text(String),
5540 Expanded(ToolResultExpanded),
5542}
5543
5544#[derive(Debug, Clone, Serialize, Deserialize)]
5546#[serde(rename_all = "camelCase")]
5547pub struct ToolResultResponse {
5548 pub result: ToolResult,
5550}
5551
5552#[derive(Debug, Clone, Serialize, Deserialize)]
5554#[serde(rename_all = "camelCase")]
5555pub struct SessionMetadata {
5556 pub session_id: SessionId,
5558 pub start_time: String,
5560 pub modified_time: String,
5562 #[serde(skip_serializing_if = "Option::is_none")]
5564 pub summary: Option<String>,
5565 pub is_remote: bool,
5567}
5568
5569#[derive(Debug, Clone, Serialize, Deserialize)]
5571#[serde(rename_all = "camelCase")]
5572pub struct ListSessionsResponse {
5573 pub sessions: Vec<SessionMetadata>,
5575}
5576
5577#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5581#[serde(rename_all = "camelCase")]
5582pub struct SessionListFilter {
5583 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5585 pub working_directory: Option<String>,
5586 #[serde(default, skip_serializing_if = "Option::is_none")]
5588 pub git_root: Option<String>,
5589 #[serde(default, skip_serializing_if = "Option::is_none")]
5591 pub repository: Option<String>,
5592 #[serde(default, skip_serializing_if = "Option::is_none")]
5594 pub branch: Option<String>,
5595}
5596
5597#[derive(Debug, Clone, Serialize, Deserialize)]
5599#[serde(rename_all = "camelCase")]
5600pub struct GetSessionMetadataResponse {
5601 #[serde(skip_serializing_if = "Option::is_none")]
5603 pub session: Option<SessionMetadata>,
5604}
5605
5606#[derive(Debug, Clone, Serialize, Deserialize)]
5608#[serde(rename_all = "camelCase")]
5609pub struct GetLastSessionIdResponse {
5610 #[serde(skip_serializing_if = "Option::is_none")]
5612 pub session_id: Option<SessionId>,
5613}
5614
5615#[derive(Debug, Clone, Serialize, Deserialize)]
5617#[serde(rename_all = "camelCase")]
5618pub struct GetForegroundSessionResponse {
5619 #[serde(skip_serializing_if = "Option::is_none")]
5621 pub session_id: Option<SessionId>,
5622}
5623
5624#[derive(Debug, Clone, Serialize, Deserialize)]
5626#[serde(rename_all = "camelCase")]
5627pub struct GetMessagesResponse {
5628 pub events: Vec<SessionEvent>,
5630}
5631
5632#[derive(Debug, Clone, Serialize, Deserialize)]
5634#[serde(rename_all = "camelCase")]
5635pub struct ElicitationResult {
5636 pub action: String,
5638 #[serde(skip_serializing_if = "Option::is_none")]
5640 pub content: Option<Value>,
5641}
5642
5643#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5649#[serde(rename_all = "camelCase")]
5650#[non_exhaustive]
5651pub enum ElicitationMode {
5652 Form,
5654 Url,
5656 #[serde(other)]
5658 Unknown,
5659}
5660
5661#[derive(Debug, Clone, Serialize, Deserialize)]
5668#[serde(rename_all = "camelCase")]
5669pub struct ElicitationRequest {
5670 pub message: String,
5672 #[serde(skip_serializing_if = "Option::is_none")]
5674 pub requested_schema: Option<Value>,
5675 #[serde(skip_serializing_if = "Option::is_none")]
5677 pub mode: Option<ElicitationMode>,
5678 #[serde(skip_serializing_if = "Option::is_none")]
5680 pub elicitation_source: Option<String>,
5681 #[serde(skip_serializing_if = "Option::is_none")]
5683 pub url: Option<String>,
5684}
5685
5686#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5691#[serde(rename_all = "camelCase")]
5692pub struct SessionCapabilities {
5693 #[serde(skip_serializing_if = "Option::is_none")]
5695 pub ui: Option<UiCapabilities>,
5696}
5697
5698#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5700#[serde(rename_all = "camelCase")]
5701pub struct UiCapabilities {
5702 #[serde(skip_serializing_if = "Option::is_none")]
5704 pub elicitation: Option<bool>,
5705 #[serde(skip_serializing_if = "Option::is_none")]
5716 pub mcp_apps: Option<bool>,
5717 #[serde(skip_serializing_if = "Option::is_none")]
5719 pub canvases: Option<bool>,
5720}
5721
5722#[derive(Debug, Clone, Default)]
5724pub struct UiInputOptions<'a> {
5725 pub title: Option<&'a str>,
5727 pub description: Option<&'a str>,
5729 pub min_length: Option<u64>,
5731 pub max_length: Option<u64>,
5733 pub format: Option<InputFormat>,
5735 pub default: Option<&'a str>,
5737}
5738
5739#[derive(Debug, Clone, Copy)]
5741#[non_exhaustive]
5742pub enum InputFormat {
5743 Email,
5745 Uri,
5747 Date,
5749 DateTime,
5751}
5752
5753impl InputFormat {
5754 pub fn as_str(&self) -> &'static str {
5756 match self {
5757 Self::Email => "email",
5758 Self::Uri => "uri",
5759 Self::Date => "date",
5760 Self::DateTime => "date-time",
5761 }
5762 }
5763}
5764
5765pub use crate::generated::api_types::{
5770 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5771 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5772 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5773 PermissionDecisionApproveOnce, PermissionDecisionContext, PermissionDecisionOutcome,
5774 PermissionDecisionReject, PermissionDecisionSource, PermissionDecisionSurface,
5775 PermissionDecisionUserNotAvailable,
5776};
5777
5778#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5784#[serde(rename_all = "kebab-case")]
5785#[non_exhaustive]
5786pub enum PermissionRequestKind {
5787 Shell,
5789 Write,
5791 Read,
5793 Url,
5795 Mcp,
5797 CustomTool,
5799 Memory,
5801 Hook,
5803 #[serde(other)]
5806 Unknown,
5807}
5808
5809#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5815#[serde(rename_all = "camelCase")]
5816pub struct PermissionRequestData {
5817 #[serde(default, skip_serializing_if = "Option::is_none")]
5821 pub kind: Option<PermissionRequestKind>,
5822 #[serde(default, skip_serializing_if = "Option::is_none")]
5825 pub tool_call_id: Option<String>,
5826 #[serde(default, skip_serializing_if = "Option::is_none")]
5828 pub managed_approval_required: Option<bool>,
5829 #[serde(default, skip_serializing_if = "is_false")]
5831 pub managed_settings_enabled: bool,
5832 #[serde(flatten)]
5836 pub extra: Value,
5837}
5838
5839#[derive(Debug, Clone, Serialize, Deserialize)]
5841#[serde(rename_all = "camelCase")]
5842pub struct ExitPlanModeData {
5843 #[serde(default)]
5845 pub summary: String,
5846 #[serde(default, skip_serializing_if = "Option::is_none")]
5848 pub plan_content: Option<String>,
5849 #[serde(default)]
5851 pub actions: Vec<String>,
5852 #[serde(default = "default_recommended_action")]
5854 pub recommended_action: String,
5855}
5856
5857fn default_recommended_action() -> String {
5858 "autopilot".to_string()
5859}
5860
5861impl Default for ExitPlanModeData {
5862 fn default() -> Self {
5863 Self {
5864 summary: String::new(),
5865 plan_content: None,
5866 actions: Vec::new(),
5867 recommended_action: default_recommended_action(),
5868 }
5869 }
5870}
5871
5872#[cfg(test)]
5873mod tests {
5874 use std::collections::HashMap;
5875 use std::path::PathBuf;
5876
5877 use serde_json::json;
5878
5879 use super::{
5880 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5881 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5882 CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry,
5883 ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
5884 InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
5885 MemoryConfiguration, NamedProviderConfig, ProviderConfig, ProviderModelConfig,
5886 ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, SessionId,
5887 SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
5888 ToolResultResponse, ensure_attachment_display_names,
5889 };
5890 use crate::generated::session_events::TypedSessionEvent;
5891
5892 #[test]
5893 fn tool_builder_composes() {
5894 let tool = Tool::new("greet")
5895 .with_description("Say hello")
5896 .with_namespaced_name("hello/greet")
5897 .with_instructions("Pass the user's name")
5898 .with_parameters(json!({
5899 "type": "object",
5900 "properties": { "name": { "type": "string" } },
5901 "required": ["name"]
5902 }))
5903 .with_overrides_built_in_tool(true)
5904 .with_skip_permission(true);
5905 assert_eq!(tool.name, "greet");
5906 assert_eq!(tool.description, "Say hello");
5907 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5908 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5909 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5910 assert!(tool.overrides_built_in_tool);
5911 assert!(tool.skip_permission);
5912 }
5913
5914 #[test]
5915 fn tool_defer_serialization() {
5916 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5917 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5918 let value = serde_json::to_value(&tool).unwrap();
5919 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5920
5921 let plain = Tool::new("plain");
5922 let value = serde_json::to_value(&plain).unwrap();
5923 assert!(value.get("defer").is_none());
5924 }
5925
5926 #[test]
5927 fn tool_metadata_serialization() {
5928 use indexmap::IndexMap;
5929
5930 let mut metadata = IndexMap::new();
5931 metadata.insert(
5932 "github.com/copilot:safeForTelemetry".to_string(),
5933 json!({ "name": true, "inputsNames": false }),
5934 );
5935 let tool = Tool::new("lookup").with_metadata(metadata);
5936 let value = serde_json::to_value(&tool).unwrap();
5937 assert_eq!(
5938 value
5939 .get("metadata")
5940 .unwrap()
5941 .get("github.com/copilot:safeForTelemetry")
5942 .unwrap(),
5943 &json!({ "name": true, "inputsNames": false })
5944 );
5945
5946 let plain = Tool::new("plain");
5948 let value = serde_json::to_value(&plain).unwrap();
5949 assert!(value.get("metadata").is_none());
5950 }
5951
5952 #[test]
5953 fn custom_agent_config_builder_with_model() {
5954 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5955 .with_model("claude-haiku-4.5")
5956 .with_display_name("My Agent");
5957 assert_eq!(agent.name, "my-agent");
5958 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5959 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5960 }
5961
5962 #[test]
5963 fn custom_agent_config_serializes_model() {
5964 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5965 let wire = serde_json::to_value(&agent).unwrap();
5966 assert_eq!(wire["model"], "claude-haiku-4.5");
5967 assert_eq!(wire["name"], "model-agent");
5968 }
5969
5970 #[test]
5971 fn custom_agent_config_omits_model_when_none() {
5972 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5973 let wire = serde_json::to_value(&agent).unwrap();
5974 assert!(wire.get("model").is_none());
5975 }
5976
5977 #[test]
5978 fn custom_agent_config_builder_with_reasoning_effort() {
5979 let agent =
5980 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5981 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
5982 }
5983
5984 #[test]
5985 fn custom_agent_config_serializes_reasoning_effort() {
5986 let agent =
5987 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
5988 let wire = serde_json::to_value(&agent).unwrap();
5989 assert_eq!(wire["reasoningEffort"], "high");
5990 }
5991
5992 #[test]
5993 fn custom_agent_config_omits_reasoning_effort_when_none() {
5994 let agent = CustomAgentConfig::new("default-agent", "prompt");
5995 let wire = serde_json::to_value(&agent).unwrap();
5996 assert!(wire.get("reasoningEffort").is_none());
5997 }
5998
5999 #[test]
6000 #[should_panic(expected = "tool parameter schema must be a JSON object")]
6001 fn tool_with_parameters_panics_on_non_object_value() {
6002 let _ = Tool::new("noop").with_parameters(json!(null));
6003 }
6004
6005 #[test]
6006 fn tool_result_expanded_serializes_binary_results_for_llm() {
6007 let response = ToolResultResponse {
6008 result: ToolResult::Expanded(ToolResultExpanded {
6009 text_result_for_llm: "rendered chart".to_string(),
6010 result_type: "success".to_string(),
6011 binary_results_for_llm: Some(vec![ToolBinaryResult {
6012 data: "aW1n".to_string(),
6013 mime_type: "image/png".to_string(),
6014 r#type: "image".to_string(),
6015 description: Some("chart preview".to_string()),
6016 }]),
6017 session_log: None,
6018 error: None,
6019 tool_telemetry: None,
6020 tool_references: None,
6021 }),
6022 };
6023
6024 let wire = serde_json::to_value(&response).unwrap();
6025
6026 assert_eq!(
6027 wire,
6028 json!({
6029 "result": {
6030 "textResultForLlm": "rendered chart",
6031 "resultType": "success",
6032 "binaryResultsForLlm": [
6033 {
6034 "data": "aW1n",
6035 "mimeType": "image/png",
6036 "type": "image",
6037 "description": "chart preview"
6038 }
6039 ]
6040 }
6041 })
6042 );
6043 }
6044
6045 #[test]
6046 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
6047 let response = ToolResultResponse {
6048 result: ToolResult::Expanded(ToolResultExpanded {
6049 text_result_for_llm: "ok".to_string(),
6050 result_type: "success".to_string(),
6051 binary_results_for_llm: None,
6052 session_log: None,
6053 error: None,
6054 tool_telemetry: None,
6055 tool_references: None,
6056 }),
6057 };
6058
6059 let wire = serde_json::to_value(&response).unwrap();
6060
6061 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6062 assert!(wire["result"].get("binaryResultsForLlm").is_none());
6063 }
6064
6065 #[test]
6066 fn tool_result_expanded_serializes_tool_references() {
6067 let response = ToolResultResponse {
6068 result: ToolResult::Expanded(
6069 ToolResultExpanded::new("found 2 tools", "success")
6070 .with_tool_references(["get_weather", "check_status"]),
6071 ),
6072 };
6073
6074 let wire = serde_json::to_value(&response).unwrap();
6075
6076 assert_eq!(
6077 wire,
6078 json!({
6079 "result": {
6080 "textResultForLlm": "found 2 tools",
6081 "resultType": "success",
6082 "toolReferences": ["get_weather", "check_status"]
6083 }
6084 })
6085 );
6086 }
6087
6088 #[test]
6089 fn tool_result_expanded_omits_tool_references_when_none() {
6090 let response = ToolResultResponse {
6091 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
6092 };
6093
6094 let wire = serde_json::to_value(&response).unwrap();
6095
6096 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6097 assert!(wire["result"].get("toolReferences").is_none());
6098 }
6099
6100 #[test]
6101 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
6102 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
6105 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
6106
6107 assert_eq!(
6108 expanded.tool_references.as_deref(),
6109 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6110 );
6111 }
6112
6113 #[test]
6114 fn tool_result_expanded_deserializes_tool_references() {
6115 let wire = json!({
6116 "textResultForLlm": "found tools",
6117 "resultType": "success",
6118 "toolReferences": ["alpha", "beta"]
6119 });
6120
6121 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
6122
6123 assert_eq!(
6124 expanded.tool_references.as_deref(),
6125 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6126 );
6127 }
6128
6129 #[test]
6130 fn session_config_default_wire_flags_off_without_handlers() {
6131 let cfg = SessionConfig::default();
6132 assert_eq!(cfg.mcp_oauth_token_storage, None);
6133 let (wire, _runtime) = cfg
6137 .into_wire(Some(SessionId::from("default-flags")))
6138 .expect("default config has no duplicate handlers");
6139 assert!(!wire.request_user_input);
6140 assert!(!wire.request_permission);
6141 assert!(!wire.request_elicitation);
6142 assert!(!wire.request_exit_plan_mode);
6143 assert!(!wire.request_auto_mode_switch);
6144 assert!(!wire.hooks);
6145 assert!(!wire.request_mcp_apps);
6146 }
6147
6148 #[test]
6149 fn resume_session_config_new_wire_flags_off_without_handlers() {
6150 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
6151 assert_eq!(cfg.mcp_oauth_token_storage, None);
6152 let (wire, _runtime) = cfg
6153 .into_wire()
6154 .expect("default resume config has no duplicate handlers");
6155 assert!(!wire.request_user_input);
6156 assert!(!wire.request_permission);
6157 assert!(!wire.request_elicitation);
6158 assert!(!wire.request_exit_plan_mode);
6159 assert!(!wire.request_auto_mode_switch);
6160 assert!(!wire.hooks);
6161 assert!(!wire.request_mcp_apps);
6162 }
6163
6164 #[test]
6165 fn custom_agents_local_only_serializes_on_create_and_resume() {
6166 let (create_wire, _) = SessionConfig::default()
6167 .with_custom_agents_local_only(false)
6168 .into_wire(Some(SessionId::from("create-locality")))
6169 .expect("create config has no duplicate handlers");
6170 let create_json = serde_json::to_value(&create_wire).unwrap();
6171 assert_eq!(create_json["customAgentsLocalOnly"], false);
6172
6173 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
6174 .with_custom_agents_local_only(false)
6175 .into_wire()
6176 .expect("resume config has no duplicate handlers");
6177 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6178 assert_eq!(resume_json["customAgentsLocalOnly"], false);
6179
6180 let (unset_create_wire, _) = SessionConfig::default()
6181 .into_wire(Some(SessionId::from("create-unset")))
6182 .expect("create config has no duplicate handlers");
6183 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6184 assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
6185
6186 let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
6187 .into_wire()
6188 .expect("resume config has no duplicate handlers");
6189 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6190 assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
6191 }
6192
6193 #[test]
6194 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6195 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
6196 assert_eq!(cfg.enable_mcp_apps, Some(true));
6197
6198 let (wire, _runtime) = cfg
6199 .into_wire(Some(SessionId::from("enable-mcp-apps")))
6200 .expect("enable_mcp_apps config has no duplicate handlers");
6201 assert!(wire.request_mcp_apps);
6202
6203 let json = serde_json::to_value(&wire).unwrap();
6204 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6205 }
6206
6207 #[test]
6208 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6209 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
6210 .with_enable_mcp_apps(true);
6211 assert_eq!(cfg.enable_mcp_apps, Some(true));
6212
6213 let (wire, _runtime) = cfg
6214 .into_wire()
6215 .expect("resume enable_mcp_apps config has no duplicate handlers");
6216 assert!(wire.request_mcp_apps);
6217
6218 let json = serde_json::to_value(&wire).unwrap();
6219 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6220 }
6221
6222 #[test]
6223 fn github_mcp_tool_config_serializes_for_create_and_resume() {
6224 let github_config = GitHubMcpToolConfig::new()
6225 .with_enable_all_tools(true)
6226 .with_additional_toolsets(["repos"])
6227 .with_additional_tools(["get_issue"])
6228 .with_enable_insiders_mode(true)
6229 .with_disable_form_deferral(true);
6230
6231 let (create_wire, _) = SessionConfig::default()
6232 .with_github_mcp_tool_config(github_config.clone())
6233 .into_wire(Some(SessionId::from("github-mcp")))
6234 .expect("create config has no duplicate handlers");
6235 assert_eq!(
6236 serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6237 serde_json::json!({
6238 "enableAllTools": true,
6239 "additionalToolsets": ["repos"],
6240 "additionalTools": ["get_issue"],
6241 "enableInsidersMode": true,
6242 "disableFormDeferral": true,
6243 })
6244 );
6245
6246 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6247 .with_github_mcp_tool_config(github_config)
6248 .into_wire()
6249 .expect("resume config has no duplicate handlers");
6250 assert!(resume_wire.github_mcp_tool_config.is_some());
6251
6252 let (unset_wire, _) = SessionConfig::default()
6253 .into_wire(Some(SessionId::from("github-mcp-unset")))
6254 .expect("default config has no duplicate handlers");
6255 assert!(
6256 serde_json::to_value(&unset_wire)
6257 .unwrap()
6258 .get("githubMcpToolConfig")
6259 .is_none()
6260 );
6261 }
6262
6263 #[test]
6264 fn memory_configuration_constructors_and_serde() {
6265 assert!(MemoryConfiguration::enabled().enabled);
6266 assert!(!MemoryConfiguration::disabled().enabled);
6267 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6268
6269 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6270 assert_eq!(json, serde_json::json!({ "enabled": true }));
6271 }
6272
6273 #[test]
6274 fn session_config_with_memory_serializes() {
6275 let (wire, _runtime) = SessionConfig::default()
6276 .with_memory(MemoryConfiguration::enabled())
6277 .into_wire(Some(SessionId::from("memory-on")))
6278 .expect("no duplicate handlers");
6279 let json = serde_json::to_value(&wire).unwrap();
6280 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6281
6282 let (wire_off, _) = SessionConfig::default()
6283 .with_memory(MemoryConfiguration::disabled())
6284 .into_wire(Some(SessionId::from("memory-off")))
6285 .expect("no duplicate handlers");
6286 let json_off = serde_json::to_value(&wire_off).unwrap();
6287 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6288
6289 let (empty_wire, _) = SessionConfig::default()
6291 .into_wire(Some(SessionId::from("memory-unset")))
6292 .expect("no duplicate handlers");
6293 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6294 assert!(empty_json.get("memory").is_none());
6295 }
6296
6297 #[test]
6298 fn resume_session_config_with_memory_serializes() {
6299 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6300 .with_memory(MemoryConfiguration::enabled())
6301 .into_wire()
6302 .expect("no duplicate handlers");
6303 let json = serde_json::to_value(&wire).unwrap();
6304 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6305
6306 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6308 .into_wire()
6309 .expect("no duplicate handlers");
6310 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6311 assert!(empty_json.get("memory").is_none());
6312 }
6313
6314 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6315 CopilotExpAssignmentResponse {
6316 features: vec!["copilot_exp_flag".to_string()],
6317 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6318 configs: vec![ExpConfigEntry {
6319 id: "cfg-1".to_string(),
6320 parameters: HashMap::from([
6321 ("threshold".to_string(), ExpFlagValue::Integer(5)),
6322 ("enabled".to_string(), ExpFlagValue::Bool(true)),
6323 ]),
6324 }],
6325 assignment_context: context.to_string(),
6326 ..Default::default()
6327 }
6328 }
6329
6330 #[test]
6331 fn exp_flag_value_round_trips_all_variants() {
6332 let values = serde_json::json!({
6333 "s": "text",
6334 "i": 7,
6335 "f": 1.5,
6336 "b": true,
6337 "n": null,
6338 });
6339 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6340 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6341 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6342 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6343 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6344 assert_eq!(parsed["n"], ExpFlagValue::Null);
6345 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6346 }
6347
6348 #[test]
6349 fn session_config_with_exp_assignments_serializes() {
6350 let assignments = sample_exp_assignments("ctx-123");
6351 let expected = serde_json::to_value(&assignments).unwrap();
6352 let (wire, _runtime) = SessionConfig::default()
6353 .with_exp_assignments(assignments)
6354 .into_wire(Some(SessionId::from("exp-on")))
6355 .expect("no duplicate handlers");
6356 let json = serde_json::to_value(&wire).unwrap();
6357 assert_eq!(json["expAssignments"], expected);
6358 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6359 assert_eq!(
6360 json["expAssignments"]["Flights"]["copilot_exp_flag"],
6361 "treatment"
6362 );
6363
6364 let (empty_wire, _) = SessionConfig::default()
6366 .into_wire(Some(SessionId::from("exp-unset")))
6367 .expect("no duplicate handlers");
6368 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6369 assert!(empty_json.get("expAssignments").is_none());
6370 }
6371
6372 #[test]
6373 fn resume_session_config_with_exp_assignments_serializes() {
6374 let assignments = sample_exp_assignments("ctx-456");
6375 let expected = serde_json::to_value(&assignments).unwrap();
6376 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6377 .with_exp_assignments(assignments)
6378 .into_wire()
6379 .expect("no duplicate handlers");
6380 let json = serde_json::to_value(&wire).unwrap();
6381 assert_eq!(json["expAssignments"], expected);
6382
6383 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6385 .into_wire()
6386 .expect("no duplicate handlers");
6387 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6388 assert!(empty_json.get("expAssignments").is_none());
6389 }
6390
6391 #[test]
6392 fn session_config_clone_preserves_exp_assignments() {
6393 let assignments = sample_exp_assignments("ctx-clone");
6394 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6395 let cloned = config.clone();
6396
6397 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6398
6399 let (wire, _runtime) = cloned
6400 .into_wire(Some(SessionId::from("exp-clone")))
6401 .expect("no duplicate handlers");
6402 let json = serde_json::to_value(&wire).unwrap();
6403 assert_eq!(
6404 json["expAssignments"],
6405 serde_json::to_value(&assignments).unwrap()
6406 );
6407 }
6408
6409 #[test]
6410 fn resume_session_config_clone_preserves_exp_assignments() {
6411 let assignments = sample_exp_assignments("ctx-clone-resume");
6412 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6413 .with_exp_assignments(assignments.clone());
6414 let cloned = config.clone();
6415
6416 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6417
6418 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6419 let json = serde_json::to_value(&wire).unwrap();
6420 assert_eq!(
6421 json["expAssignments"],
6422 serde_json::to_value(&assignments).unwrap()
6423 );
6424 }
6425
6426 #[test]
6427 #[allow(clippy::field_reassign_with_default)]
6428 fn session_config_into_wire_serializes_bucket_b_fields() {
6429 use std::path::PathBuf;
6430
6431 use super::{CloudSessionOptions, CloudSessionRepository};
6432
6433 let mut cfg = SessionConfig::default();
6434 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6435 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6436 cfg.github_token = Some("ghs_secret".to_string());
6437 cfg.include_sub_agent_streaming_events = Some(false);
6438 cfg.enable_session_telemetry = Some(false);
6439 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
6440 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
6441 cfg.enable_on_demand_instruction_discovery = Some(false);
6442 cfg.cloud = Some(CloudSessionOptions::with_repository(
6443 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
6444 ));
6445
6446 let (wire, _runtime) = cfg
6447 .into_wire(Some(SessionId::from("custom-id")))
6448 .expect("no duplicate handlers");
6449 let wire_json = serde_json::to_value(&wire).unwrap();
6450 assert_eq!(wire_json["sessionId"], "custom-id");
6451 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6452 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6453 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6454 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6455 assert_eq!(wire_json["enableSessionTelemetry"], false);
6456 assert_eq!(wire_json["reasoningSummary"], "concise");
6457 assert_eq!(wire_json["remoteSession"], "export");
6458 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6459 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6460 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6461 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6462
6463 let (empty_wire, _) = SessionConfig::default()
6465 .into_wire(Some(SessionId::from("empty")))
6466 .expect("default has no duplicate handlers");
6467 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6468 assert!(empty_json.get("gitHubToken").is_none());
6469 assert!(empty_json.get("enableSessionTelemetry").is_none());
6470 assert!(empty_json.get("reasoningSummary").is_none());
6471 assert!(empty_json.get("remoteSession").is_none());
6472 assert!(
6473 empty_json
6474 .get("enableOnDemandInstructionDiscovery")
6475 .is_none()
6476 );
6477 assert!(empty_json.get("cloud").is_none());
6478 }
6479
6480 #[test]
6481 fn session_config_into_wire_serializes_named_providers_and_models() {
6482 let cfg = SessionConfig::default()
6483 .with_providers(vec![
6484 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6485 .with_provider_type("openai")
6486 .with_wire_api("responses")
6487 .with_api_key("sk-test"),
6488 ])
6489 .with_models(vec![
6490 ProviderModelConfig::new("gpt-x", "my-openai")
6491 .with_wire_model("gpt-x-2025")
6492 .with_max_output_tokens(2048),
6493 ]);
6494
6495 let (wire, _) = cfg
6496 .into_wire(Some(SessionId::from("sess-providers")))
6497 .expect("no duplicate handlers");
6498 let wire_json = serde_json::to_value(&wire).unwrap();
6499 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6500 assert_eq!(
6501 wire_json["providers"][0]["baseUrl"],
6502 "https://api.example.com/v1"
6503 );
6504 assert_eq!(wire_json["providers"][0]["type"], "openai");
6505 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6506 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6507 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
6508 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
6509 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
6510 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
6511
6512 let (empty_wire, _) = SessionConfig::default()
6513 .into_wire(Some(SessionId::from("empty")))
6514 .expect("default has no duplicate handlers");
6515 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6516 assert!(empty_json.get("providers").is_none());
6517 assert!(empty_json.get("models").is_none());
6518 }
6519
6520 #[test]
6521 fn resume_config_into_wire_serializes_named_providers_and_models() {
6522 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
6523 .with_providers(vec![
6524 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
6525 .with_provider_type("azure")
6526 .with_azure(AzureProviderOptions {
6527 api_version: Some("2024-10-21".to_string()),
6528 }),
6529 ])
6530 .with_models(vec![
6531 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
6532 ]);
6533
6534 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6535 let wire_json = serde_json::to_value(&wire).unwrap();
6536 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
6537 assert_eq!(wire_json["providers"][0]["type"], "azure");
6538 assert_eq!(
6539 wire_json["providers"][0]["azure"]["apiVersion"],
6540 "2024-10-21"
6541 );
6542 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
6543 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
6544 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
6545
6546 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
6547 .into_wire()
6548 .expect("default has no duplicate handlers");
6549 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6550 assert!(empty_json.get("providers").is_none());
6551 assert!(empty_json.get("models").is_none());
6552 }
6553
6554 #[test]
6555 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
6556 use std::path::PathBuf;
6557
6558 let cfg = SessionConfig {
6559 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
6560 disabled_mcp_servers: Some(vec![
6561 "local-files".to_string(),
6562 "remote-github".to_string(),
6563 ]),
6564 large_output: Some(
6565 LargeToolOutputConfig::new()
6566 .with_enabled(true)
6567 .with_max_size_bytes(1024)
6568 .with_output_directory(PathBuf::from("/tmp/large-output")),
6569 ),
6570 ..Default::default()
6571 };
6572
6573 let (wire, _) = cfg
6574 .into_wire(Some(SessionId::from("sess-1")))
6575 .expect("no duplicate handlers");
6576 let wire_json = serde_json::to_value(&wire).unwrap();
6577 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
6578 assert_eq!(
6579 wire_json["disabledMcpServers"],
6580 serde_json::json!(["local-files", "remote-github"])
6581 );
6582 assert_eq!(wire_json["largeOutput"]["enabled"], true);
6583 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
6584 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
6585
6586 let (empty_wire, _) = SessionConfig::default()
6587 .into_wire(Some(SessionId::from("empty")))
6588 .expect("default has no duplicate handlers");
6589 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6590 assert!(empty_json.get("pluginDirectories").is_none());
6591 assert!(empty_json.get("disabledMcpServers").is_none());
6592 assert!(empty_json.get("largeOutput").is_none());
6593 }
6594
6595 #[test]
6596 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
6597 use std::path::PathBuf;
6598
6599 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6600 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6601 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6602 cfg.github_token = Some("ghs_secret".to_string());
6603 cfg.include_sub_agent_streaming_events = Some(true);
6604 cfg.enable_session_telemetry = Some(false);
6605 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
6606 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
6607 cfg.enable_on_demand_instruction_discovery = Some(false);
6608
6609 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6610 let wire_json = serde_json::to_value(&wire).unwrap();
6611 assert_eq!(wire_json["sessionId"], "sess-1");
6612 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6613 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6614 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6615 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
6616 assert_eq!(wire_json["enableSessionTelemetry"], false);
6617 assert_eq!(wire_json["reasoningSummary"], "detailed");
6618 assert_eq!(wire_json["remoteSession"], "on");
6619 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6620
6621 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6623 .into_wire()
6624 .expect("default resume has no duplicate handlers");
6625 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6626 assert!(empty_json.get("reasoningSummary").is_none());
6627 assert!(empty_json.get("remoteSession").is_none());
6628 assert!(
6629 empty_json
6630 .get("enableOnDemandInstructionDiscovery")
6631 .is_none()
6632 );
6633 }
6634
6635 #[test]
6636 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
6637 use std::path::PathBuf;
6638
6639 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6640 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6641 cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]);
6642 cfg.large_output = Some(
6643 LargeToolOutputConfig::new()
6644 .with_enabled(false)
6645 .with_max_size_bytes(2048)
6646 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6647 );
6648
6649 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6650 let wire_json = serde_json::to_value(&wire).unwrap();
6651 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6652 assert_eq!(
6653 wire_json["disabledMcpServers"],
6654 serde_json::json!(["local-files-r"])
6655 );
6656 assert_eq!(wire_json["largeOutput"]["enabled"], false);
6657 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6658 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6659
6660 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6661 .into_wire()
6662 .expect("default resume has no duplicate handlers");
6663 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6664 assert!(empty_json.get("pluginDirectories").is_none());
6665 assert!(empty_json.get("disabledMcpServers").is_none());
6666 assert!(empty_json.get("largeOutput").is_none());
6667 }
6668
6669 #[test]
6670 fn session_config_clones_disabled_mcp_servers() {
6671 let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]);
6672 let mut create_clone = create.clone();
6673 create_clone
6674 .disabled_mcp_servers
6675 .as_mut()
6676 .expect("configured disabled MCP servers")
6677 .push("remote-github".to_string());
6678 assert_eq!(
6679 create.disabled_mcp_servers.as_deref(),
6680 Some(&["local-files".to_string()][..])
6681 );
6682
6683 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6684 .with_disabled_mcp_servers(["local-files"]);
6685 let mut resume_clone = resume.clone();
6686 resume_clone
6687 .disabled_mcp_servers
6688 .as_mut()
6689 .expect("configured disabled MCP servers")
6690 .push("remote-github".to_string());
6691 assert_eq!(
6692 resume.disabled_mcp_servers.as_deref(),
6693 Some(&["local-files".to_string()][..])
6694 );
6695 }
6696
6697 #[test]
6698 fn session_config_builder_composes() {
6699 use indexmap::IndexMap;
6700
6701 let cfg = SessionConfig::default()
6702 .with_session_id(SessionId::from("sess-1"))
6703 .with_model("claude-sonnet-4")
6704 .with_client_name("test-app")
6705 .with_reasoning_effort("medium")
6706 .with_reasoning_summary(ReasoningSummary::Concise)
6707 .with_context_tier("long_context")
6708 .with_streaming(true)
6709 .with_tools([Tool::new("greet")])
6710 .with_available_tools(["bash", "view"])
6711 .with_excluded_tools(["dangerous"])
6712 .with_mcp_servers(IndexMap::new())
6713 .with_mcp_oauth_token_storage("persistent")
6714 .with_enable_config_discovery(true)
6715 .with_enable_on_demand_instruction_discovery(true)
6716 .with_skill_directories([PathBuf::from("/tmp/skills")])
6717 .with_disabled_skills(["broken-skill"])
6718 .with_disabled_mcp_servers(["local-files"])
6719 .with_agent("researcher")
6720 .with_config_directory(PathBuf::from("/tmp/config"))
6721 .with_working_directory(PathBuf::from("/tmp/work"))
6722 .with_additional_directories([PathBuf::from("/tmp/shared")])
6723 .with_github_token("ghp_test")
6724 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6725 .with_enable_session_telemetry(false)
6726 .with_include_sub_agent_streaming_events(false)
6727 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6728
6729 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
6730 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
6731 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6732 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
6733 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
6734 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
6735 assert_eq!(cfg.streaming, Some(true));
6736 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6737 assert_eq!(
6738 cfg.available_tools.as_deref(),
6739 Some(&["bash".to_string(), "view".to_string()][..])
6740 );
6741 assert_eq!(
6742 cfg.excluded_tools.as_deref(),
6743 Some(&["dangerous".to_string()][..])
6744 );
6745 assert!(cfg.mcp_servers.is_some());
6746 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6747 assert_eq!(cfg.enable_config_discovery, Some(true));
6748 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
6749 assert_eq!(
6750 cfg.skill_directories.as_deref(),
6751 Some(&[PathBuf::from("/tmp/skills")][..])
6752 );
6753 assert_eq!(
6754 cfg.disabled_skills.as_deref(),
6755 Some(&["broken-skill".to_string()][..])
6756 );
6757 assert_eq!(
6758 cfg.disabled_mcp_servers.as_deref(),
6759 Some(&["local-files".to_string()][..])
6760 );
6761 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6762 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6763 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6764 assert_eq!(
6765 cfg.additional_directories.as_deref(),
6766 Some(&[PathBuf::from("/tmp/shared")][..])
6767 );
6768 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6769 assert_eq!(
6770 cfg.capi,
6771 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6772 );
6773 assert_eq!(cfg.enable_session_telemetry, Some(false));
6774 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
6775 assert_eq!(
6776 cfg.extension_info,
6777 Some(ExtensionInfo::new("github-app", "counter"))
6778 );
6779 }
6780
6781 #[test]
6782 fn resume_session_config_builder_composes() {
6783 use indexmap::IndexMap;
6784
6785 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
6786 .with_client_name("test-app")
6787 .with_reasoning_summary(ReasoningSummary::None)
6788 .with_context_tier("default")
6789 .with_streaming(true)
6790 .with_tools([Tool::new("greet")])
6791 .with_available_tools(["bash", "view"])
6792 .with_excluded_tools(["dangerous"])
6793 .with_mcp_servers(IndexMap::new())
6794 .with_mcp_oauth_token_storage("persistent")
6795 .with_enable_config_discovery(true)
6796 .with_enable_on_demand_instruction_discovery(false)
6797 .with_skill_directories([PathBuf::from("/tmp/skills")])
6798 .with_disabled_skills(["broken-skill"])
6799 .with_disabled_mcp_servers(["local-files"])
6800 .with_agent("researcher")
6801 .with_config_directory(PathBuf::from("/tmp/config"))
6802 .with_working_directory(PathBuf::from("/tmp/work"))
6803 .with_additional_directories([PathBuf::from("/tmp/shared")])
6804 .with_github_token("ghp_test")
6805 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6806 .with_enable_session_telemetry(false)
6807 .with_include_sub_agent_streaming_events(true)
6808 .with_suppress_resume_event(true)
6809 .with_continue_pending_work(true)
6810 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6811
6812 assert_eq!(cfg.session_id.as_str(), "sess-2");
6813 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6814 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
6815 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
6816 assert_eq!(cfg.streaming, Some(true));
6817 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6818 assert_eq!(
6819 cfg.available_tools.as_deref(),
6820 Some(&["bash".to_string(), "view".to_string()][..])
6821 );
6822 assert_eq!(
6823 cfg.excluded_tools.as_deref(),
6824 Some(&["dangerous".to_string()][..])
6825 );
6826 assert!(cfg.mcp_servers.is_some());
6827 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6828 assert_eq!(cfg.enable_config_discovery, Some(true));
6829 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
6830 assert_eq!(
6831 cfg.skill_directories.as_deref(),
6832 Some(&[PathBuf::from("/tmp/skills")][..])
6833 );
6834 assert_eq!(
6835 cfg.disabled_skills.as_deref(),
6836 Some(&["broken-skill".to_string()][..])
6837 );
6838 assert_eq!(
6839 cfg.disabled_mcp_servers.as_deref(),
6840 Some(&["local-files".to_string()][..])
6841 );
6842 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6843 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6844 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6845 assert_eq!(
6846 cfg.additional_directories.as_deref(),
6847 Some(&[PathBuf::from("/tmp/shared")][..])
6848 );
6849 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6850 assert_eq!(
6851 cfg.capi,
6852 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6853 );
6854 assert_eq!(cfg.enable_session_telemetry, Some(false));
6855 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
6856 assert_eq!(cfg.suppress_resume_event, Some(true));
6857 assert_eq!(cfg.continue_pending_work, Some(true));
6858 assert_eq!(
6859 cfg.extension_info,
6860 Some(ExtensionInfo::new("github-app", "counter"))
6861 );
6862 }
6863
6864 #[test]
6868 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
6869 let cfg =
6870 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
6871 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6872 let json = serde_json::to_value(&wire).unwrap();
6873 assert_eq!(json["continuePendingWork"], true);
6874
6875 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6877 .into_wire()
6878 .expect("no duplicate handlers");
6879 let json = serde_json::to_value(&wire).unwrap();
6880 assert!(json.get("continuePendingWork").is_none());
6881 }
6882
6883 #[test]
6884 fn session_configs_serialize_additional_directories() {
6885 let create = SessionConfig::default().with_additional_directories([
6886 PathBuf::from("/tmp/shared"),
6887 PathBuf::from("/tmp/generated"),
6888 ]);
6889 let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
6890 let create_json = serde_json::to_value(&create_wire).unwrap();
6891 assert_eq!(
6892 create_json["additionalDirectories"],
6893 serde_json::json!(["/tmp/shared", "/tmp/generated"])
6894 );
6895
6896 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6897 .with_additional_directories([PathBuf::from("/tmp/resumed")]);
6898 let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
6899 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6900 assert_eq!(
6901 resume_json["additionalDirectories"],
6902 serde_json::json!(["/tmp/resumed"])
6903 );
6904 }
6905
6906 #[test]
6910 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
6911 let cfg =
6912 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
6913 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6914 let json = serde_json::to_value(&wire).unwrap();
6915 assert_eq!(json["disableResume"], true);
6916 assert!(json.get("suppressResumeEvent").is_none());
6917 }
6918
6919 #[test]
6922 fn session_config_serializes_instruction_directories_to_camel_case() {
6923 let cfg =
6924 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
6925 let (wire, _) = cfg
6926 .into_wire(Some(SessionId::from("instr-on")))
6927 .expect("no duplicate handlers");
6928 let json = serde_json::to_value(&wire).unwrap();
6929 assert_eq!(
6930 json["instructionDirectories"],
6931 serde_json::json!(["/tmp/instr"])
6932 );
6933
6934 let (wire, _) = SessionConfig::default()
6936 .into_wire(Some(SessionId::from("instr-off")))
6937 .expect("no duplicate handlers");
6938 let json = serde_json::to_value(&wire).unwrap();
6939 assert!(json.get("instructionDirectories").is_none());
6940 }
6941
6942 #[test]
6945 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
6946 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
6947 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
6948 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6949 let json = serde_json::to_value(&wire).unwrap();
6950 assert_eq!(
6951 json["instructionDirectories"],
6952 serde_json::json!(["/tmp/instr"])
6953 );
6954
6955 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6956 .into_wire()
6957 .expect("no duplicate handlers");
6958 let json = serde_json::to_value(&wire).unwrap();
6959 assert!(json.get("instructionDirectories").is_none());
6960 }
6961
6962 #[test]
6963 fn custom_agent_config_builder_composes() {
6964 use indexmap::IndexMap;
6965
6966 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
6967 .with_display_name("Research Assistant")
6968 .with_description("Investigates technical questions.")
6969 .with_tools(["bash", "view"])
6970 .with_mcp_servers(IndexMap::new())
6971 .with_infer(true)
6972 .with_skills(["rust-coding-skill"]);
6973
6974 assert_eq!(cfg.name, "researcher");
6975 assert_eq!(cfg.prompt, "You are a research assistant.");
6976 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
6977 assert_eq!(
6978 cfg.description.as_deref(),
6979 Some("Investigates technical questions.")
6980 );
6981 assert_eq!(
6982 cfg.tools.as_deref(),
6983 Some(&["bash".to_string(), "view".to_string()][..])
6984 );
6985 assert!(cfg.mcp_servers.is_some());
6986 assert_eq!(cfg.infer, Some(true));
6987 assert_eq!(
6988 cfg.skills.as_deref(),
6989 Some(&["rust-coding-skill".to_string()][..])
6990 );
6991 }
6992
6993 #[test]
6994 fn mcp_servers_serialize_in_insertion_order() {
6995 use indexmap::IndexMap;
6996
6997 let order = [
7003 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
7004 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
7005 ];
7006 let mut servers = IndexMap::new();
7007 for name in order {
7008 servers.insert(
7009 name.to_string(),
7010 McpServerConfig::Stdio(McpStdioServerConfig {
7011 command: "run".to_string(),
7012 ..Default::default()
7013 }),
7014 );
7015 }
7016
7017 let (wire, _runtime) = SessionConfig::default()
7018 .with_mcp_servers(servers)
7019 .into_wire(None)
7020 .expect("into_wire should succeed");
7021 let json = serde_json::to_string(&wire).expect("serialize wire");
7022
7023 let positions: Vec<usize> = order
7024 .iter()
7025 .map(|name| {
7026 json.find(&format!("\"{name}\""))
7027 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
7028 })
7029 .collect();
7030 let mut ascending = positions.clone();
7031 ascending.sort_unstable();
7032 assert_eq!(
7033 positions, ascending,
7034 "mcp server keys must serialize in insertion order: {json}"
7035 );
7036 }
7037
7038 #[test]
7039 fn infinite_session_config_builder_composes() {
7040 let cfg = InfiniteSessionConfig::new()
7041 .with_enabled(true)
7042 .with_background_compaction_threshold(0.75)
7043 .with_buffer_exhaustion_threshold(0.92);
7044
7045 assert_eq!(cfg.enabled, Some(true));
7046 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
7047 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
7048 }
7049
7050 #[test]
7051 fn provider_config_builder_composes() {
7052 use std::collections::HashMap;
7053
7054 let mut headers = HashMap::new();
7055 headers.insert("X-Custom".to_string(), "value".to_string());
7056
7057 let cfg = ProviderConfig::new("https://api.example.com")
7058 .with_provider_type("openai")
7059 .with_wire_api("completions")
7060 .with_transport("websockets")
7061 .with_api_key("sk-test")
7062 .with_bearer_token("bearer-test")
7063 .with_headers(headers)
7064 .with_model_id("gpt-4")
7065 .with_wire_model("azure-gpt-4-deployment")
7066 .with_max_prompt_tokens(8192)
7067 .with_max_output_tokens(2048);
7068
7069 assert_eq!(cfg.base_url, "https://api.example.com");
7070 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
7071 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
7072 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
7073 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
7074 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
7075 assert_eq!(
7076 cfg.headers
7077 .as_ref()
7078 .and_then(|h| h.get("X-Custom"))
7079 .map(String::as_str),
7080 Some("value"),
7081 );
7082 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
7083 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
7084 assert_eq!(cfg.max_prompt_tokens, Some(8192));
7085 assert_eq!(cfg.max_output_tokens, Some(2048));
7086
7087 let wire = serde_json::to_value(&cfg).unwrap();
7089 assert_eq!(wire["modelId"], "gpt-4");
7090 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
7091 assert_eq!(wire["maxPromptTokens"], 8192);
7092 assert_eq!(wire["maxOutputTokens"], 2048);
7093
7094 let unset = ProviderConfig::new("https://api.example.com");
7095 let wire_unset = serde_json::to_value(&unset).unwrap();
7096 assert!(wire_unset.get("modelId").is_none());
7097 assert!(wire_unset.get("wireModel").is_none());
7098 assert!(wire_unset.get("maxPromptTokens").is_none());
7099 assert!(wire_unset.get("maxOutputTokens").is_none());
7100 }
7101
7102 #[test]
7103 fn capi_session_options_builder_composes_and_serializes() {
7104 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
7105
7106 assert_eq!(cfg.enable_web_socket_responses, Some(false));
7107
7108 let wire = serde_json::to_value(&cfg).unwrap();
7109 assert_eq!(
7110 wire,
7111 serde_json::json!({ "enableWebSocketResponses": false })
7112 );
7113
7114 let unset = CapiSessionOptions::new();
7115 let wire_unset = serde_json::to_value(&unset).unwrap();
7116 assert!(wire_unset.get("enableWebSocketResponses").is_none());
7117 }
7118
7119 #[test]
7120 fn session_config_with_capi_serializes() {
7121 let (wire, _) = SessionConfig::default()
7122 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7123 .into_wire(Some(SessionId::from("capi-create")))
7124 .expect("no duplicate handlers");
7125 let json = serde_json::to_value(&wire).unwrap();
7126 assert_eq!(
7127 json["capi"],
7128 serde_json::json!({ "enableWebSocketResponses": false })
7129 );
7130
7131 let (empty_wire, _) = SessionConfig::default()
7132 .into_wire(Some(SessionId::from("capi-create-unset")))
7133 .expect("no duplicate handlers");
7134 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7135 assert!(empty_json.get("capi").is_none());
7136 }
7137
7138 #[test]
7139 fn resume_session_config_with_capi_serializes() {
7140 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7141 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7142 .into_wire()
7143 .expect("no duplicate handlers");
7144 let json = serde_json::to_value(&wire).unwrap();
7145 assert_eq!(
7146 json["capi"],
7147 serde_json::json!({ "enableWebSocketResponses": false })
7148 );
7149
7150 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
7151 .into_wire()
7152 .expect("no duplicate handlers");
7153 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7154 assert!(empty_json.get("capi").is_none());
7155 }
7156
7157 #[test]
7158 fn system_message_config_builder_composes() {
7159 use std::collections::HashMap;
7160
7161 let cfg = SystemMessageConfig::new()
7162 .with_mode("replace")
7163 .with_content("Custom system message.")
7164 .with_sections(HashMap::new());
7165
7166 assert_eq!(cfg.mode.as_deref(), Some("replace"));
7167 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
7168 assert!(cfg.sections.is_some());
7169 }
7170
7171 #[test]
7172 fn delivery_mode_serializes_to_kebab_case_strings() {
7173 assert_eq!(
7174 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
7175 "\"enqueue\""
7176 );
7177 assert_eq!(
7178 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
7179 "\"immediate\""
7180 );
7181 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
7182 assert_eq!(parsed, DeliveryMode::Immediate);
7183 }
7184
7185 #[test]
7186 fn agent_mode_serializes_to_kebab_case_strings() {
7187 assert_eq!(
7188 serde_json::to_string(&AgentMode::Interactive).unwrap(),
7189 "\"interactive\""
7190 );
7191 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
7192 assert_eq!(
7193 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
7194 "\"autopilot\""
7195 );
7196 assert_eq!(
7197 serde_json::to_string(&AgentMode::Shell).unwrap(),
7198 "\"shell\""
7199 );
7200 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
7201 assert_eq!(parsed, AgentMode::Plan);
7202 }
7203
7204 #[test]
7205 fn connection_state_distinguishes_variants() {
7206 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
7209 }
7210
7211 #[test]
7217 fn session_event_round_trips_agent_id_on_envelope() {
7218 let wire = json!({
7219 "id": "evt-1",
7220 "timestamp": "2026-04-30T12:00:00Z",
7221 "parentId": null,
7222 "agentId": "sub-agent-42",
7223 "type": "assistant.message",
7224 "data": { "message": "hi" }
7225 });
7226
7227 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
7228 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7229
7230 let roundtripped = serde_json::to_value(&event).unwrap();
7232 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7233
7234 let main_agent_event: SessionEvent = serde_json::from_value(json!({
7236 "id": "evt-2",
7237 "timestamp": "2026-04-30T12:00:01Z",
7238 "parentId": null,
7239 "type": "session.idle",
7240 "data": {}
7241 }))
7242 .unwrap();
7243 assert!(main_agent_event.agent_id.is_none());
7244 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
7245 assert!(roundtripped.get("agentId").is_none());
7246 }
7247
7248 #[test]
7250 fn typed_session_event_round_trips_agent_id_on_envelope() {
7251 let wire = json!({
7252 "id": "evt-1",
7253 "timestamp": "2026-04-30T12:00:00Z",
7254 "parentId": null,
7255 "agentId": "sub-agent-42",
7256 "type": "session.idle",
7257 "data": {}
7258 });
7259
7260 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
7261 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7262
7263 let roundtripped = serde_json::to_value(&event).unwrap();
7264 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7265 }
7266
7267 #[test]
7268 fn connection_state_variants_compile() {
7269 let _ = ConnectionState::Disconnected;
7273 let _ = ConnectionState::Connecting;
7274 let _ = ConnectionState::Connected;
7275 let _ = ConnectionState::Error;
7276 }
7277
7278 #[test]
7279 fn deserializes_runtime_attachment_variants() {
7280 let attachments: Vec<Attachment> = serde_json::from_value(json!([
7281 {
7282 "type": "file",
7283 "path": "/tmp/file.rs",
7284 "displayName": "file.rs",
7285 "lineRange": { "start": 7, "end": 12 }
7286 },
7287 {
7288 "type": "directory",
7289 "path": "/tmp/project",
7290 "displayName": "project"
7291 },
7292 {
7293 "type": "selection",
7294 "filePath": "/tmp/lib.rs",
7295 "displayName": "lib.rs",
7296 "text": "fn main() {}",
7297 "selection": {
7298 "start": { "line": 1, "character": 2 },
7299 "end": { "line": 3, "character": 4 }
7300 }
7301 },
7302 {
7303 "type": "blob",
7304 "data": "Zm9v",
7305 "mimeType": "image/png",
7306 "displayName": "image.png"
7307 },
7308 {
7309 "type": "github_reference",
7310 "number": 42,
7311 "title": "Fix rendering",
7312 "referenceType": "issue",
7313 "state": "open",
7314 "url": "https://github.com/example/repo/issues/42"
7315 }
7316 ]))
7317 .expect("attachments should deserialize");
7318
7319 assert_eq!(attachments.len(), 5);
7320 assert!(matches!(
7321 &attachments[0],
7322 Attachment::File {
7323 path,
7324 display_name,
7325 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7326 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7327 ));
7328 assert!(matches!(
7329 &attachments[1],
7330 Attachment::Directory { path, display_name }
7331 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7332 ));
7333 assert!(matches!(
7334 &attachments[2],
7335 Attachment::Selection {
7336 file_path,
7337 display_name,
7338 selection:
7339 AttachmentSelectionRange {
7340 start: AttachmentSelectionPosition { line: 1, character: 2 },
7341 end: AttachmentSelectionPosition { line: 3, character: 4 },
7342 },
7343 ..
7344 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
7345 ));
7346 assert!(matches!(
7347 &attachments[3],
7348 Attachment::Blob {
7349 data,
7350 mime_type,
7351 display_name,
7352 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
7353 ));
7354 assert!(matches!(
7355 &attachments[4],
7356 Attachment::GitHubReference {
7357 number: 42,
7358 title,
7359 reference_type: GitHubReferenceType::Issue,
7360 state,
7361 url,
7362 } if title == "Fix rendering"
7363 && state == "open"
7364 && url == "https://github.com/example/repo/issues/42"
7365 ));
7366 }
7367
7368 #[test]
7369 fn ensures_display_names_for_variants_that_support_them() {
7370 let mut attachments = vec![
7371 Attachment::File {
7372 path: PathBuf::from("/tmp/file.rs"),
7373 display_name: None,
7374 line_range: None,
7375 },
7376 Attachment::Selection {
7377 file_path: PathBuf::from("/tmp/src/lib.rs"),
7378 display_name: None,
7379 text: "fn main() {}".to_string(),
7380 selection: AttachmentSelectionRange {
7381 start: AttachmentSelectionPosition {
7382 line: 0,
7383 character: 0,
7384 },
7385 end: AttachmentSelectionPosition {
7386 line: 0,
7387 character: 10,
7388 },
7389 },
7390 },
7391 Attachment::Blob {
7392 data: "Zm9v".to_string(),
7393 mime_type: "image/png".to_string(),
7394 display_name: None,
7395 },
7396 Attachment::GitHubReference {
7397 number: 7,
7398 title: "Track regressions".to_string(),
7399 reference_type: GitHubReferenceType::Issue,
7400 state: "open".to_string(),
7401 url: "https://example.com/issues/7".to_string(),
7402 },
7403 ];
7404
7405 ensure_attachment_display_names(&mut attachments);
7406
7407 assert_eq!(attachments[0].display_name(), Some("file.rs"));
7408 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
7409 assert_eq!(attachments[2].display_name(), Some("attachment"));
7410 assert_eq!(attachments[3].display_name(), None);
7411 assert_eq!(
7412 attachments[3].label(),
7413 Some("Track regressions".to_string())
7414 );
7415 }
7416
7417 #[test]
7418 fn github_anchored_attachment_variants_round_trip() {
7419 let cases = vec![
7420 (
7421 "github_commit",
7422 json!({
7423 "type": "github_commit",
7424 "message": "Fix the thing",
7425 "oid": "abc123",
7426 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
7427 "url": "https://github.com/octocat/repo/commit/abc123"
7428 }),
7429 ),
7430 (
7431 "github_release",
7432 json!({
7433 "type": "github_release",
7434 "name": "v1.2.3",
7435 "repo": { "name": "repo", "owner": "octocat" },
7436 "tagName": "v1.2.3",
7437 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
7438 }),
7439 ),
7440 (
7441 "github_actions_job",
7442 json!({
7443 "type": "github_actions_job",
7444 "conclusion": "failure",
7445 "jobId": 99,
7446 "jobName": "build",
7447 "repo": { "name": "repo", "owner": "octocat" },
7448 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
7449 "workflowName": "CI"
7450 }),
7451 ),
7452 (
7453 "github_repository",
7454 json!({
7455 "type": "github_repository",
7456 "description": "An example repository",
7457 "ref": "main",
7458 "repo": { "name": "repo", "owner": "octocat" },
7459 "url": "https://github.com/octocat/repo"
7460 }),
7461 ),
7462 (
7463 "github_file_diff",
7464 json!({
7465 "type": "github_file_diff",
7466 "base": {
7467 "path": "src/lib.rs",
7468 "ref": "main",
7469 "repo": { "name": "repo", "owner": "octocat" }
7470 },
7471 "head": {
7472 "path": "src/lib.rs",
7473 "ref": "feature",
7474 "repo": { "name": "repo", "owner": "octocat" }
7475 },
7476 "url": "https://github.com/octocat/repo/compare/main...feature"
7477 }),
7478 ),
7479 (
7480 "github_tree_comparison",
7481 json!({
7482 "type": "github_tree_comparison",
7483 "base": {
7484 "repo": { "name": "repo", "owner": "octocat" },
7485 "revision": "main"
7486 },
7487 "head": {
7488 "repo": { "name": "repo", "owner": "octocat" },
7489 "revision": "feature"
7490 },
7491 "url": "https://github.com/octocat/repo/compare/main...feature"
7492 }),
7493 ),
7494 (
7495 "github_url",
7496 json!({
7497 "type": "github_url",
7498 "url": "https://github.com/octocat/repo/wiki"
7499 }),
7500 ),
7501 (
7502 "github_file",
7503 json!({
7504 "type": "github_file",
7505 "path": "src/main.rs",
7506 "ref": "main",
7507 "repo": { "name": "repo", "owner": "octocat" },
7508 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
7509 }),
7510 ),
7511 (
7512 "github_snippet",
7513 json!({
7514 "type": "github_snippet",
7515 "lineRange": { "start": 10, "end": 20 },
7516 "path": "src/main.rs",
7517 "ref": "main",
7518 "repo": { "name": "repo", "owner": "octocat" },
7519 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
7520 }),
7521 ),
7522 ];
7523
7524 for (expected_type, input) in cases {
7525 let attachment: Attachment = serde_json::from_value(input.clone())
7526 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
7527
7528 let serialized_string = serde_json::to_string(&attachment)
7533 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
7534
7535 assert_eq!(
7537 serialized_string.matches("\"type\":").count(),
7538 1,
7539 "{expected_type} must serialize a single `type` key"
7540 );
7541
7542 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
7543 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
7544 assert_eq!(
7545 serialized.get("type").and_then(|value| value.as_str()),
7546 Some(expected_type),
7547 "{expected_type} must serialize the correct discriminator"
7548 );
7549
7550 assert_eq!(
7552 serialized, input,
7553 "{expected_type} should round-trip without data loss"
7554 );
7555 let reparsed: Attachment = serde_json::from_value(serialized)
7556 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
7557 assert_eq!(
7558 reparsed, attachment,
7559 "{expected_type} should re-deserialize to the same value"
7560 );
7561 }
7562 }
7563}
7564
7565#[cfg(test)]
7566mod permission_builder_tests {
7567 use std::sync::Arc;
7568
7569 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
7570 use crate::permission;
7571 use crate::types::{
7572 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
7573 SessionId,
7574 };
7575
7576 fn data() -> PermissionRequestData {
7577 PermissionRequestData {
7578 extra: serde_json::json!({"tool": "shell"}),
7579 ..Default::default()
7580 }
7581 }
7582
7583 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7586 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7587 }
7588
7589 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7590 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7591 }
7592
7593 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
7594 handler
7595 .handle(SessionId::from("s1"), RequestId::new("1"), data())
7596 .await
7597 }
7598
7599 #[tokio::test]
7600 async fn approve_all_with_handler_present_approves() {
7601 let cfg = SessionConfig::default()
7602 .with_permission_handler(Arc::new(ApproveAllHandler))
7603 .approve_all_permissions();
7604 let h = resolve_create(cfg).expect("policy + handler yields handler");
7605 assert!(matches!(
7606 dispatch(&h).await,
7607 PermissionResult::Decision {
7608 decision: PermissionDecision::ApproveOnce(_),
7609 ..
7610 }
7611 ));
7612 }
7613
7614 #[tokio::test]
7615 async fn approve_all_standalone_produces_handler() {
7616 let cfg = SessionConfig::default().approve_all_permissions();
7617 let h = resolve_create(cfg).expect("policy alone yields handler");
7618 assert!(matches!(
7619 dispatch(&h).await,
7620 PermissionResult::Decision {
7621 decision: PermissionDecision::ApproveOnce(_),
7622 ..
7623 }
7624 ));
7625 }
7626
7627 #[tokio::test]
7630 async fn approve_all_is_order_independent() {
7631 let a = SessionConfig::default()
7632 .with_permission_handler(Arc::new(ApproveAllHandler))
7633 .approve_all_permissions();
7634 let b = SessionConfig::default()
7635 .approve_all_permissions()
7636 .with_permission_handler(Arc::new(ApproveAllHandler));
7637 let ha = resolve_create(a).unwrap();
7638 let hb = resolve_create(b).unwrap();
7639 assert!(matches!(
7640 dispatch(&ha).await,
7641 PermissionResult::Decision {
7642 decision: PermissionDecision::ApproveOnce(_),
7643 ..
7644 }
7645 ));
7646 assert!(matches!(
7647 dispatch(&hb).await,
7648 PermissionResult::Decision {
7649 decision: PermissionDecision::ApproveOnce(_),
7650 ..
7651 }
7652 ));
7653 }
7654
7655 #[tokio::test]
7656 async fn deny_all_is_order_independent() {
7657 let a = SessionConfig::default()
7658 .with_permission_handler(Arc::new(ApproveAllHandler))
7659 .deny_all_permissions();
7660 let b = SessionConfig::default()
7661 .deny_all_permissions()
7662 .with_permission_handler(Arc::new(ApproveAllHandler));
7663 let ha = resolve_create(a).unwrap();
7664 let hb = resolve_create(b).unwrap();
7665 assert!(matches!(
7666 dispatch(&ha).await,
7667 PermissionResult::Decision {
7668 decision: PermissionDecision::Reject(_),
7669 ..
7670 }
7671 ));
7672 assert!(matches!(
7673 dispatch(&hb).await,
7674 PermissionResult::Decision {
7675 decision: PermissionDecision::Reject(_),
7676 ..
7677 }
7678 ));
7679 }
7680
7681 #[tokio::test]
7682 async fn approve_permissions_if_consults_predicate() {
7683 let cfg = SessionConfig::default().approve_permissions_if(|d| {
7684 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7685 });
7686 let h = resolve_create(cfg).unwrap();
7687 assert!(matches!(
7688 dispatch(&h).await,
7689 PermissionResult::Decision {
7690 decision: PermissionDecision::Reject(_),
7691 ..
7692 }
7693 ));
7694 }
7695
7696 #[tokio::test]
7697 async fn approve_permissions_if_is_order_independent() {
7698 let predicate = |d: &PermissionRequestData| {
7699 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7700 };
7701 let a = SessionConfig::default()
7702 .with_permission_handler(Arc::new(ApproveAllHandler))
7703 .approve_permissions_if(predicate);
7704 let b = SessionConfig::default()
7705 .approve_permissions_if(predicate)
7706 .with_permission_handler(Arc::new(ApproveAllHandler));
7707 let ha = resolve_create(a).unwrap();
7708 let hb = resolve_create(b).unwrap();
7709 assert!(matches!(
7710 dispatch(&ha).await,
7711 PermissionResult::Decision {
7712 decision: PermissionDecision::Reject(_),
7713 ..
7714 }
7715 ));
7716 assert!(matches!(
7717 dispatch(&hb).await,
7718 PermissionResult::Decision {
7719 decision: PermissionDecision::Reject(_),
7720 ..
7721 }
7722 ));
7723 }
7724
7725 #[tokio::test]
7726 async fn resume_session_config_approve_all_works() {
7727 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
7728 .with_permission_handler(Arc::new(ApproveAllHandler))
7729 .approve_all_permissions();
7730 let h = resolve_resume(cfg).unwrap();
7731 assert!(matches!(
7732 dispatch(&h).await,
7733 PermissionResult::Decision {
7734 decision: PermissionDecision::ApproveOnce(_),
7735 ..
7736 }
7737 ));
7738 }
7739
7740 #[tokio::test]
7741 async fn resume_session_config_approve_all_is_order_independent() {
7742 let a = ResumeSessionConfig::new(SessionId::from("s1"))
7743 .with_permission_handler(Arc::new(ApproveAllHandler))
7744 .approve_all_permissions();
7745 let b = ResumeSessionConfig::new(SessionId::from("s1"))
7746 .approve_all_permissions()
7747 .with_permission_handler(Arc::new(ApproveAllHandler));
7748 let ha = resolve_resume(a).unwrap();
7749 let hb = resolve_resume(b).unwrap();
7750 assert!(matches!(
7751 dispatch(&ha).await,
7752 PermissionResult::Decision {
7753 decision: PermissionDecision::ApproveOnce(_),
7754 ..
7755 }
7756 ));
7757 assert!(matches!(
7758 dispatch(&hb).await,
7759 PermissionResult::Decision {
7760 decision: PermissionDecision::ApproveOnce(_),
7761 ..
7762 }
7763 ));
7764 }
7765
7766 #[test]
7767 fn session_config_enable_experimental_mode_serializes_when_set() {
7768 let cfg = SessionConfig::default().with_enable_experimental_mode(false);
7769 assert_eq!(cfg.enable_experimental_mode, Some(false));
7770
7771 let (wire, _runtime) = cfg
7772 .into_wire(Some(SessionId::from("experimental-mode")))
7773 .expect("enable_experimental_mode config has no duplicate handlers");
7774 assert_eq!(wire.is_experimental_mode, Some(false));
7775
7776 let json = serde_json::to_value(&wire).unwrap();
7777 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
7778 }
7779
7780 #[test]
7781 fn session_config_enable_experimental_mode_omitted_when_none() {
7782 let cfg = SessionConfig::default();
7783 assert_eq!(cfg.enable_experimental_mode, None);
7784
7785 let (wire, _runtime) = cfg
7786 .into_wire(Some(SessionId::from("no-experimental-mode")))
7787 .expect("default config has no duplicate handlers");
7788 assert_eq!(wire.is_experimental_mode, None);
7789
7790 let json = serde_json::to_value(&wire).unwrap();
7791 assert!(json.get("isExperimentalMode").is_none());
7792 }
7793
7794 #[test]
7795 fn resume_session_config_enable_experimental_mode_serializes_when_set() {
7796 let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
7797 .with_enable_experimental_mode(false);
7798 assert_eq!(cfg.enable_experimental_mode, Some(false));
7799
7800 let (wire, _runtime) = cfg
7801 .into_wire()
7802 .expect("resume 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 resume_session_config_enable_experimental_mode_omitted_when_none() {
7811 let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
7812 assert_eq!(cfg.enable_experimental_mode, None);
7813
7814 let (wire, _runtime) = cfg
7815 .into_wire()
7816 .expect("default resume 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
7824#[cfg(test)]
7825mod is_terminal_tests {
7826 use super::Tool;
7827
7828 #[test]
7829 fn is_terminal_serializes_as_camel_case_when_set() {
7830 let tool = Tool {
7831 name: "clear_context".to_owned(),
7832 is_terminal: true,
7833 ..Default::default()
7834 };
7835 let value = serde_json::to_value(&tool).expect("tool serializes");
7836 assert_eq!(
7837 value.get("isTerminal"),
7838 Some(&serde_json::Value::Bool(true))
7839 );
7840 }
7841
7842 #[test]
7843 fn is_terminal_is_omitted_when_false() {
7844 let tool = Tool {
7845 name: "plain".to_owned(),
7846 ..Default::default()
7847 };
7848 let value = serde_json::to_value(&tool).expect("tool serializes");
7849 assert!(value.get("isTerminal").is_none());
7850 }
7851
7852 #[test]
7855 fn is_terminal_appears_in_debug_output() {
7856 let terminal = Tool {
7857 name: "clear_context".to_owned(),
7858 is_terminal: true,
7859 ..Default::default()
7860 };
7861 assert!(format!("{terminal:?}").contains("is_terminal: true"));
7862
7863 let plain = Tool {
7864 name: "plain".to_owned(),
7865 ..Default::default()
7866 };
7867 assert!(format!("{plain:?}").contains("is_terminal: false"));
7868 }
7869}