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};
24pub use crate::generated::session_events::AutoTier;
26use crate::generated::session_events::ReasoningSummary;
27pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig};
29use crate::github_token::GitHubTokenProvider;
30use crate::handler::{
31 AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler,
32 PermissionHandler, UserInputHandler,
33};
34use crate::hooks::SessionHooks;
35use crate::provider_token::BearerTokenProvider;
36pub use crate::session_fs::{
37 DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig,
38 SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult,
39 SessionFsSqliteQueryType, SessionFsSqliteTransactionError,
40 SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionStatement,
41};
42pub use crate::trace_context::{TraceContext, TraceContextProvider};
43use crate::transforms::SystemMessageTransform;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48#[allow(dead_code)]
49#[non_exhaustive]
50pub(crate) enum ConnectionState {
51 Disconnected,
53 Connecting,
55 Connected,
57 Error,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
66#[non_exhaustive]
67pub enum SessionLifecycleEventType {
68 #[serde(rename = "session.created")]
70 Created,
71 #[serde(rename = "session.deleted")]
73 Deleted,
74 #[serde(rename = "session.updated")]
76 Updated,
77 #[serde(rename = "session.foreground")]
79 Foreground,
80 #[serde(rename = "session.background")]
82 Background,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct SessionLifecycleEventMetadata {
88 #[serde(rename = "startTime")]
90 pub start_time: String,
91 #[serde(rename = "modifiedTime")]
93 pub modified_time: String,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub summary: Option<String>,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct SessionLifecycleEvent {
103 #[serde(rename = "type")]
105 pub event_type: SessionLifecycleEventType,
106 #[serde(rename = "sessionId")]
108 pub session_id: SessionId,
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub metadata: Option<SessionLifecycleEventMetadata>,
112}
113
114#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
120#[serde(transparent)]
121pub struct SessionId(String);
122
123impl SessionId {
124 pub fn new(id: impl Into<String>) -> Self {
126 Self(id.into())
127 }
128
129 pub fn as_str(&self) -> &str {
131 &self.0
132 }
133
134 pub fn into_inner(self) -> String {
136 self.0
137 }
138}
139
140impl std::ops::Deref for SessionId {
141 type Target = str;
142
143 fn deref(&self) -> &str {
144 &self.0
145 }
146}
147
148impl std::fmt::Display for SessionId {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 f.write_str(&self.0)
151 }
152}
153
154impl From<String> for SessionId {
155 fn from(s: String) -> Self {
156 Self(s)
157 }
158}
159
160impl From<&str> for SessionId {
161 fn from(s: &str) -> Self {
162 Self(s.to_owned())
163 }
164}
165
166impl AsRef<str> for SessionId {
167 fn as_ref(&self) -> &str {
168 &self.0
169 }
170}
171
172impl std::borrow::Borrow<str> for SessionId {
173 fn borrow(&self) -> &str {
174 &self.0
175 }
176}
177
178impl From<SessionId> for String {
179 fn from(id: SessionId) -> String {
180 id.0
181 }
182}
183
184impl PartialEq<str> for SessionId {
185 fn eq(&self, other: &str) -> bool {
186 self.0 == other
187 }
188}
189
190impl PartialEq<String> for SessionId {
191 fn eq(&self, other: &String) -> bool {
192 &self.0 == other
193 }
194}
195
196impl PartialEq<SessionId> for String {
197 fn eq(&self, other: &SessionId) -> bool {
198 self == &other.0
199 }
200}
201
202impl PartialEq<&str> for SessionId {
203 fn eq(&self, other: &&str) -> bool {
204 self.0 == *other
205 }
206}
207
208impl PartialEq<&SessionId> for SessionId {
209 fn eq(&self, other: &&SessionId) -> bool {
210 self.0 == other.0
211 }
212}
213
214impl PartialEq<SessionId> for &SessionId {
215 fn eq(&self, other: &SessionId) -> bool {
216 self.0 == other.0
217 }
218}
219
220#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
226#[serde(transparent)]
227pub struct RequestId(String);
228
229impl RequestId {
230 pub fn new(id: impl Into<String>) -> Self {
232 Self(id.into())
233 }
234
235 pub fn into_inner(self) -> String {
237 self.0
238 }
239}
240
241impl std::ops::Deref for RequestId {
242 type Target = str;
243
244 fn deref(&self) -> &str {
245 &self.0
246 }
247}
248
249impl std::fmt::Display for RequestId {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 f.write_str(&self.0)
252 }
253}
254
255impl From<String> for RequestId {
256 fn from(s: String) -> Self {
257 Self(s)
258 }
259}
260
261impl From<&str> for RequestId {
262 fn from(s: &str) -> Self {
263 Self(s.to_owned())
264 }
265}
266
267impl AsRef<str> for RequestId {
268 fn as_ref(&self) -> &str {
269 &self.0
270 }
271}
272
273impl std::borrow::Borrow<str> for RequestId {
274 fn borrow(&self) -> &str {
275 &self.0
276 }
277}
278
279impl From<RequestId> for String {
280 fn from(id: RequestId) -> String {
281 id.0
282 }
283}
284
285impl PartialEq<str> for RequestId {
286 fn eq(&self, other: &str) -> bool {
287 self.0 == other
288 }
289}
290
291impl PartialEq<String> for RequestId {
292 fn eq(&self, other: &String) -> bool {
293 &self.0 == other
294 }
295}
296
297impl PartialEq<RequestId> for String {
298 fn eq(&self, other: &RequestId) -> bool {
299 self == &other.0
300 }
301}
302
303impl PartialEq<&str> for RequestId {
304 fn eq(&self, other: &&str) -> bool {
305 self.0 == *other
306 }
307}
308
309#[derive(Clone, Default, Serialize, Deserialize)]
324#[serde(rename_all = "camelCase")]
325#[non_exhaustive]
326pub struct Tool {
327 pub name: String,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub namespaced_name: Option<String>,
333 #[serde(default)]
335 pub description: String,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
338 pub instructions: Option<String>,
339 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
341 pub parameters: IndexMap<String, Value>,
342 #[serde(default, skip_serializing_if = "is_false")]
346 pub overrides_built_in_tool: bool,
347 #[serde(default, skip_serializing_if = "is_false")]
351 pub skip_permission: bool,
352 #[serde(default, skip_serializing_if = "is_false")]
357 pub is_terminal: bool,
358 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub defer: Option<DeferMode>,
365 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
370 pub metadata: IndexMap<String, Value>,
371 #[serde(skip)]
383 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
384}
385
386#[inline]
387fn is_false(b: &bool) -> bool {
388 !*b
389}
390
391#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
394#[serde(rename_all = "lowercase")]
395pub enum DeferMode {
396 Auto,
398 Never,
400}
401
402impl Tool {
403 pub fn new(name: impl Into<String>) -> Self {
423 Self {
424 name: name.into(),
425 ..Default::default()
426 }
427 }
428
429 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
432 self.namespaced_name = Some(namespaced_name.into());
433 self
434 }
435
436 pub fn with_description(mut self, description: impl Into<String>) -> Self {
438 self.description = description.into();
439 self
440 }
441
442 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
444 self.instructions = Some(instructions.into());
445 self
446 }
447
448 pub fn with_parameters(mut self, parameters: Value) -> Self {
462 self.parameters = crate::tool::tool_parameters(parameters);
463 self
464 }
465
466 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
470 self.overrides_built_in_tool = overrides;
471 self
472 }
473
474 pub fn with_skip_permission(mut self, skip: bool) -> Self {
478 self.skip_permission = skip;
479 self
480 }
481
482 #[must_use]
489 pub fn with_is_terminal(mut self, is_terminal: bool) -> Self {
490 self.is_terminal = is_terminal;
491 self
492 }
493
494 pub fn with_defer(mut self, defer: DeferMode) -> Self {
498 self.defer = Some(defer);
499 self
500 }
501
502 pub fn with_metadata(mut self, metadata: IndexMap<String, Value>) -> Self {
505 self.metadata = metadata;
506 self
507 }
508
509 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
513 self.handler = Some(handler);
514 self
515 }
516
517 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
522 self.handler.as_ref()
523 }
524}
525
526impl std::fmt::Debug for Tool {
527 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528 f.debug_struct("Tool")
529 .field("name", &self.name)
530 .field("namespaced_name", &self.namespaced_name)
531 .field("description", &self.description)
532 .field("instructions", &self.instructions)
533 .field("parameters", &self.parameters)
534 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
535 .field("skip_permission", &self.skip_permission)
536 .field("is_terminal", &self.is_terminal)
537 .field("defer", &self.defer)
538 .field("metadata", &self.metadata)
539 .field(
540 "handler",
541 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
542 )
543 .finish()
544 }
545}
546
547#[non_exhaustive]
550#[derive(Debug, Clone)]
551pub struct CommandContext {
552 pub session_id: SessionId,
554 pub command: String,
556 pub command_name: String,
558 pub args: String,
560}
561
562#[async_trait::async_trait]
568pub trait CommandHandler: Send + Sync {
569 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
571}
572
573#[non_exhaustive]
579#[derive(Clone)]
580pub struct CommandDefinition {
581 pub name: String,
583 pub description: Option<String>,
585 pub handler: Arc<dyn CommandHandler>,
587}
588
589impl CommandDefinition {
590 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
593 Self {
594 name: name.into(),
595 description: None,
596 handler,
597 }
598 }
599
600 pub fn with_description(mut self, description: impl Into<String>) -> Self {
602 self.description = Some(description.into());
603 self
604 }
605}
606
607impl std::fmt::Debug for CommandDefinition {
608 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
609 f.debug_struct("CommandDefinition")
610 .field("name", &self.name)
611 .field("description", &self.description)
612 .field("handler", &"<set>")
613 .finish()
614 }
615}
616
617impl Serialize for CommandDefinition {
618 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
619 use serde::ser::SerializeStruct;
620 let mut state = serializer.serialize_struct("CommandDefinition", 2)?;
621 state.serialize_field("name", &self.name)?;
622 state.serialize_field("description", self.description.as_deref().unwrap_or(""))?;
623 state.end()
624 }
625}
626
627#[derive(Debug, Clone, Default, Serialize, Deserialize)]
634#[serde(rename_all = "camelCase")]
635#[non_exhaustive]
636pub struct CustomAgentConfig {
637 pub name: String,
639 #[serde(default, skip_serializing_if = "Option::is_none")]
641 pub display_name: Option<String>,
642 #[serde(default, skip_serializing_if = "Option::is_none")]
644 pub description: Option<String>,
645 #[serde(default, skip_serializing_if = "Option::is_none")]
647 pub tools: Option<Vec<String>>,
648 pub prompt: String,
650 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub infer: Option<bool>,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub skills: Option<Vec<String>>,
659 #[serde(default, skip_serializing_if = "Option::is_none")]
664 pub model: Option<String>,
665 #[serde(default, skip_serializing_if = "Option::is_none")]
670 pub reasoning_effort: Option<String>,
671}
672
673impl CustomAgentConfig {
674 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
681 Self {
682 name: name.into(),
683 prompt: prompt.into(),
684 ..Self::default()
685 }
686 }
687
688 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
690 self.display_name = Some(display_name.into());
691 self
692 }
693
694 pub fn with_description(mut self, description: impl Into<String>) -> Self {
696 self.description = Some(description.into());
697 self
698 }
699
700 pub fn with_tools<I, S>(mut self, tools: I) -> Self
703 where
704 I: IntoIterator<Item = S>,
705 S: Into<String>,
706 {
707 self.tools = Some(tools.into_iter().map(Into::into).collect());
708 self
709 }
710
711 pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
713 self.mcp_servers = Some(mcp_servers);
714 self
715 }
716
717 pub fn with_infer(mut self, infer: bool) -> Self {
719 self.infer = Some(infer);
720 self
721 }
722
723 pub fn with_skills<I, S>(mut self, skills: I) -> Self
725 where
726 I: IntoIterator<Item = S>,
727 S: Into<String>,
728 {
729 self.skills = Some(skills.into_iter().map(Into::into).collect());
730 self
731 }
732
733 pub fn with_model(mut self, model: impl Into<String>) -> Self {
735 self.model = Some(model.into());
736 self
737 }
738
739 pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into<String>) -> Self {
741 self.reasoning_effort = Some(reasoning_effort.into());
742 self
743 }
744}
745
746#[derive(Debug, Clone, Default, Serialize, Deserialize)]
753#[serde(rename_all = "camelCase")]
754pub struct DefaultAgentConfig {
755 #[serde(default, skip_serializing_if = "Option::is_none")]
757 pub excluded_tools: Option<Vec<String>>,
758}
759
760#[derive(Debug, Clone, Default, Serialize, Deserialize)]
766#[serde(rename_all = "camelCase")]
767#[non_exhaustive]
768pub struct LargeToolOutputConfig {
769 #[serde(default, skip_serializing_if = "Option::is_none")]
771 pub enabled: Option<bool>,
772 #[serde(default, skip_serializing_if = "Option::is_none")]
775 pub max_size_bytes: Option<u64>,
776 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
779 pub output_directory: Option<PathBuf>,
780}
781
782impl LargeToolOutputConfig {
783 pub fn new() -> Self {
786 Self::default()
787 }
788
789 pub fn with_enabled(mut self, enabled: bool) -> Self {
791 self.enabled = Some(enabled);
792 self
793 }
794
795 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
797 self.max_size_bytes = Some(max_size_bytes);
798 self
799 }
800
801 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
803 self.output_directory = Some(output_directory.into());
804 self
805 }
806}
807
808#[derive(Debug, Clone, Default, Serialize, Deserialize)]
814#[serde(rename_all = "camelCase")]
815#[non_exhaustive]
816pub struct ToolSearchConfig {
817 #[serde(default, skip_serializing_if = "Option::is_none")]
819 pub enabled: Option<bool>,
820 #[serde(default, skip_serializing_if = "Option::is_none")]
823 pub defer_threshold: Option<u32>,
824}
825
826impl ToolSearchConfig {
827 pub fn new() -> Self {
830 Self::default()
831 }
832
833 pub fn with_enabled(mut self, enabled: bool) -> Self {
835 self.enabled = Some(enabled);
836 self
837 }
838
839 pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
842 self.defer_threshold = Some(defer_threshold);
843 self
844 }
845}
846
847#[derive(Debug, Clone, Default, Serialize, Deserialize)]
852#[serde(rename_all = "camelCase")]
853#[non_exhaustive]
854pub struct GitHubMcpToolConfig {
855 #[serde(default, skip_serializing_if = "Option::is_none")]
857 pub enable_all_tools: Option<bool>,
858 #[serde(default, skip_serializing_if = "Option::is_none")]
860 pub additional_toolsets: Option<Vec<String>>,
861 #[serde(default, skip_serializing_if = "Option::is_none")]
863 pub additional_tools: Option<Vec<String>>,
864 #[serde(default, skip_serializing_if = "Option::is_none")]
866 pub enable_insiders_mode: Option<bool>,
867 #[serde(default, skip_serializing_if = "Option::is_none")]
871 pub disable_form_deferral: Option<bool>,
872}
873
874impl GitHubMcpToolConfig {
875 pub fn new() -> Self {
877 Self::default()
878 }
879
880 pub fn with_enable_all_tools(mut self, value: bool) -> Self {
882 self.enable_all_tools = Some(value);
883 self
884 }
885
886 pub fn with_additional_toolsets<I, S>(mut self, values: I) -> Self
888 where
889 I: IntoIterator<Item = S>,
890 S: Into<String>,
891 {
892 self.additional_toolsets = Some(values.into_iter().map(Into::into).collect());
893 self
894 }
895
896 pub fn with_additional_tools<I, S>(mut self, values: I) -> Self
898 where
899 I: IntoIterator<Item = S>,
900 S: Into<String>,
901 {
902 self.additional_tools = Some(values.into_iter().map(Into::into).collect());
903 self
904 }
905
906 pub fn with_enable_insiders_mode(mut self, value: bool) -> Self {
908 self.enable_insiders_mode = Some(value);
909 self
910 }
911
912 pub fn with_disable_form_deferral(mut self, value: bool) -> Self {
916 self.disable_form_deferral = Some(value);
917 self
918 }
919}
920
921#[derive(Debug, Clone, Default, Serialize, Deserialize)]
928#[serde(rename_all = "camelCase")]
929#[non_exhaustive]
930pub struct InfiniteSessionConfig {
931 #[serde(default, skip_serializing_if = "Option::is_none")]
933 pub enabled: Option<bool>,
934 #[serde(default, skip_serializing_if = "Option::is_none")]
937 pub background_compaction_threshold: Option<f64>,
938 #[serde(default, skip_serializing_if = "Option::is_none")]
941 pub buffer_exhaustion_threshold: Option<f64>,
942}
943
944impl InfiniteSessionConfig {
945 pub fn new() -> Self {
948 Self::default()
949 }
950
951 pub fn with_enabled(mut self, enabled: bool) -> Self {
954 self.enabled = Some(enabled);
955 self
956 }
957
958 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
961 self.background_compaction_threshold = Some(threshold);
962 self
963 }
964
965 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
968 self.buffer_exhaustion_threshold = Some(threshold);
969 self
970 }
971}
972
973#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
984#[serde(rename_all = "camelCase")]
985#[non_exhaustive]
986pub struct MemoryConfiguration {
987 pub enabled: bool,
989}
990
991impl MemoryConfiguration {
992 pub fn enabled() -> Self {
994 Self { enabled: true }
995 }
996
997 pub fn disabled() -> Self {
999 Self { enabled: false }
1000 }
1001
1002 pub fn with_enabled(mut self, enabled: bool) -> Self {
1004 self.enabled = enabled;
1005 self
1006 }
1007}
1008
1009#[derive(Debug, Clone, Serialize, Deserialize)]
1011#[serde(rename_all = "camelCase")]
1012#[non_exhaustive]
1013pub struct CloudSessionRepository {
1014 pub owner: String,
1016 pub name: String,
1018 #[serde(skip_serializing_if = "Option::is_none")]
1020 pub branch: Option<String>,
1021}
1022
1023impl CloudSessionRepository {
1024 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
1026 Self {
1027 owner: owner.into(),
1028 name: name.into(),
1029 branch: None,
1030 }
1031 }
1032
1033 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
1035 self.branch = Some(branch.into());
1036 self
1037 }
1038}
1039
1040#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1042#[serde(rename_all = "camelCase")]
1043#[non_exhaustive]
1044pub struct CloudSessionOptions {
1045 #[serde(skip_serializing_if = "Option::is_none")]
1047 pub repository: Option<CloudSessionRepository>,
1048}
1049
1050impl CloudSessionOptions {
1051 pub fn with_repository(repository: CloudSessionRepository) -> Self {
1053 Self {
1054 repository: Some(repository),
1055 }
1056 }
1057}
1058
1059#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1061#[serde(rename_all = "camelCase")]
1062pub struct ExtensionInfo {
1063 pub source: String,
1065 pub name: String,
1067}
1068
1069impl ExtensionInfo {
1070 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
1072 Self {
1073 source: source.into(),
1074 name: name.into(),
1075 }
1076 }
1077}
1078
1079#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1090#[serde(rename_all = "camelCase")]
1091pub struct CanvasProviderIdentity {
1092 pub id: String,
1094 #[serde(skip_serializing_if = "Option::is_none")]
1096 pub name: Option<String>,
1097}
1098
1099impl CanvasProviderIdentity {
1100 pub fn new(id: impl Into<String>) -> Self {
1102 Self {
1103 id: id.into(),
1104 name: None,
1105 }
1106 }
1107
1108 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1110 self.name = Some(name.into());
1111 self
1112 }
1113}
1114
1115#[derive(Debug, Clone, Serialize, Deserialize)]
1149#[serde(tag = "type", rename_all = "lowercase")]
1150#[non_exhaustive]
1151pub enum McpServerConfig {
1152 #[serde(alias = "local")]
1156 Stdio(McpStdioServerConfig),
1157 Http(McpHttpServerConfig),
1159 Sse(McpHttpServerConfig),
1161}
1162
1163#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1167#[serde(rename_all = "camelCase")]
1168pub struct McpStdioServerConfig {
1169 #[serde(default, skip_serializing_if = "Option::is_none")]
1175 pub tools: Option<Vec<String>>,
1176 #[serde(default, skip_serializing_if = "Option::is_none")]
1178 pub timeout: Option<i64>,
1179 pub command: String,
1181 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1183 pub args: Vec<String>,
1184 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1187 pub env: HashMap<String, String>,
1188 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1190 pub working_directory: Option<String>,
1191}
1192
1193#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1197#[serde(rename_all = "camelCase")]
1198pub struct McpHttpServerConfig {
1199 #[serde(default, skip_serializing_if = "Option::is_none")]
1205 pub tools: Option<Vec<String>>,
1206 #[serde(default, skip_serializing_if = "Option::is_none")]
1208 pub timeout: Option<i64>,
1209 pub url: String,
1211 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1213 pub headers: HashMap<String, String>,
1214}
1215
1216#[derive(Clone, Default, Serialize, Deserialize)]
1222#[serde(rename_all = "camelCase")]
1223#[non_exhaustive]
1224pub struct ProviderConfig {
1225 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1228 pub provider_type: Option<String>,
1229 #[serde(default, skip_serializing_if = "Option::is_none")]
1232 pub wire_api: Option<String>,
1233 #[serde(default, skip_serializing_if = "Option::is_none")]
1238 pub transport: Option<String>,
1239 pub base_url: String,
1241 #[serde(default, skip_serializing_if = "Option::is_none")]
1243 pub api_key: Option<String>,
1244 #[serde(default, skip_serializing_if = "Option::is_none")]
1248 pub bearer_token: Option<String>,
1249 #[serde(skip)]
1252 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1253 #[serde(default, skip_serializing_if = "Option::is_none")]
1254 pub(crate) has_bearer_token_provider: Option<bool>,
1255 #[serde(default, skip_serializing_if = "Option::is_none")]
1257 pub azure: Option<AzureProviderOptions>,
1258 #[serde(default, skip_serializing_if = "Option::is_none")]
1260 pub headers: Option<HashMap<String, String>>,
1261 #[serde(default, skip_serializing_if = "Option::is_none")]
1265 pub model_id: Option<String>,
1266 #[serde(default, skip_serializing_if = "Option::is_none")]
1273 pub wire_model: Option<String>,
1274 #[serde(default, skip_serializing_if = "Option::is_none")]
1279 pub max_prompt_tokens: Option<i64>,
1280 #[serde(default, skip_serializing_if = "Option::is_none")]
1283 pub max_output_tokens: Option<i64>,
1284}
1285
1286impl std::fmt::Debug for ProviderConfig {
1287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1288 f.debug_struct("ProviderConfig")
1289 .field("provider_type", &self.provider_type)
1290 .field("wire_api", &self.wire_api)
1291 .field("transport", &self.transport)
1292 .field("base_url", &self.base_url)
1293 .field("api_key", &self.api_key)
1294 .field("bearer_token", &self.bearer_token)
1295 .field(
1296 "bearer_token_provider",
1297 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1298 )
1299 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1300 .field("azure", &self.azure)
1301 .field("headers", &self.headers)
1302 .field("model_id", &self.model_id)
1303 .field("wire_model", &self.wire_model)
1304 .field("max_prompt_tokens", &self.max_prompt_tokens)
1305 .field("max_output_tokens", &self.max_output_tokens)
1306 .finish()
1307 }
1308}
1309
1310impl ProviderConfig {
1311 pub fn new(base_url: impl Into<String>) -> Self {
1314 Self {
1315 base_url: base_url.into(),
1316 ..Self::default()
1317 }
1318 }
1319
1320 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1322 self.provider_type = Some(provider_type.into());
1323 self
1324 }
1325
1326 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1328 self.wire_api = Some(wire_api.into());
1329 self
1330 }
1331
1332 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1335 self.transport = Some(transport.into());
1336 self
1337 }
1338
1339 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1341 self.api_key = Some(api_key.into());
1342 self
1343 }
1344
1345 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1348 self.bearer_token = Some(bearer_token.into());
1349 self
1350 }
1351
1352 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1358 self.bearer_token_provider = Some(provider);
1359 self
1360 }
1361
1362 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1364 self.azure = Some(azure);
1365 self
1366 }
1367
1368 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1370 self.headers = Some(headers);
1371 self
1372 }
1373
1374 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1377 self.model_id = Some(model_id.into());
1378 self
1379 }
1380
1381 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1386 self.wire_model = Some(wire_model.into());
1387 self
1388 }
1389
1390 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1394 self.max_prompt_tokens = Some(max);
1395 self
1396 }
1397
1398 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1401 self.max_output_tokens = Some(max);
1402 self
1403 }
1404}
1405
1406#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1419#[serde(rename_all = "camelCase")]
1420#[non_exhaustive]
1421pub struct CapiSessionOptions {
1422 #[serde(default, skip_serializing_if = "Option::is_none")]
1430 pub auto_tier: Option<AutoTier>,
1431
1432 #[serde(default, skip_serializing_if = "Option::is_none")]
1438 pub enable_web_socket_responses: Option<bool>,
1439}
1440
1441impl CapiSessionOptions {
1442 pub fn new() -> Self {
1444 Self::default()
1445 }
1446
1447 pub fn with_auto_tier(mut self, auto_tier: AutoTier) -> Self {
1449 self.auto_tier = Some(auto_tier);
1450 self
1451 }
1452
1453 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1455 self.enable_web_socket_responses = Some(enable);
1456 self
1457 }
1458}
1459
1460#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1462#[serde(rename_all = "camelCase")]
1463pub struct AzureProviderOptions {
1464 #[serde(default, skip_serializing_if = "Option::is_none")]
1466 pub api_version: Option<String>,
1467}
1468
1469#[derive(Clone, Default, Serialize, Deserialize)]
1480#[serde(rename_all = "camelCase")]
1481#[non_exhaustive]
1482pub struct NamedProviderConfig {
1483 pub name: String,
1486 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1489 pub provider_type: Option<String>,
1490 #[serde(default, skip_serializing_if = "Option::is_none")]
1493 pub wire_api: Option<String>,
1494 pub base_url: String,
1496 #[serde(default, skip_serializing_if = "Option::is_none")]
1498 pub api_key: Option<String>,
1499 #[serde(default, skip_serializing_if = "Option::is_none")]
1502 pub bearer_token: Option<String>,
1503 #[serde(skip)]
1506 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1507 #[serde(default, skip_serializing_if = "Option::is_none")]
1508 pub(crate) has_bearer_token_provider: Option<bool>,
1509 #[serde(default, skip_serializing_if = "Option::is_none")]
1511 pub azure: Option<AzureProviderOptions>,
1512 #[serde(default, skip_serializing_if = "Option::is_none")]
1514 pub headers: Option<HashMap<String, String>>,
1515}
1516
1517impl std::fmt::Debug for NamedProviderConfig {
1518 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1519 f.debug_struct("NamedProviderConfig")
1520 .field("name", &self.name)
1521 .field("provider_type", &self.provider_type)
1522 .field("wire_api", &self.wire_api)
1523 .field("base_url", &self.base_url)
1524 .field("api_key", &self.api_key)
1525 .field("bearer_token", &self.bearer_token)
1526 .field(
1527 "bearer_token_provider",
1528 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1529 )
1530 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1531 .field("azure", &self.azure)
1532 .field("headers", &self.headers)
1533 .finish()
1534 }
1535}
1536
1537impl NamedProviderConfig {
1538 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1541 Self {
1542 name: name.into(),
1543 base_url: base_url.into(),
1544 ..Self::default()
1545 }
1546 }
1547
1548 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1550 self.provider_type = Some(provider_type.into());
1551 self
1552 }
1553
1554 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1556 self.wire_api = Some(wire_api.into());
1557 self
1558 }
1559
1560 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1562 self.api_key = Some(api_key.into());
1563 self
1564 }
1565
1566 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1569 self.bearer_token = Some(bearer_token.into());
1570 self
1571 }
1572
1573 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1579 self.bearer_token_provider = Some(provider);
1580 self
1581 }
1582
1583 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1585 self.azure = Some(azure);
1586 self
1587 }
1588
1589 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1591 self.headers = Some(headers);
1592 self
1593 }
1594}
1595
1596fn prepare_bearer_token_providers(
1597 provider: &mut Option<ProviderConfig>,
1598 providers: &mut Option<Vec<NamedProviderConfig>>,
1599) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1600 let mut bearer_token_providers = HashMap::new();
1601
1602 if let Some(provider) = provider.as_mut()
1603 && let Some(token_provider) = provider.bearer_token_provider.take()
1604 {
1605 provider.has_bearer_token_provider = Some(true);
1606 bearer_token_providers.insert("default".to_string(), token_provider);
1607 }
1608
1609 if let Some(providers) = providers.as_mut() {
1610 for provider in providers {
1611 if let Some(token_provider) = provider.bearer_token_provider.take() {
1612 provider.has_bearer_token_provider = Some(true);
1613 bearer_token_providers.insert(provider.name.clone(), token_provider);
1614 }
1615 }
1616 }
1617
1618 bearer_token_providers
1619}
1620
1621#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1629#[serde(rename_all = "camelCase")]
1630#[non_exhaustive]
1631pub struct ProviderModelConfig {
1632 pub id: String,
1635 pub provider: String,
1637 #[serde(default, skip_serializing_if = "Option::is_none")]
1640 pub wire_model: Option<String>,
1641 #[serde(default, skip_serializing_if = "Option::is_none")]
1644 pub model_id: Option<String>,
1645 #[serde(default, skip_serializing_if = "Option::is_none")]
1647 pub name: Option<String>,
1648 #[serde(default, skip_serializing_if = "Option::is_none")]
1650 pub max_prompt_tokens: Option<i64>,
1651 #[serde(default, skip_serializing_if = "Option::is_none")]
1653 pub max_context_window_tokens: Option<i64>,
1654 #[serde(default, skip_serializing_if = "Option::is_none")]
1656 pub max_output_tokens: Option<i64>,
1657 #[serde(default, skip_serializing_if = "Option::is_none")]
1660 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1661}
1662
1663impl ProviderModelConfig {
1664 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1667 Self {
1668 id: id.into(),
1669 provider: provider.into(),
1670 ..Self::default()
1671 }
1672 }
1673
1674 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1676 self.wire_model = Some(wire_model.into());
1677 self
1678 }
1679
1680 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1683 self.model_id = Some(model_id.into());
1684 self
1685 }
1686
1687 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1689 self.name = Some(name.into());
1690 self
1691 }
1692
1693 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1695 self.max_prompt_tokens = Some(max);
1696 self
1697 }
1698
1699 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1701 self.max_context_window_tokens = Some(max);
1702 self
1703 }
1704
1705 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1707 self.max_output_tokens = Some(max);
1708 self
1709 }
1710
1711 pub fn with_capabilities(
1713 mut self,
1714 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1715 ) -> Self {
1716 self.capabilities = Some(capabilities);
1717 self
1718 }
1719}
1720
1721#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1725#[serde(untagged)]
1726pub enum ExpFlagValue {
1727 Bool(bool),
1729 Integer(i64),
1731 Float(f64),
1733 String(String),
1735 Null,
1737}
1738
1739#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1743#[serde(rename_all = "PascalCase")]
1744pub struct ExpConfigEntry {
1745 pub id: String,
1747 pub parameters: HashMap<String, ExpFlagValue>,
1749}
1750
1751#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1757#[serde(rename_all = "PascalCase")]
1758pub struct CopilotExpAssignmentResponse {
1759 #[serde(default)]
1761 pub features: Vec<String>,
1762 #[serde(default)]
1764 pub flights: HashMap<String, String>,
1765 #[serde(default)]
1767 pub configs: Vec<ExpConfigEntry>,
1768 #[serde(default, skip_serializing_if = "Option::is_none")]
1770 pub parameter_groups: Option<Value>,
1771 #[serde(default, skip_serializing_if = "Option::is_none")]
1773 pub flighting_version: Option<i64>,
1774 #[serde(default, skip_serializing_if = "Option::is_none")]
1776 pub impression_id: Option<String>,
1777 #[serde(default)]
1779 pub assignment_context: String,
1780}
1781
1782pub struct DisableBypassPermissionsModes;
1784
1785impl DisableBypassPermissionsModes {
1786 pub const ALLOW_AUTO_ONLY: &'static str = "allow-auto-only";
1788 pub const DISABLE: &'static str = "disable";
1790}
1791
1792#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1801#[serde(rename_all = "camelCase")]
1802#[non_exhaustive]
1803pub struct ManagedSettingsPermissions {
1804 #[serde(default, skip_serializing_if = "Option::is_none")]
1808 pub disable_bypass_permissions_mode: Option<String>,
1809 #[serde(default, skip_serializing_if = "Option::is_none")]
1811 pub deny: Option<Vec<String>>,
1812 #[serde(default, skip_serializing_if = "Option::is_none")]
1814 pub ask: Option<Vec<String>>,
1815 #[serde(default, skip_serializing_if = "Option::is_none")]
1817 pub allow: Option<Vec<String>>,
1818}
1819
1820impl ManagedSettingsPermissions {
1821 pub fn with_disable_bypass_permissions_mode(mut self, value: impl Into<String>) -> Self {
1823 self.disable_bypass_permissions_mode = Some(value.into());
1824 self
1825 }
1826
1827 pub fn with_deny(mut self, rules: Vec<String>) -> Self {
1829 self.deny = Some(rules);
1830 self
1831 }
1832
1833 pub fn with_ask(mut self, rules: Vec<String>) -> Self {
1835 self.ask = Some(rules);
1836 self
1837 }
1838
1839 pub fn with_allow(mut self, rules: Vec<String>) -> Self {
1841 self.allow = Some(rules);
1842 self
1843 }
1844}
1845
1846#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1856#[serde(rename_all = "camelCase")]
1857#[non_exhaustive]
1858pub struct ManagedSettings {
1859 #[serde(default, skip_serializing_if = "Option::is_none")]
1861 pub permissions: Option<ManagedSettingsPermissions>,
1862}
1863
1864impl ManagedSettings {
1865 pub fn with_permissions(mut self, permissions: ManagedSettingsPermissions) -> Self {
1867 self.permissions = Some(permissions);
1868 self
1869 }
1870}
1871
1872#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1874#[serde(rename_all = "lowercase")]
1875#[non_exhaustive]
1876pub enum AskUserVariant {
1877 #[default]
1879 Legacy,
1880 Elicitation,
1882}
1883
1884#[derive(Clone)]
1936#[non_exhaustive]
1937pub struct SessionConfig {
1938 pub session_id: Option<SessionId>,
1940 pub model: Option<String>,
1942 pub client_name: Option<String>,
1944 pub reasoning_effort: Option<String>,
1946 pub reasoning_summary: Option<ReasoningSummary>,
1950 pub context_tier: Option<String>,
1953 pub streaming: Option<bool>,
1955 pub system_message: Option<SystemMessageConfig>,
1957 pub ask_user_variant: Option<AskUserVariant>,
1962 pub tools: Option<Vec<Tool>>,
1964 pub canvases: Option<Vec<CanvasDeclaration>>,
1966 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1971 pub request_canvas_renderer: Option<bool>,
1973 pub request_extensions: Option<bool>,
1975 pub extension_sdk_path: Option<String>,
1979 pub extension_info: Option<ExtensionInfo>,
1981 pub canvas_provider: Option<CanvasProviderIdentity>,
1984 pub available_tools: Option<Vec<String>>,
1986 pub excluded_tools: Option<Vec<String>>,
1988 pub excluded_builtin_agents: Option<Vec<String>>,
1994 pub included_builtin_skills: Option<Vec<String>>,
1998 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
2000 pub mcp_oauth_token_storage: Option<String>,
2009 pub enable_config_discovery: Option<bool>,
2012 pub skip_embedding_retrieval: Option<bool>,
2014 pub embedding_cache_storage: Option<String>,
2017 pub organization_custom_instructions: Option<String>,
2019 pub enable_on_demand_instruction_discovery: Option<bool>,
2021 pub enable_file_hooks: Option<bool>,
2023 pub enable_host_git_operations: Option<bool>,
2025 pub enable_session_store: Option<bool>,
2027 pub enable_skills: Option<bool>,
2029 pub enable_mcp_apps: Option<bool>,
2056 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
2061 pub skill_directories: Option<Vec<PathBuf>>,
2063 pub instruction_directories: Option<Vec<PathBuf>>,
2066 pub plugin_directories: Option<Vec<PathBuf>>,
2068 pub large_output: Option<LargeToolOutputConfig>,
2070 pub tool_search: Option<ToolSearchConfig>,
2074 pub disabled_skills: Option<Vec<String>>,
2077 pub disabled_mcp_servers: Option<Vec<String>>,
2081 pub hooks: Option<bool>,
2085 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2087 pub default_agent: Option<DefaultAgentConfig>,
2091 pub agent: Option<String>,
2094 pub infinite_sessions: Option<InfiniteSessionConfig>,
2097 pub provider: Option<ProviderConfig>,
2101 pub capi: Option<CapiSessionOptions>,
2107 pub providers: Option<Vec<NamedProviderConfig>>,
2114 pub models: Option<Vec<ProviderModelConfig>>,
2120 pub enable_session_telemetry: Option<bool>,
2128 pub enable_citations: Option<bool>,
2130 pub enable_file_change_tracking: Option<bool>,
2133 pub session_limits: Option<SessionLimitsConfig>,
2135 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2138 pub memory: Option<MemoryConfiguration>,
2140 pub config_directory: Option<PathBuf>,
2143 pub working_directory: Option<PathBuf>,
2146 pub additional_directories: Option<Vec<PathBuf>>,
2150 pub github_token: Option<String>,
2156 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2162 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2168 pub cloud: Option<CloudSessionOptions>,
2171 pub include_sub_agent_streaming_events: Option<bool>,
2175 pub commands: Option<Vec<CommandDefinition>>,
2179 pub feature_flags: Option<HashMap<String, bool>>,
2185 #[doc(hidden)]
2192 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
2193 pub enable_managed_settings: Option<bool>,
2201 pub managed_settings: Option<ManagedSettings>,
2210 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2215 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2219 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2222 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2225 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2229 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2232 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2235 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2239 pub(crate) permission_policy: Option<crate::permission::Policy>,
2243 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2248 pub skip_custom_instructions: Option<bool>,
2252 pub custom_agents_local_only: Option<bool>,
2256 pub enable_experimental_mode: Option<bool>,
2261 pub coauthor_enabled: Option<bool>,
2265 pub manage_schedule_enabled: Option<bool>,
2269}
2270
2271impl std::fmt::Debug for SessionConfig {
2272 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2273 f.debug_struct("SessionConfig")
2274 .field("session_id", &self.session_id)
2275 .field("model", &self.model)
2276 .field("client_name", &self.client_name)
2277 .field("reasoning_effort", &self.reasoning_effort)
2278 .field("reasoning_summary", &self.reasoning_summary)
2279 .field("context_tier", &self.context_tier)
2280 .field("streaming", &self.streaming)
2281 .field("system_message", &self.system_message)
2282 .field("ask_user_variant", &self.ask_user_variant)
2283 .field("tools", &self.tools)
2284 .field("canvases", &self.canvases)
2285 .field(
2286 "canvas_handler",
2287 &self.canvas_handler.as_ref().map(|_| "<set>"),
2288 )
2289 .field("request_canvas_renderer", &self.request_canvas_renderer)
2290 .field("request_extensions", &self.request_extensions)
2291 .field("extension_sdk_path", &self.extension_sdk_path)
2292 .field("extension_info", &self.extension_info)
2293 .field("canvas_provider", &self.canvas_provider)
2294 .field("available_tools", &self.available_tools)
2295 .field("excluded_tools", &self.excluded_tools)
2296 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2297 .field("included_builtin_skills", &self.included_builtin_skills)
2298 .field("mcp_servers", &self.mcp_servers)
2299 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2300 .field("embedding_cache_storage", &self.embedding_cache_storage)
2301 .field("enable_config_discovery", &self.enable_config_discovery)
2302 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2303 .field(
2304 "organization_custom_instructions",
2305 &self
2306 .organization_custom_instructions
2307 .as_ref()
2308 .map(|_| "<redacted>"),
2309 )
2310 .field(
2311 "enable_on_demand_instruction_discovery",
2312 &self.enable_on_demand_instruction_discovery,
2313 )
2314 .field("enable_file_hooks", &self.enable_file_hooks)
2315 .field(
2316 "enable_host_git_operations",
2317 &self.enable_host_git_operations,
2318 )
2319 .field("enable_session_store", &self.enable_session_store)
2320 .field("enable_skills", &self.enable_skills)
2321 .field("enable_mcp_apps", &self.enable_mcp_apps)
2322 .field("skill_directories", &self.skill_directories)
2323 .field("instruction_directories", &self.instruction_directories)
2324 .field("plugin_directories", &self.plugin_directories)
2325 .field("large_output", &self.large_output)
2326 .field("tool_search", &self.tool_search)
2327 .field("disabled_skills", &self.disabled_skills)
2328 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
2329 .field("hooks", &self.hooks)
2330 .field("custom_agents", &self.custom_agents)
2331 .field("default_agent", &self.default_agent)
2332 .field("agent", &self.agent)
2333 .field("infinite_sessions", &self.infinite_sessions)
2334 .field("provider", &self.provider)
2335 .field("capi", &self.capi)
2336 .field("enable_session_telemetry", &self.enable_session_telemetry)
2337 .field("enable_citations", &self.enable_citations)
2338 .field(
2339 "enable_file_change_tracking",
2340 &self.enable_file_change_tracking,
2341 )
2342 .field("session_limits", &self.session_limits)
2343 .field("model_capabilities", &self.model_capabilities)
2344 .field("memory", &self.memory)
2345 .field("config_directory", &self.config_directory)
2346 .field("working_directory", &self.working_directory)
2347 .field("additional_directories", &self.additional_directories)
2348 .field(
2349 "github_token",
2350 &self.github_token.as_ref().map(|_| "<redacted>"),
2351 )
2352 .field(
2353 "github_token_provider",
2354 &self.github_token_provider.as_ref().map(|_| "<set>"),
2355 )
2356 .field("remote_session", &self.remote_session)
2357 .field("cloud", &self.cloud)
2358 .field(
2359 "include_sub_agent_streaming_events",
2360 &self.include_sub_agent_streaming_events,
2361 )
2362 .field("commands", &self.commands)
2363 .field("feature_flags", &self.feature_flags)
2364 .field("exp_assignments", &self.exp_assignments)
2365 .field("enable_managed_settings", &self.enable_managed_settings)
2366 .field("enable_experimental_mode", &self.enable_experimental_mode)
2367 .field("managed_settings", &self.managed_settings)
2368 .field(
2369 "session_fs_provider",
2370 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2371 )
2372 .field(
2373 "permission_handler",
2374 &self.permission_handler.as_ref().map(|_| "<set>"),
2375 )
2376 .field(
2377 "elicitation_handler",
2378 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2379 )
2380 .field(
2381 "mcp_auth_handler",
2382 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2383 )
2384 .field(
2385 "user_input_handler",
2386 &self.user_input_handler.as_ref().map(|_| "<set>"),
2387 )
2388 .field(
2389 "exit_plan_mode_handler",
2390 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2391 )
2392 .field(
2393 "auto_mode_switch_handler",
2394 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2395 )
2396 .field(
2397 "hooks_handler",
2398 &self.hooks_handler.as_ref().map(|_| "<set>"),
2399 )
2400 .field(
2401 "system_message_transform",
2402 &self.system_message_transform.as_ref().map(|_| "<set>"),
2403 )
2404 .finish()
2405 }
2406}
2407
2408impl Default for SessionConfig {
2409 fn default() -> Self {
2415 Self {
2416 session_id: None,
2417 model: None,
2418 client_name: None,
2419 reasoning_effort: None,
2420 reasoning_summary: None,
2421 context_tier: None,
2422 streaming: None,
2423 system_message: None,
2424 ask_user_variant: None,
2425 tools: None,
2426 canvases: None,
2427 canvas_handler: None,
2428 request_canvas_renderer: None,
2429 request_extensions: None,
2430 extension_sdk_path: None,
2431 extension_info: None,
2432 canvas_provider: None,
2433 available_tools: None,
2434 excluded_tools: None,
2435 excluded_builtin_agents: None,
2436 included_builtin_skills: None,
2437 mcp_servers: None,
2438 mcp_oauth_token_storage: None,
2439 enable_config_discovery: None,
2440 skip_embedding_retrieval: None,
2441 organization_custom_instructions: None,
2442 enable_on_demand_instruction_discovery: None,
2443 enable_file_hooks: None,
2444 enable_host_git_operations: None,
2445 enable_session_store: None,
2446 enable_skills: None,
2447 embedding_cache_storage: None,
2448 enable_mcp_apps: None,
2449 github_mcp_tool_config: None,
2450 skill_directories: None,
2451 instruction_directories: None,
2452 plugin_directories: None,
2453 large_output: None,
2454 tool_search: None,
2455 disabled_skills: None,
2456 disabled_mcp_servers: None,
2457 hooks: None,
2458 custom_agents: None,
2459 default_agent: None,
2460 agent: None,
2461 infinite_sessions: None,
2462 provider: None,
2463 capi: None,
2464 providers: None,
2465 models: None,
2466 enable_session_telemetry: None,
2467 enable_citations: None,
2468 enable_file_change_tracking: None,
2469 session_limits: None,
2470 model_capabilities: None,
2471 memory: None,
2472 config_directory: None,
2473 working_directory: None,
2474 additional_directories: None,
2475 github_token: None,
2476 github_token_provider: None,
2477 remote_session: None,
2478 cloud: None,
2479 include_sub_agent_streaming_events: None,
2480 commands: None,
2481 feature_flags: None,
2482 exp_assignments: None,
2483 enable_managed_settings: None,
2484 managed_settings: None,
2485 session_fs_provider: None,
2486 permission_handler: None,
2487 elicitation_handler: None,
2488 mcp_auth_handler: None,
2489 user_input_handler: None,
2490 exit_plan_mode_handler: None,
2491 auto_mode_switch_handler: None,
2492 hooks_handler: None,
2493 permission_policy: None,
2494 system_message_transform: None,
2495 skip_custom_instructions: None,
2496 custom_agents_local_only: None,
2497 enable_experimental_mode: None,
2498 coauthor_enabled: None,
2499 manage_schedule_enabled: None,
2500 }
2501 }
2502}
2503
2504pub(crate) struct SessionConfigRuntime {
2510 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2511 pub permission_policy: Option<crate::permission::Policy>,
2512 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2513 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2514 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2515 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2516 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2517 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2518 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2519 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2520 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2521 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2522 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2523 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2524 pub commands: Option<Vec<CommandDefinition>>,
2525}
2526
2527impl SessionConfig {
2528 pub(crate) fn into_wire(
2540 mut self,
2541 session_id: Option<SessionId>,
2542 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2543 if self.github_token.is_some() && self.github_token_provider.is_some() {
2544 return Err(crate::Error::with_message(
2545 crate::ErrorKind::InvalidConfig,
2546 "github_token and github_token_provider are mutually exclusive",
2547 ));
2548 }
2549 let permission_active =
2550 self.permission_handler.is_some() || self.permission_policy.is_some();
2551 let request_user_input = self.user_input_handler.is_some();
2552 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2553 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2554 let request_elicitation = self.elicitation_handler.is_some();
2555 let hooks_flag = self.hooks_handler.is_some();
2556
2557 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2558 if let Some(tools) = self.tools.as_mut() {
2559 for tool in tools.iter_mut() {
2560 if let Some(handler) = tool.handler.take()
2561 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2562 {
2563 return Err(crate::Error::with_message(
2564 crate::ErrorKind::InvalidConfig,
2565 format!("duplicate tool handler registered for name {:?}", tool.name),
2566 ));
2567 }
2568 }
2569 }
2570
2571 let wire_commands = self.commands.as_ref().map(|cmds| {
2572 cmds.iter()
2573 .map(|c| crate::wire::CommandWireDefinition {
2574 name: c.name.clone(),
2575 description: c.description.clone().unwrap_or_default(),
2576 })
2577 .collect()
2578 });
2579 let wire_canvases = self.canvases.clone();
2580 let canvas_handler = self.canvas_handler.clone();
2581 let bearer_token_providers =
2582 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2583
2584 let wire = crate::wire::SessionCreateWire {
2585 session_id,
2586 model: self.model,
2587 client_name: self.client_name,
2588 reasoning_effort: self.reasoning_effort,
2589 reasoning_summary: self.reasoning_summary,
2590 context_tier: self.context_tier,
2591 streaming: self.streaming,
2592 system_message: self.system_message,
2593 ask_user_variant: self.ask_user_variant,
2594 tools: self.tools,
2595 canvases: wire_canvases,
2596 request_canvas_renderer: self.request_canvas_renderer,
2597 request_extensions: self.request_extensions,
2598 extension_sdk_path: self.extension_sdk_path,
2599 extension_info: self.extension_info,
2600 canvas_provider: self.canvas_provider,
2601 available_tools: self.available_tools,
2602 excluded_tools: self.excluded_tools,
2603 excluded_builtin_agents: self.excluded_builtin_agents,
2604 tool_filter_precedence: "excluded",
2605 mcp_servers: self.mcp_servers,
2606 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2607 embedding_cache_storage: self.embedding_cache_storage,
2608 env_value_mode: "direct",
2609 enable_config_discovery: self.enable_config_discovery,
2610 skip_embedding_retrieval: self.skip_embedding_retrieval,
2611 organization_custom_instructions: self.organization_custom_instructions,
2612 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2613 enable_file_hooks: self.enable_file_hooks,
2614 enable_host_git_operations: self.enable_host_git_operations,
2615 enable_session_store: self.enable_session_store,
2616 enable_skills: self.enable_skills,
2617 request_user_input,
2618 request_permission: permission_active,
2619 request_exit_plan_mode,
2620 request_auto_mode_switch,
2621 request_elicitation,
2622 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2623 github_mcp_tool_config: self.github_mcp_tool_config,
2624 hooks: hooks_flag,
2625 skill_directories: self.skill_directories,
2626 instruction_directories: self.instruction_directories,
2627 plugin_directories: self.plugin_directories,
2628 large_output: self.large_output,
2629 tool_search: self.tool_search,
2630 disabled_skills: self.disabled_skills,
2631 disabled_mcp_servers: self.disabled_mcp_servers,
2632 custom_agents: self.custom_agents,
2633 custom_agents_local_only: self.custom_agents_local_only,
2634 default_agent: self.default_agent,
2635 agent: self.agent,
2636 infinite_sessions: self.infinite_sessions,
2637 provider: self.provider,
2638 capi: self.capi,
2639 providers: self.providers,
2640 models: self.models,
2641 enable_session_telemetry: self.enable_session_telemetry,
2642 enable_citations: self.enable_citations,
2643 enable_file_change_tracking: self.enable_file_change_tracking,
2644 session_limits: self.session_limits,
2645 model_capabilities: self.model_capabilities,
2646 memory: self.memory,
2647 config_dir: self.config_directory,
2648 working_directory: self.working_directory,
2649 additional_directories: self.additional_directories,
2650 github_token: self.github_token,
2651 github_token_provider_registration_id: None,
2652 remote_session: self.remote_session,
2653 cloud: self.cloud,
2654 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2655 enable_github_telemetry_forwarding: None,
2656 commands: wire_commands,
2657 feature_flags: self.feature_flags,
2658 exp_assignments: self.exp_assignments,
2659 enable_managed_settings: self.enable_managed_settings,
2660 is_experimental_mode: self.enable_experimental_mode,
2661 managed_settings: self.managed_settings,
2662 };
2663
2664 let runtime = SessionConfigRuntime {
2665 permission_handler: self.permission_handler,
2666 permission_policy: self.permission_policy,
2667 elicitation_handler: self.elicitation_handler,
2668 mcp_auth_handler: self.mcp_auth_handler,
2669 user_input_handler: self.user_input_handler,
2670 exit_plan_mode_handler: self.exit_plan_mode_handler,
2671 auto_mode_switch_handler: self.auto_mode_switch_handler,
2672 hooks_handler: self.hooks_handler,
2673 system_message_transform: self.system_message_transform,
2674 tool_handlers,
2675 canvas_handler,
2676 session_fs_provider: self.session_fs_provider,
2677 bearer_token_providers,
2678 github_token_provider: self.github_token_provider,
2679 commands: self.commands,
2680 };
2681
2682 Ok((wire, runtime))
2683 }
2684
2685 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2689 self.permission_handler = Some(handler);
2690 self
2691 }
2692
2693 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2696 self.elicitation_handler = Some(handler);
2697 self
2698 }
2699
2700 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2702 self.mcp_auth_handler = Some(handler);
2703 self
2704 }
2705
2706 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2709 self.user_input_handler = Some(handler);
2710 self
2711 }
2712
2713 pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self {
2715 self.ask_user_variant = Some(variant);
2716 self
2717 }
2718
2719 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2721 self.exit_plan_mode_handler = Some(handler);
2722 self
2723 }
2724
2725 pub fn with_auto_mode_switch_handler(
2727 mut self,
2728 handler: Arc<dyn AutoModeSwitchHandler>,
2729 ) -> Self {
2730 self.auto_mode_switch_handler = Some(handler);
2731 self
2732 }
2733
2734 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2739 self.commands = Some(commands);
2740 self
2741 }
2742
2743 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2747 self.session_fs_provider = Some(provider);
2748 self
2749 }
2750
2751 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2754 self.hooks_handler = Some(hooks);
2755 self
2756 }
2757
2758 pub fn with_system_message_transform(
2762 mut self,
2763 transform: Arc<dyn SystemMessageTransform>,
2764 ) -> Self {
2765 self.system_message_transform = Some(transform);
2766 self
2767 }
2768
2769 pub fn approve_all_permissions(mut self) -> Self {
2775 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2776 self
2777 }
2778
2779 pub fn deny_all_permissions(mut self) -> Self {
2782 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2783 self
2784 }
2785
2786 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2791 where
2792 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2793 {
2794 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2795 self
2796 }
2797
2798 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2800 self.session_id = Some(id.into());
2801 self
2802 }
2803
2804 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2806 self.model = Some(model.into());
2807 self
2808 }
2809
2810 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2812 self.client_name = Some(name.into());
2813 self
2814 }
2815
2816 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2818 self.reasoning_effort = Some(effort.into());
2819 self
2820 }
2821
2822 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2824 self.reasoning_summary = Some(summary);
2825 self
2826 }
2827
2828 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2830 self.context_tier = Some(tier.into());
2831 self
2832 }
2833
2834 pub fn with_streaming(mut self, streaming: bool) -> Self {
2836 self.streaming = Some(streaming);
2837 self
2838 }
2839
2840 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2842 self.system_message = Some(system_message);
2843 self
2844 }
2845
2846 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2848 self.tools = Some(tools.into_iter().collect());
2849 self
2850 }
2851
2852 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2857 self.canvases = Some(canvases.into_iter().collect());
2858 self
2859 }
2860
2861 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2863 self.canvas_handler = Some(handler);
2864 self
2865 }
2866
2867 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2869 self.request_canvas_renderer = Some(request);
2870 self
2871 }
2872
2873 pub fn with_request_extensions(mut self, request: bool) -> Self {
2875 self.request_extensions = Some(request);
2876 self
2877 }
2878
2879 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2883 self.extension_sdk_path = Some(path.into());
2884 self
2885 }
2886
2887 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2889 self.extension_info = Some(extension_info);
2890 self
2891 }
2892
2893 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2896 self.canvas_provider = Some(canvas_provider);
2897 self
2898 }
2899
2900 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2902 where
2903 I: IntoIterator<Item = S>,
2904 S: Into<String>,
2905 {
2906 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2907 self
2908 }
2909
2910 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2912 where
2913 I: IntoIterator<Item = S>,
2914 S: Into<String>,
2915 {
2916 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2917 self
2918 }
2919
2920 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2922 where
2923 I: IntoIterator<Item = S>,
2924 S: Into<String>,
2925 {
2926 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2927 self
2928 }
2929
2930 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2932 self.mcp_servers = Some(servers);
2933 self
2934 }
2935
2936 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2944 self.mcp_oauth_token_storage = Some(mode.into());
2945 self
2946 }
2947
2948 pub fn with_embedding_cache_storage(
2950 mut self,
2951 embedding_cache_storage: impl Into<String>,
2952 ) -> Self {
2953 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2954 self
2955 }
2956
2957 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2960 self.enable_config_discovery = Some(enable);
2961 self
2962 }
2963
2964 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2966 self.skip_embedding_retrieval = Some(value);
2967 self
2968 }
2969
2970 pub fn with_organization_custom_instructions(
2972 mut self,
2973 instructions: impl Into<String>,
2974 ) -> Self {
2975 self.organization_custom_instructions = Some(instructions.into());
2976 self
2977 }
2978
2979 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2981 self.enable_on_demand_instruction_discovery = Some(value);
2982 self
2983 }
2984
2985 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2987 self.enable_file_hooks = Some(value);
2988 self
2989 }
2990
2991 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2993 self.enable_host_git_operations = Some(value);
2994 self
2995 }
2996
2997 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2999 self.enable_session_store = Some(value);
3000 self
3001 }
3002
3003 pub fn with_enable_skills(mut self, value: bool) -> Self {
3005 self.enable_skills = Some(value);
3006 self
3007 }
3008
3009 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3015 self.enable_mcp_apps = Some(enable);
3016 self
3017 }
3018
3019 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
3021 self.github_mcp_tool_config = Some(config);
3022 self
3023 }
3024
3025 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3027 where
3028 I: IntoIterator<Item = P>,
3029 P: Into<PathBuf>,
3030 {
3031 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3032 self
3033 }
3034
3035 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
3037 where
3038 I: IntoIterator<Item = S>,
3039 S: Into<String>,
3040 {
3041 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
3042 self
3043 }
3044
3045 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3049 where
3050 I: IntoIterator<Item = P>,
3051 P: Into<PathBuf>,
3052 {
3053 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3054 self
3055 }
3056
3057 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3059 where
3060 I: IntoIterator<Item = P>,
3061 P: Into<PathBuf>,
3062 {
3063 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3064 self
3065 }
3066
3067 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3069 self.large_output = Some(config);
3070 self
3071 }
3072
3073 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
3076 self.tool_search = Some(config);
3077 self
3078 }
3079
3080 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3082 where
3083 I: IntoIterator<Item = S>,
3084 S: Into<String>,
3085 {
3086 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3087 self
3088 }
3089
3090 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
3092 where
3093 I: IntoIterator<Item = S>,
3094 S: Into<String>,
3095 {
3096 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
3097 self
3098 }
3099
3100 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3102 mut self,
3103 agents: I,
3104 ) -> Self {
3105 self.custom_agents = Some(agents.into_iter().collect());
3106 self
3107 }
3108
3109 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3111 self.default_agent = Some(agent);
3112 self
3113 }
3114
3115 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3118 self.agent = Some(name.into());
3119 self
3120 }
3121
3122 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3125 self.infinite_sessions = Some(config);
3126 self
3127 }
3128
3129 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3131 self.provider = Some(provider);
3132 self
3133 }
3134
3135 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3137 self.capi = Some(capi);
3138 self
3139 }
3140
3141 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3147 self.providers = Some(providers);
3148 self
3149 }
3150
3151 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3157 self.models = Some(models);
3158 self
3159 }
3160
3161 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3165 self.enable_session_telemetry = Some(enable);
3166 self
3167 }
3168
3169 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3171 self.enable_citations = Some(enable);
3172 self
3173 }
3174
3175 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
3178 self.enable_file_change_tracking = Some(enable);
3179 self
3180 }
3181
3182 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3184 self.session_limits = Some(limits);
3185 self
3186 }
3187
3188 pub fn with_model_capabilities(
3190 mut self,
3191 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3192 ) -> Self {
3193 self.model_capabilities = Some(capabilities);
3194 self
3195 }
3196
3197 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3199 self.memory = Some(memory);
3200 self
3201 }
3202
3203 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3205 self.config_directory = Some(dir.into());
3206 self
3207 }
3208
3209 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3212 self.working_directory = Some(dir.into());
3213 self
3214 }
3215
3216 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
3218 where
3219 I: IntoIterator<Item = P>,
3220 P: Into<PathBuf>,
3221 {
3222 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
3223 self
3224 }
3225
3226 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3231 self.github_token = Some(token.into());
3232 self
3233 }
3234
3235 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
3241 self.github_token_provider = Some(provider);
3242 self
3243 }
3244
3245 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3248 self.include_sub_agent_streaming_events = Some(include);
3249 self
3250 }
3251
3252 pub fn with_remote_session(
3254 mut self,
3255 mode: crate::generated::api_types::RemoteSessionMode,
3256 ) -> Self {
3257 self.remote_session = Some(mode);
3258 self
3259 }
3260
3261 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
3263 self.cloud = Some(cloud);
3264 self
3265 }
3266
3267 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3269 self.skip_custom_instructions = Some(value);
3270 self
3271 }
3272
3273 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3275 self.custom_agents_local_only = Some(value);
3276 self
3277 }
3278
3279 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
3281 self.enable_experimental_mode = Some(enable_experimental_mode);
3282 self
3283 }
3284
3285 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3287 self.coauthor_enabled = Some(value);
3288 self
3289 }
3290
3291 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3293 self.manage_schedule_enabled = Some(value);
3294 self
3295 }
3296
3297 pub fn with_feature_flags(mut self, feature_flags: HashMap<String, bool>) -> Self {
3299 self.feature_flags = Some(feature_flags);
3300 self
3301 }
3302
3303 #[doc(hidden)]
3311 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
3312 self.exp_assignments = Some(assignments);
3313 self
3314 }
3315
3316 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3323 self.enable_managed_settings = Some(enabled);
3324 self
3325 }
3326
3327 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
3332 self.managed_settings = Some(managed_settings);
3333 self
3334 }
3335}
3336#[derive(Clone)]
3343#[non_exhaustive]
3344pub struct ResumeSessionConfig {
3345 pub session_id: SessionId,
3347 pub model: Option<String>,
3350 pub client_name: Option<String>,
3352 pub reasoning_effort: Option<String>,
3354 pub reasoning_summary: Option<ReasoningSummary>,
3358 pub context_tier: Option<String>,
3361 pub streaming: Option<bool>,
3363 pub system_message: Option<SystemMessageConfig>,
3366 pub ask_user_variant: Option<AskUserVariant>,
3371 pub tools: Option<Vec<Tool>>,
3373 pub canvases: Option<Vec<CanvasDeclaration>>,
3375 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
3378 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3380 pub request_canvas_renderer: Option<bool>,
3382 pub request_extensions: Option<bool>,
3384 pub extension_sdk_path: Option<String>,
3388 pub extension_info: Option<ExtensionInfo>,
3390 pub canvas_provider: Option<CanvasProviderIdentity>,
3393 pub available_tools: Option<Vec<String>>,
3395 pub excluded_tools: Option<Vec<String>>,
3397 pub excluded_builtin_agents: Option<Vec<String>>,
3403 pub included_builtin_skills: Option<Vec<String>>,
3407 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3409 pub mcp_oauth_token_storage: Option<String>,
3412 pub enable_config_discovery: Option<bool>,
3415 pub skip_embedding_retrieval: Option<bool>,
3417 pub embedding_cache_storage: Option<String>,
3419 pub organization_custom_instructions: Option<String>,
3421 pub enable_on_demand_instruction_discovery: Option<bool>,
3423 pub enable_file_hooks: Option<bool>,
3425 pub enable_host_git_operations: Option<bool>,
3427 pub enable_session_store: Option<bool>,
3429 pub enable_skills: Option<bool>,
3431 pub enable_mcp_apps: Option<bool>,
3437 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
3442 pub skill_directories: Option<Vec<PathBuf>>,
3444 pub instruction_directories: Option<Vec<PathBuf>>,
3447 pub plugin_directories: Option<Vec<PathBuf>>,
3449 pub large_output: Option<LargeToolOutputConfig>,
3451 pub tool_search: Option<ToolSearchConfig>,
3454 pub disabled_skills: Option<Vec<String>>,
3456 pub disabled_mcp_servers: Option<Vec<String>>,
3459 pub hooks: Option<bool>,
3461 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3463 pub default_agent: Option<DefaultAgentConfig>,
3465 pub agent: Option<String>,
3467 pub infinite_sessions: Option<InfiniteSessionConfig>,
3469 pub provider: Option<ProviderConfig>,
3471 pub capi: Option<CapiSessionOptions>,
3477 pub providers: Option<Vec<NamedProviderConfig>>,
3483 pub models: Option<Vec<ProviderModelConfig>>,
3489 pub enable_session_telemetry: Option<bool>,
3497 pub enable_citations: Option<bool>,
3499 pub enable_file_change_tracking: Option<bool>,
3503 pub session_limits: Option<SessionLimitsConfig>,
3505 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3507 pub memory: Option<MemoryConfiguration>,
3509 pub config_directory: Option<PathBuf>,
3511 pub working_directory: Option<PathBuf>,
3513 pub additional_directories: Option<Vec<PathBuf>>,
3516 pub github_token: Option<String>,
3519 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
3522 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3525 pub include_sub_agent_streaming_events: Option<bool>,
3527 pub commands: Option<Vec<CommandDefinition>>,
3531 pub feature_flags: Option<HashMap<String, bool>>,
3535 #[doc(hidden)]
3540 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3541 pub enable_managed_settings: Option<bool>,
3547 pub managed_settings: Option<ManagedSettings>,
3553 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3558 pub suppress_resume_event: Option<bool>,
3561 pub continue_pending_work: Option<bool>,
3569 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3572 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3575 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3577 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3580 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3583 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3586 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3588 pub(crate) permission_policy: Option<crate::permission::Policy>,
3590 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3592 pub skip_custom_instructions: Option<bool>,
3594 pub custom_agents_local_only: Option<bool>,
3596 pub enable_experimental_mode: Option<bool>,
3601 pub coauthor_enabled: Option<bool>,
3603 pub manage_schedule_enabled: Option<bool>,
3605}
3606
3607impl std::fmt::Debug for ResumeSessionConfig {
3608 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3609 f.debug_struct("ResumeSessionConfig")
3610 .field("session_id", &self.session_id)
3611 .field("model", &self.model)
3612 .field("client_name", &self.client_name)
3613 .field("reasoning_effort", &self.reasoning_effort)
3614 .field("reasoning_summary", &self.reasoning_summary)
3615 .field("context_tier", &self.context_tier)
3616 .field("streaming", &self.streaming)
3617 .field("system_message", &self.system_message)
3618 .field("ask_user_variant", &self.ask_user_variant)
3619 .field("tools", &self.tools)
3620 .field("canvases", &self.canvases)
3621 .field(
3622 "canvas_handler",
3623 &self.canvas_handler.as_ref().map(|_| "<set>"),
3624 )
3625 .field("open_canvases", &self.open_canvases)
3626 .field("request_canvas_renderer", &self.request_canvas_renderer)
3627 .field("request_extensions", &self.request_extensions)
3628 .field("extension_sdk_path", &self.extension_sdk_path)
3629 .field("extension_info", &self.extension_info)
3630 .field("canvas_provider", &self.canvas_provider)
3631 .field("available_tools", &self.available_tools)
3632 .field("excluded_tools", &self.excluded_tools)
3633 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3634 .field("included_builtin_skills", &self.included_builtin_skills)
3635 .field("mcp_servers", &self.mcp_servers)
3636 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3637 .field("embedding_cache_storage", &self.embedding_cache_storage)
3638 .field("enable_config_discovery", &self.enable_config_discovery)
3639 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3640 .field(
3641 "organization_custom_instructions",
3642 &self
3643 .organization_custom_instructions
3644 .as_ref()
3645 .map(|_| "<redacted>"),
3646 )
3647 .field(
3648 "enable_on_demand_instruction_discovery",
3649 &self.enable_on_demand_instruction_discovery,
3650 )
3651 .field("enable_file_hooks", &self.enable_file_hooks)
3652 .field(
3653 "enable_host_git_operations",
3654 &self.enable_host_git_operations,
3655 )
3656 .field("enable_session_store", &self.enable_session_store)
3657 .field("enable_skills", &self.enable_skills)
3658 .field("enable_mcp_apps", &self.enable_mcp_apps)
3659 .field("skill_directories", &self.skill_directories)
3660 .field("instruction_directories", &self.instruction_directories)
3661 .field("plugin_directories", &self.plugin_directories)
3662 .field("large_output", &self.large_output)
3663 .field("tool_search", &self.tool_search)
3664 .field("disabled_skills", &self.disabled_skills)
3665 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
3666 .field("hooks", &self.hooks)
3667 .field("custom_agents", &self.custom_agents)
3668 .field("default_agent", &self.default_agent)
3669 .field("agent", &self.agent)
3670 .field("infinite_sessions", &self.infinite_sessions)
3671 .field("provider", &self.provider)
3672 .field("capi", &self.capi)
3673 .field("enable_session_telemetry", &self.enable_session_telemetry)
3674 .field("enable_citations", &self.enable_citations)
3675 .field(
3676 "enable_file_change_tracking",
3677 &self.enable_file_change_tracking,
3678 )
3679 .field("session_limits", &self.session_limits)
3680 .field("model_capabilities", &self.model_capabilities)
3681 .field("memory", &self.memory)
3682 .field("config_directory", &self.config_directory)
3683 .field("working_directory", &self.working_directory)
3684 .field("additional_directories", &self.additional_directories)
3685 .field(
3686 "github_token",
3687 &self.github_token.as_ref().map(|_| "<redacted>"),
3688 )
3689 .field(
3690 "github_token_provider",
3691 &self.github_token_provider.as_ref().map(|_| "<set>"),
3692 )
3693 .field("remote_session", &self.remote_session)
3694 .field(
3695 "include_sub_agent_streaming_events",
3696 &self.include_sub_agent_streaming_events,
3697 )
3698 .field("commands", &self.commands)
3699 .field("feature_flags", &self.feature_flags)
3700 .field("exp_assignments", &self.exp_assignments)
3701 .field("enable_managed_settings", &self.enable_managed_settings)
3702 .field("enable_experimental_mode", &self.enable_experimental_mode)
3703 .field("managed_settings", &self.managed_settings)
3704 .field(
3705 "session_fs_provider",
3706 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3707 )
3708 .field(
3709 "permission_handler",
3710 &self.permission_handler.as_ref().map(|_| "<set>"),
3711 )
3712 .field(
3713 "elicitation_handler",
3714 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3715 )
3716 .field(
3717 "user_input_handler",
3718 &self.user_input_handler.as_ref().map(|_| "<set>"),
3719 )
3720 .field(
3721 "exit_plan_mode_handler",
3722 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3723 )
3724 .field(
3725 "auto_mode_switch_handler",
3726 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3727 )
3728 .field(
3729 "hooks_handler",
3730 &self.hooks_handler.as_ref().map(|_| "<set>"),
3731 )
3732 .field(
3733 "system_message_transform",
3734 &self.system_message_transform.as_ref().map(|_| "<set>"),
3735 )
3736 .field("suppress_resume_event", &self.suppress_resume_event)
3737 .field("continue_pending_work", &self.continue_pending_work)
3738 .finish()
3739 }
3740}
3741
3742impl ResumeSessionConfig {
3743 pub(crate) fn into_wire(
3751 mut self,
3752 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3753 if self.github_token.is_some() && self.github_token_provider.is_some() {
3754 return Err(crate::Error::with_message(
3755 crate::ErrorKind::InvalidConfig,
3756 "github_token and github_token_provider are mutually exclusive",
3757 ));
3758 }
3759 let permission_active =
3760 self.permission_handler.is_some() || self.permission_policy.is_some();
3761 let request_user_input = self.user_input_handler.is_some();
3762 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3763 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3764 let request_elicitation = self.elicitation_handler.is_some();
3765 let hooks_flag = self.hooks_handler.is_some();
3766
3767 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3768 if let Some(tools) = self.tools.as_mut() {
3769 for tool in tools.iter_mut() {
3770 if let Some(handler) = tool.handler.take()
3771 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3772 {
3773 return Err(crate::Error::with_message(
3774 crate::ErrorKind::InvalidConfig,
3775 format!("duplicate tool handler registered for name {:?}", tool.name),
3776 ));
3777 }
3778 }
3779 }
3780
3781 let wire_commands = self.commands.as_ref().map(|cmds| {
3782 cmds.iter()
3783 .map(|c| crate::wire::CommandWireDefinition {
3784 name: c.name.clone(),
3785 description: c.description.clone().unwrap_or_default(),
3786 })
3787 .collect()
3788 });
3789 let wire_canvases = self.canvases.clone();
3790 let canvas_handler = self.canvas_handler.clone();
3791 let bearer_token_providers =
3792 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3793
3794 let wire = crate::wire::SessionResumeWire {
3795 session_id: self.session_id,
3796 model: self.model,
3797 client_name: self.client_name,
3798 reasoning_effort: self.reasoning_effort,
3799 reasoning_summary: self.reasoning_summary,
3800 context_tier: self.context_tier,
3801 streaming: self.streaming,
3802 system_message: self.system_message,
3803 ask_user_variant: self.ask_user_variant,
3804 tools: self.tools,
3805 canvases: wire_canvases,
3806 open_canvases: self.open_canvases,
3807 request_canvas_renderer: self.request_canvas_renderer,
3808 request_extensions: self.request_extensions,
3809 extension_sdk_path: self.extension_sdk_path,
3810 extension_info: self.extension_info,
3811 canvas_provider: self.canvas_provider,
3812 available_tools: self.available_tools,
3813 excluded_tools: self.excluded_tools,
3814 excluded_builtin_agents: self.excluded_builtin_agents,
3815 tool_filter_precedence: "excluded",
3816 mcp_servers: self.mcp_servers,
3817 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3818 embedding_cache_storage: self.embedding_cache_storage,
3819 env_value_mode: "direct",
3820 enable_config_discovery: self.enable_config_discovery,
3821 skip_embedding_retrieval: self.skip_embedding_retrieval,
3822 organization_custom_instructions: self.organization_custom_instructions,
3823 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3824 enable_file_hooks: self.enable_file_hooks,
3825 enable_host_git_operations: self.enable_host_git_operations,
3826 enable_session_store: self.enable_session_store,
3827 enable_skills: self.enable_skills,
3828 request_user_input,
3829 request_permission: permission_active,
3830 request_exit_plan_mode,
3831 request_auto_mode_switch,
3832 request_elicitation,
3833 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3834 github_mcp_tool_config: self.github_mcp_tool_config,
3835 hooks: hooks_flag,
3836 skill_directories: self.skill_directories,
3837 instruction_directories: self.instruction_directories,
3838 plugin_directories: self.plugin_directories,
3839 large_output: self.large_output,
3840 tool_search: self.tool_search,
3841 disabled_skills: self.disabled_skills,
3842 disabled_mcp_servers: self.disabled_mcp_servers,
3843 custom_agents: self.custom_agents,
3844 custom_agents_local_only: self.custom_agents_local_only,
3845 default_agent: self.default_agent,
3846 agent: self.agent,
3847 infinite_sessions: self.infinite_sessions,
3848 provider: self.provider,
3849 capi: self.capi,
3850 providers: self.providers,
3851 models: self.models,
3852 enable_session_telemetry: self.enable_session_telemetry,
3853 enable_citations: self.enable_citations,
3854 enable_file_change_tracking: self.enable_file_change_tracking,
3855 session_limits: self.session_limits,
3856 model_capabilities: self.model_capabilities,
3857 memory: self.memory,
3858 config_dir: self.config_directory,
3859 working_directory: self.working_directory,
3860 additional_directories: self.additional_directories,
3861 github_token: self.github_token,
3862 github_token_provider_registration_id: None,
3863 remote_session: self.remote_session,
3864 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3865 enable_github_telemetry_forwarding: None,
3866 commands: wire_commands,
3867 feature_flags: self.feature_flags,
3868 exp_assignments: self.exp_assignments,
3869 enable_managed_settings: self.enable_managed_settings,
3870 is_experimental_mode: self.enable_experimental_mode,
3871 managed_settings: self.managed_settings,
3872 suppress_resume_event: self.suppress_resume_event,
3873 continue_pending_work: self.continue_pending_work,
3874 };
3875
3876 let runtime = SessionConfigRuntime {
3877 permission_handler: self.permission_handler,
3878 permission_policy: self.permission_policy,
3879 elicitation_handler: self.elicitation_handler,
3880 mcp_auth_handler: self.mcp_auth_handler,
3881 user_input_handler: self.user_input_handler,
3882 exit_plan_mode_handler: self.exit_plan_mode_handler,
3883 auto_mode_switch_handler: self.auto_mode_switch_handler,
3884 hooks_handler: self.hooks_handler,
3885 system_message_transform: self.system_message_transform,
3886 tool_handlers,
3887 canvas_handler,
3888 session_fs_provider: self.session_fs_provider,
3889 bearer_token_providers,
3890 github_token_provider: self.github_token_provider,
3891 commands: self.commands,
3892 };
3893
3894 Ok((wire, runtime))
3895 }
3896
3897 pub fn new(session_id: SessionId) -> Self {
3902 Self {
3903 session_id,
3904 model: None,
3905 client_name: None,
3906 reasoning_effort: None,
3907 reasoning_summary: None,
3908 context_tier: None,
3909 streaming: None,
3910 system_message: None,
3911 ask_user_variant: None,
3912 tools: None,
3913 canvases: None,
3914 canvas_handler: None,
3915 open_canvases: None,
3916 request_canvas_renderer: None,
3917 request_extensions: None,
3918 extension_sdk_path: None,
3919 extension_info: None,
3920 canvas_provider: None,
3921 available_tools: None,
3922 excluded_tools: None,
3923 excluded_builtin_agents: None,
3924 included_builtin_skills: None,
3925 mcp_servers: None,
3926 mcp_oauth_token_storage: None,
3927 enable_config_discovery: None,
3928 skip_embedding_retrieval: None,
3929 organization_custom_instructions: None,
3930 enable_on_demand_instruction_discovery: None,
3931 enable_file_hooks: None,
3932 enable_host_git_operations: None,
3933 enable_session_store: None,
3934 enable_skills: None,
3935 embedding_cache_storage: None,
3936 enable_mcp_apps: None,
3937 github_mcp_tool_config: None,
3938 skill_directories: None,
3939 instruction_directories: None,
3940 plugin_directories: None,
3941 large_output: None,
3942 tool_search: None,
3943 disabled_skills: None,
3944 disabled_mcp_servers: None,
3945 hooks: None,
3946 custom_agents: None,
3947 default_agent: None,
3948 agent: None,
3949 infinite_sessions: None,
3950 provider: None,
3951 capi: None,
3952 providers: None,
3953 models: None,
3954 enable_session_telemetry: None,
3955 enable_citations: None,
3956 enable_file_change_tracking: None,
3957 session_limits: None,
3958 model_capabilities: None,
3959 memory: None,
3960 config_directory: None,
3961 working_directory: None,
3962 additional_directories: None,
3963 github_token: None,
3964 github_token_provider: None,
3965 remote_session: None,
3966 include_sub_agent_streaming_events: None,
3967 commands: None,
3968 feature_flags: None,
3969 exp_assignments: None,
3970 enable_managed_settings: None,
3971 managed_settings: None,
3972 session_fs_provider: None,
3973 suppress_resume_event: None,
3974 continue_pending_work: None,
3975 permission_handler: None,
3976 elicitation_handler: None,
3977 mcp_auth_handler: None,
3978 user_input_handler: None,
3979 exit_plan_mode_handler: None,
3980 auto_mode_switch_handler: None,
3981 hooks_handler: None,
3982 permission_policy: None,
3983 system_message_transform: None,
3984 skip_custom_instructions: None,
3985 custom_agents_local_only: None,
3986 enable_experimental_mode: None,
3987 coauthor_enabled: None,
3988 manage_schedule_enabled: None,
3989 }
3990 }
3991
3992 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3994 self.permission_handler = Some(handler);
3995 self
3996 }
3997
3998 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
4000 self.elicitation_handler = Some(handler);
4001 self
4002 }
4003
4004 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
4006 self.mcp_auth_handler = Some(handler);
4007 self
4008 }
4009
4010 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
4012 self.user_input_handler = Some(handler);
4013 self
4014 }
4015
4016 pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self {
4018 self.ask_user_variant = Some(variant);
4019 self
4020 }
4021
4022 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
4024 self.exit_plan_mode_handler = Some(handler);
4025 self
4026 }
4027
4028 pub fn with_auto_mode_switch_handler(
4030 mut self,
4031 handler: Arc<dyn AutoModeSwitchHandler>,
4032 ) -> Self {
4033 self.auto_mode_switch_handler = Some(handler);
4034 self
4035 }
4036
4037 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
4040 self.hooks_handler = Some(hooks);
4041 self
4042 }
4043
4044 pub fn with_system_message_transform(
4046 mut self,
4047 transform: Arc<dyn SystemMessageTransform>,
4048 ) -> Self {
4049 self.system_message_transform = Some(transform);
4050 self
4051 }
4052
4053 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
4057 self.commands = Some(commands);
4058 self
4059 }
4060
4061 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
4064 self.session_fs_provider = Some(provider);
4065 self
4066 }
4067
4068 pub fn approve_all_permissions(mut self) -> Self {
4071 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
4072 self
4073 }
4074
4075 pub fn deny_all_permissions(mut self) -> Self {
4078 self.permission_policy = Some(crate::permission::Policy::DenyAll);
4079 self
4080 }
4081
4082 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
4085 where
4086 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
4087 {
4088 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
4089 self
4090 }
4091
4092 pub fn with_model(mut self, model: impl Into<String>) -> Self {
4094 self.model = Some(model.into());
4095 self
4096 }
4097
4098 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
4100 self.client_name = Some(name.into());
4101 self
4102 }
4103
4104 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4106 self.reasoning_effort = Some(effort.into());
4107 self
4108 }
4109
4110 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4112 self.reasoning_summary = Some(summary);
4113 self
4114 }
4115
4116 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
4119 self.context_tier = Some(tier.into());
4120 self
4121 }
4122
4123 pub fn with_streaming(mut self, streaming: bool) -> Self {
4125 self.streaming = Some(streaming);
4126 self
4127 }
4128
4129 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
4132 self.system_message = Some(system_message);
4133 self
4134 }
4135
4136 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
4138 self.tools = Some(tools.into_iter().collect());
4139 self
4140 }
4141
4142 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
4144 self.canvases = Some(canvases.into_iter().collect());
4145 self
4146 }
4147
4148 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
4150 self.canvas_handler = Some(handler);
4151 self
4152 }
4153
4154 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
4156 mut self,
4157 open_canvases: I,
4158 ) -> Self {
4159 self.open_canvases = Some(open_canvases.into_iter().collect());
4160 self
4161 }
4162
4163 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
4165 self.request_canvas_renderer = Some(request);
4166 self
4167 }
4168
4169 pub fn with_request_extensions(mut self, request: bool) -> Self {
4171 self.request_extensions = Some(request);
4172 self
4173 }
4174
4175 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
4179 self.extension_sdk_path = Some(path.into());
4180 self
4181 }
4182
4183 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
4185 self.extension_info = Some(extension_info);
4186 self
4187 }
4188
4189 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
4192 self.canvas_provider = Some(canvas_provider);
4193 self
4194 }
4195
4196 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
4198 where
4199 I: IntoIterator<Item = S>,
4200 S: Into<String>,
4201 {
4202 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
4203 self
4204 }
4205
4206 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
4208 where
4209 I: IntoIterator<Item = S>,
4210 S: Into<String>,
4211 {
4212 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
4213 self
4214 }
4215
4216 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
4218 where
4219 I: IntoIterator<Item = S>,
4220 S: Into<String>,
4221 {
4222 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
4223 self
4224 }
4225
4226 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
4228 self.mcp_servers = Some(servers);
4229 self
4230 }
4231
4232 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
4235 self.mcp_oauth_token_storage = Some(mode.into());
4236 self
4237 }
4238
4239 pub fn with_embedding_cache_storage(
4241 mut self,
4242 embedding_cache_storage: impl Into<String>,
4243 ) -> Self {
4244 self.embedding_cache_storage = Some(embedding_cache_storage.into());
4245 self
4246 }
4247
4248 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
4251 self.enable_config_discovery = Some(enable);
4252 self
4253 }
4254
4255 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
4257 self.skip_embedding_retrieval = Some(value);
4258 self
4259 }
4260
4261 pub fn with_organization_custom_instructions(
4263 mut self,
4264 instructions: impl Into<String>,
4265 ) -> Self {
4266 self.organization_custom_instructions = Some(instructions.into());
4267 self
4268 }
4269
4270 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
4272 self.enable_on_demand_instruction_discovery = Some(value);
4273 self
4274 }
4275
4276 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
4278 self.enable_file_hooks = Some(value);
4279 self
4280 }
4281
4282 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
4284 self.enable_host_git_operations = Some(value);
4285 self
4286 }
4287
4288 pub fn with_enable_session_store(mut self, value: bool) -> Self {
4290 self.enable_session_store = Some(value);
4291 self
4292 }
4293
4294 pub fn with_enable_skills(mut self, value: bool) -> Self {
4296 self.enable_skills = Some(value);
4297 self
4298 }
4299
4300 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
4306 self.enable_mcp_apps = Some(enable);
4307 self
4308 }
4309
4310 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
4312 self.github_mcp_tool_config = Some(config);
4313 self
4314 }
4315
4316 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
4318 where
4319 I: IntoIterator<Item = P>,
4320 P: Into<PathBuf>,
4321 {
4322 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
4323 self
4324 }
4325
4326 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
4328 where
4329 I: IntoIterator<Item = S>,
4330 S: Into<String>,
4331 {
4332 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
4333 self
4334 }
4335
4336 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
4340 where
4341 I: IntoIterator<Item = P>,
4342 P: Into<PathBuf>,
4343 {
4344 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
4345 self
4346 }
4347
4348 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
4350 where
4351 I: IntoIterator<Item = P>,
4352 P: Into<PathBuf>,
4353 {
4354 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
4355 self
4356 }
4357
4358 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
4360 self.large_output = Some(config);
4361 self
4362 }
4363
4364 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
4367 self.tool_search = Some(config);
4368 self
4369 }
4370
4371 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
4373 where
4374 I: IntoIterator<Item = S>,
4375 S: Into<String>,
4376 {
4377 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
4378 self
4379 }
4380
4381 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
4383 where
4384 I: IntoIterator<Item = S>,
4385 S: Into<String>,
4386 {
4387 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
4388 self
4389 }
4390
4391 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
4393 mut self,
4394 agents: I,
4395 ) -> Self {
4396 self.custom_agents = Some(agents.into_iter().collect());
4397 self
4398 }
4399
4400 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
4402 self.default_agent = Some(agent);
4403 self
4404 }
4405
4406 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
4408 self.agent = Some(name.into());
4409 self
4410 }
4411
4412 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
4414 self.infinite_sessions = Some(config);
4415 self
4416 }
4417
4418 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
4420 self.provider = Some(provider);
4421 self
4422 }
4423
4424 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
4426 self.capi = Some(capi);
4427 self
4428 }
4429
4430 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
4436 self.providers = Some(providers);
4437 self
4438 }
4439
4440 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
4446 self.models = Some(models);
4447 self
4448 }
4449
4450 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
4454 self.enable_session_telemetry = Some(enable);
4455 self
4456 }
4457
4458 pub fn with_enable_citations(mut self, enable: bool) -> Self {
4460 self.enable_citations = Some(enable);
4461 self
4462 }
4463
4464 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
4467 self.enable_file_change_tracking = Some(enable);
4468 self
4469 }
4470
4471 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
4473 self.session_limits = Some(limits);
4474 self
4475 }
4476
4477 pub fn with_model_capabilities(
4479 mut self,
4480 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
4481 ) -> Self {
4482 self.model_capabilities = Some(capabilities);
4483 self
4484 }
4485
4486 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
4488 self.memory = Some(memory);
4489 self
4490 }
4491
4492 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4494 self.config_directory = Some(dir.into());
4495 self
4496 }
4497
4498 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4500 self.working_directory = Some(dir.into());
4501 self
4502 }
4503
4504 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
4506 where
4507 I: IntoIterator<Item = P>,
4508 P: Into<PathBuf>,
4509 {
4510 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
4511 self
4512 }
4513
4514 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
4518 self.github_token = Some(token.into());
4519 self
4520 }
4521
4522 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
4528 self.github_token_provider = Some(provider);
4529 self
4530 }
4531
4532 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4534 self.include_sub_agent_streaming_events = Some(include);
4535 self
4536 }
4537
4538 pub fn with_remote_session(
4540 mut self,
4541 mode: crate::generated::api_types::RemoteSessionMode,
4542 ) -> Self {
4543 self.remote_session = Some(mode);
4544 self
4545 }
4546
4547 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4550 self.suppress_resume_event = Some(suppress);
4551 self
4552 }
4553
4554 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4560 self.continue_pending_work = Some(continue_pending);
4561 self
4562 }
4563
4564 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4566 self.skip_custom_instructions = Some(value);
4567 self
4568 }
4569
4570 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4572 self.custom_agents_local_only = Some(value);
4573 self
4574 }
4575
4576 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
4578 self.enable_experimental_mode = Some(enable_experimental_mode);
4579 self
4580 }
4581
4582 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4584 self.coauthor_enabled = Some(value);
4585 self
4586 }
4587
4588 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4590 self.manage_schedule_enabled = Some(value);
4591 self
4592 }
4593
4594 pub fn with_feature_flags(mut self, feature_flags: HashMap<String, bool>) -> Self {
4596 self.feature_flags = Some(feature_flags);
4597 self
4598 }
4599
4600 #[doc(hidden)]
4604 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4605 self.exp_assignments = Some(assignments);
4606 self
4607 }
4608
4609 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4612 self.enable_managed_settings = Some(enabled);
4613 self
4614 }
4615
4616 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
4620 self.managed_settings = Some(managed_settings);
4621 self
4622 }
4623}
4624
4625#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4631#[serde(rename_all = "camelCase")]
4632#[non_exhaustive]
4633pub struct SystemMessageConfig {
4634 #[serde(skip_serializing_if = "Option::is_none")]
4636 pub mode: Option<String>,
4637 #[serde(skip_serializing_if = "Option::is_none")]
4639 pub content: Option<String>,
4640 #[serde(skip_serializing_if = "Option::is_none")]
4642 pub sections: Option<HashMap<String, SectionOverride>>,
4643}
4644
4645impl SystemMessageConfig {
4646 pub fn new() -> Self {
4649 Self::default()
4650 }
4651
4652 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4655 self.mode = Some(mode.into());
4656 self
4657 }
4658
4659 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4662 self.content = Some(content.into());
4663 self
4664 }
4665
4666 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4668 self.sections = Some(sections);
4669 self
4670 }
4671}
4672
4673#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4679#[serde(rename_all = "camelCase")]
4680pub struct SectionOverride {
4681 #[serde(skip_serializing_if = "Option::is_none")]
4684 pub action: Option<String>,
4685 #[serde(skip_serializing_if = "Option::is_none")]
4687 pub content: Option<String>,
4688}
4689
4690#[derive(Debug, Clone, Serialize, Deserialize)]
4692#[serde(rename_all = "camelCase")]
4693pub struct CreateSessionResult {
4694 pub session_id: SessionId,
4696 #[serde(skip_serializing_if = "Option::is_none")]
4698 pub workspace_path: Option<PathBuf>,
4699 #[serde(default, alias = "remote_url")]
4701 pub remote_url: Option<String>,
4702 #[serde(skip_serializing_if = "Option::is_none")]
4704 pub capabilities: Option<SessionCapabilities>,
4705}
4706
4707#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4709#[serde(rename_all = "camelCase")]
4710pub(crate) struct ResumeSessionResult {
4711 #[serde(default)]
4713 pub session_id: Option<SessionId>,
4714 #[serde(default, skip_serializing_if = "Option::is_none")]
4716 pub workspace_path: Option<PathBuf>,
4717 #[serde(default, alias = "remote_url")]
4719 pub remote_url: Option<String>,
4720 #[serde(default, skip_serializing_if = "Option::is_none")]
4722 pub capabilities: Option<SessionCapabilities>,
4723 #[serde(
4725 default,
4726 alias = "openCanvasInstances",
4727 skip_serializing_if = "Option::is_none"
4728 )]
4729 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4730}
4731
4732#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4734#[serde(rename_all = "lowercase")]
4735pub enum LogLevel {
4736 #[default]
4738 Info,
4739 Warning,
4741 Error,
4743}
4744
4745#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4750#[serde(rename_all = "camelCase")]
4751pub struct LogOptions {
4752 #[serde(skip_serializing_if = "Option::is_none")]
4754 pub level: Option<LogLevel>,
4755 #[serde(skip_serializing_if = "Option::is_none")]
4758 pub ephemeral: Option<bool>,
4759}
4760
4761impl LogOptions {
4762 pub fn with_level(mut self, level: LogLevel) -> Self {
4764 self.level = Some(level);
4765 self
4766 }
4767
4768 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4770 self.ephemeral = Some(ephemeral);
4771 self
4772 }
4773}
4774
4775#[derive(Debug, Clone, Default)]
4779pub struct SetModelOptions {
4780 pub reasoning_effort: Option<String>,
4783 pub reasoning_summary: Option<ReasoningSummary>,
4787 pub context_tier: Option<ContextTier>,
4790 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4794}
4795
4796impl SetModelOptions {
4797 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4799 self.reasoning_effort = Some(effort.into());
4800 self
4801 }
4802
4803 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4805 self.reasoning_summary = Some(summary);
4806 self
4807 }
4808
4809 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4811 self.context_tier = Some(tier);
4812 self
4813 }
4814
4815 pub fn with_model_capabilities(
4817 mut self,
4818 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4819 ) -> Self {
4820 self.model_capabilities = Some(caps);
4821 self
4822 }
4823}
4824
4825#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4832#[serde(rename_all = "camelCase")]
4833pub struct PingResponse {
4834 #[serde(default)]
4836 pub message: String,
4837 #[serde(default)]
4839 pub timestamp: String,
4840 #[serde(skip_serializing_if = "Option::is_none")]
4842 pub protocol_version: Option<u32>,
4843}
4844
4845#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4847#[serde(rename_all = "camelCase")]
4848pub struct AttachmentLineRange {
4849 pub start: u32,
4851 pub end: u32,
4853}
4854
4855#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4857#[serde(rename_all = "camelCase")]
4858pub struct AttachmentSelectionPosition {
4859 pub line: u32,
4861 pub character: u32,
4863}
4864
4865#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4867#[serde(rename_all = "camelCase")]
4868pub struct AttachmentSelectionRange {
4869 pub start: AttachmentSelectionPosition,
4871 pub end: AttachmentSelectionPosition,
4873}
4874
4875#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4877#[serde(rename_all = "snake_case")]
4878#[non_exhaustive]
4879pub enum GitHubReferenceType {
4880 Issue,
4882 Pr,
4884 Discussion,
4886}
4887
4888#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4894#[serde(rename_all = "camelCase")]
4895pub struct GitHubRepoPointer {
4896 #[serde(skip_serializing_if = "Option::is_none")]
4898 pub id: Option<i64>,
4899 pub name: String,
4901 pub owner: String,
4903}
4904
4905#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4907#[serde(rename_all = "camelCase")]
4908pub struct GitHubFileDiffSide {
4909 pub path: String,
4911 pub r#ref: String,
4913 pub repo: GitHubRepoPointer,
4915}
4916
4917#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4919#[serde(rename_all = "camelCase")]
4920pub struct GitHubTreeComparisonSide {
4921 pub repo: GitHubRepoPointer,
4923 pub revision: String,
4925}
4926
4927#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4929#[serde(rename_all = "camelCase")]
4930pub struct GitHubSnippetLineRange {
4931 pub start: i64,
4933 pub end: i64,
4935}
4936
4937#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4939#[serde(
4940 tag = "type",
4941 rename_all = "camelCase",
4942 rename_all_fields = "camelCase"
4943)]
4944#[non_exhaustive]
4945pub enum Attachment {
4946 File {
4948 path: PathBuf,
4950 #[serde(skip_serializing_if = "Option::is_none")]
4952 display_name: Option<String>,
4953 #[serde(skip_serializing_if = "Option::is_none")]
4955 line_range: Option<AttachmentLineRange>,
4956 },
4957 Directory {
4959 path: PathBuf,
4961 #[serde(skip_serializing_if = "Option::is_none")]
4963 display_name: Option<String>,
4964 },
4965 Selection {
4967 file_path: PathBuf,
4969 text: String,
4971 #[serde(skip_serializing_if = "Option::is_none")]
4973 display_name: Option<String>,
4974 selection: AttachmentSelectionRange,
4976 },
4977 Blob {
4979 data: String,
4981 mime_type: String,
4983 #[serde(skip_serializing_if = "Option::is_none")]
4985 display_name: Option<String>,
4986 },
4987 #[serde(rename = "github_reference")]
4989 GitHubReference {
4990 number: u64,
4992 title: String,
4994 reference_type: GitHubReferenceType,
4996 state: String,
4998 url: String,
5000 },
5001 #[serde(rename = "github_commit")]
5003 GitHubCommit {
5004 message: String,
5006 oid: String,
5008 repo: GitHubRepoPointer,
5010 url: String,
5012 },
5013 #[serde(rename = "github_release")]
5015 GitHubRelease {
5016 name: String,
5018 repo: GitHubRepoPointer,
5020 tag_name: String,
5022 url: String,
5024 },
5025 #[serde(rename = "github_actions_job")]
5027 GitHubActionsJob {
5028 #[serde(skip_serializing_if = "Option::is_none")]
5031 conclusion: Option<String>,
5032 job_id: i64,
5034 job_name: String,
5036 repo: GitHubRepoPointer,
5038 url: String,
5040 workflow_name: String,
5042 },
5043 #[serde(rename = "github_repository")]
5045 GitHubRepository {
5046 #[serde(skip_serializing_if = "Option::is_none")]
5048 description: Option<String>,
5049 #[serde(skip_serializing_if = "Option::is_none")]
5052 r#ref: Option<String>,
5053 repo: GitHubRepoPointer,
5055 url: String,
5057 },
5058 #[serde(rename = "github_file_diff")]
5060 GitHubFileDiff {
5061 #[serde(skip_serializing_if = "Option::is_none")]
5063 base: Option<GitHubFileDiffSide>,
5064 #[serde(skip_serializing_if = "Option::is_none")]
5066 head: Option<GitHubFileDiffSide>,
5067 url: String,
5069 },
5070 #[serde(rename = "github_tree_comparison")]
5072 GitHubTreeComparison {
5073 base: GitHubTreeComparisonSide,
5075 head: GitHubTreeComparisonSide,
5077 url: String,
5079 },
5080 #[serde(rename = "github_url")]
5082 GitHubUrl {
5083 url: String,
5085 },
5086 #[serde(rename = "github_file")]
5088 GitHubFile {
5089 path: String,
5091 r#ref: String,
5093 repo: GitHubRepoPointer,
5095 url: String,
5097 },
5098 #[serde(rename = "github_snippet")]
5100 GitHubSnippet {
5101 line_range: GitHubSnippetLineRange,
5103 path: String,
5105 r#ref: String,
5107 repo: GitHubRepoPointer,
5109 url: String,
5111 },
5112}
5113
5114impl Attachment {
5115 pub fn display_name(&self) -> Option<&str> {
5117 match self {
5118 Self::File { display_name, .. }
5119 | Self::Directory { display_name, .. }
5120 | Self::Selection { display_name, .. }
5121 | Self::Blob { display_name, .. } => display_name.as_deref(),
5122 Self::GitHubReference { .. }
5123 | Self::GitHubCommit { .. }
5124 | Self::GitHubRelease { .. }
5125 | Self::GitHubActionsJob { .. }
5126 | Self::GitHubRepository { .. }
5127 | Self::GitHubFileDiff { .. }
5128 | Self::GitHubTreeComparison { .. }
5129 | Self::GitHubUrl { .. }
5130 | Self::GitHubFile { .. }
5131 | Self::GitHubSnippet { .. } => None,
5132 }
5133 }
5134
5135 pub fn label(&self) -> Option<String> {
5137 if let Some(display_name) = self
5138 .display_name()
5139 .map(str::trim)
5140 .filter(|name| !name.is_empty())
5141 {
5142 return Some(display_name.to_string());
5143 }
5144
5145 match self {
5146 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
5147 format!("#{}", number)
5148 } else {
5149 title.trim().to_string()
5150 }),
5151 _ => self.derived_display_name(),
5152 }
5153 }
5154
5155 pub fn ensure_display_name(&mut self) {
5157 if self
5158 .display_name()
5159 .map(str::trim)
5160 .is_some_and(|name| !name.is_empty())
5161 {
5162 return;
5163 }
5164
5165 let Some(derived_display_name) = self.derived_display_name() else {
5166 return;
5167 };
5168
5169 match self {
5170 Self::File { display_name, .. }
5171 | Self::Directory { display_name, .. }
5172 | Self::Selection { display_name, .. }
5173 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
5174 Self::GitHubReference { .. }
5175 | Self::GitHubCommit { .. }
5176 | Self::GitHubRelease { .. }
5177 | Self::GitHubActionsJob { .. }
5178 | Self::GitHubRepository { .. }
5179 | Self::GitHubFileDiff { .. }
5180 | Self::GitHubTreeComparison { .. }
5181 | Self::GitHubUrl { .. }
5182 | Self::GitHubFile { .. }
5183 | Self::GitHubSnippet { .. } => {}
5184 }
5185 }
5186
5187 fn derived_display_name(&self) -> Option<String> {
5188 match self {
5189 Self::File { path, .. } | Self::Directory { path, .. } => {
5190 Some(attachment_name_from_path(path))
5191 }
5192 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
5193 Self::Blob { .. } => Some("attachment".to_string()),
5194 Self::GitHubReference { .. }
5195 | Self::GitHubCommit { .. }
5196 | Self::GitHubRelease { .. }
5197 | Self::GitHubActionsJob { .. }
5198 | Self::GitHubRepository { .. }
5199 | Self::GitHubFileDiff { .. }
5200 | Self::GitHubTreeComparison { .. }
5201 | Self::GitHubUrl { .. }
5202 | Self::GitHubFile { .. }
5203 | Self::GitHubSnippet { .. } => None,
5204 }
5205 }
5206}
5207
5208fn attachment_name_from_path(path: &Path) -> String {
5209 path.file_name()
5210 .map(|name| name.to_string_lossy().into_owned())
5211 .filter(|name| !name.is_empty())
5212 .unwrap_or_else(|| {
5213 let full = path.to_string_lossy();
5214 if full.is_empty() {
5215 "attachment".to_string()
5216 } else {
5217 full.into_owned()
5218 }
5219 })
5220}
5221
5222pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
5224 for attachment in attachments {
5225 attachment.ensure_display_name();
5226 }
5227}
5228
5229#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5234#[serde(rename_all = "lowercase")]
5235#[non_exhaustive]
5236pub enum DeliveryMode {
5237 Enqueue,
5239 Immediate,
5241}
5242
5243#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5248#[serde(rename_all = "lowercase")]
5249#[non_exhaustive]
5250pub enum AgentMode {
5251 Interactive,
5253 Plan,
5255 Autopilot,
5257 Shell,
5259}
5260
5261#[derive(Debug, Clone)]
5290#[non_exhaustive]
5291pub struct MessageOptions {
5292 pub prompt: String,
5294 pub mode: Option<DeliveryMode>,
5300 pub agent_mode: Option<AgentMode>,
5304 pub attachments: Option<Vec<Attachment>>,
5306 pub wait_timeout: Option<Duration>,
5309 pub request_headers: Option<HashMap<String, String>>,
5313 pub traceparent: Option<String>,
5320 pub tracestate: Option<String>,
5324 pub display_prompt: Option<String>,
5326}
5327
5328impl MessageOptions {
5329 pub fn new(prompt: impl Into<String>) -> Self {
5331 Self {
5332 prompt: prompt.into(),
5333 mode: None,
5334 agent_mode: None,
5335 attachments: None,
5336 wait_timeout: None,
5337 request_headers: None,
5338 traceparent: None,
5339 tracestate: None,
5340 display_prompt: None,
5341 }
5342 }
5343
5344 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
5350 self.mode = Some(mode);
5351 self
5352 }
5353
5354 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
5358 self.agent_mode = Some(agent_mode);
5359 self
5360 }
5361
5362 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
5364 self.attachments = Some(attachments);
5365 self
5366 }
5367
5368 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
5370 self.wait_timeout = Some(timeout);
5371 self
5372 }
5373
5374 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
5376 self.request_headers = Some(headers);
5377 self
5378 }
5379
5380 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
5385 self.traceparent = ctx.traceparent;
5386 self.tracestate = ctx.tracestate;
5387 self
5388 }
5389
5390 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5392 self.traceparent = Some(traceparent.into());
5393 self
5394 }
5395
5396 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5398 self.tracestate = Some(tracestate.into());
5399 self
5400 }
5401
5402 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5404 self.display_prompt = Some(display_prompt.into());
5405 self
5406 }
5407}
5408
5409impl From<&str> for MessageOptions {
5410 fn from(prompt: &str) -> Self {
5411 Self::new(prompt)
5412 }
5413}
5414
5415impl From<String> for MessageOptions {
5416 fn from(prompt: String) -> Self {
5417 Self::new(prompt)
5418 }
5419}
5420
5421impl From<&String> for MessageOptions {
5422 fn from(prompt: &String) -> Self {
5423 Self::new(prompt.clone())
5424 }
5425}
5426
5427#[derive(Debug, Clone, Serialize, Deserialize)]
5429#[serde(rename_all = "camelCase")]
5430#[non_exhaustive]
5431pub struct GetStatusResponse {
5432 pub version: String,
5434 pub protocol_version: u32,
5436}
5437
5438#[derive(Debug, Clone, Serialize, Deserialize)]
5440#[serde(rename_all = "camelCase")]
5441#[non_exhaustive]
5442pub struct GetAuthStatusResponse {
5443 pub is_authenticated: bool,
5445 #[serde(skip_serializing_if = "Option::is_none")]
5448 pub auth_type: Option<String>,
5449 #[serde(skip_serializing_if = "Option::is_none")]
5451 pub host: Option<String>,
5452 #[serde(skip_serializing_if = "Option::is_none")]
5454 pub login: Option<String>,
5455 #[serde(skip_serializing_if = "Option::is_none")]
5457 pub status_message: Option<String>,
5458}
5459
5460#[derive(Debug, Clone, Serialize, Deserialize)]
5464#[serde(rename_all = "camelCase")]
5465pub struct SessionEventNotification {
5466 pub session_id: SessionId,
5468 pub event: SessionEvent,
5470}
5471
5472#[derive(Debug, Clone, Serialize, Deserialize)]
5479#[serde(rename_all = "camelCase")]
5480pub struct SessionEvent {
5481 pub id: String,
5483 pub timestamp: String,
5485 pub parent_id: Option<String>,
5487 #[serde(skip_serializing_if = "Option::is_none")]
5489 pub ephemeral: Option<bool>,
5490 #[serde(skip_serializing_if = "Option::is_none")]
5493 pub agent_id: Option<String>,
5494 #[serde(skip_serializing_if = "Option::is_none")]
5496 pub debug_cli_received_at_ms: Option<i64>,
5497 #[serde(skip_serializing_if = "Option::is_none")]
5499 pub debug_ws_forwarded_at_ms: Option<i64>,
5500 #[serde(rename = "type")]
5502 pub event_type: String,
5503 pub data: Value,
5505}
5506
5507impl SessionEvent {
5508 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5513 use serde::de::IntoDeserializer;
5514 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5515 self.event_type.as_str().into_deserializer();
5516 crate::generated::SessionEventType::deserialize(deserializer)
5517 .unwrap_or(crate::generated::SessionEventType::Unknown)
5518 }
5519
5520 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5526 serde_json::from_value(self.data.clone()).ok()
5527 }
5528
5529 pub fn is_transient_error(&self) -> bool {
5533 self.event_type == "session.error"
5534 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5535 }
5536}
5537
5538#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5543#[serde(rename_all = "camelCase")]
5544#[non_exhaustive]
5545pub struct ToolInvocation {
5546 pub session_id: SessionId,
5548 pub tool_call_id: String,
5550 pub tool_name: String,
5552 pub arguments: Value,
5554 #[serde(skip)]
5562 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5563 #[serde(default, skip_serializing_if = "Option::is_none")]
5568 pub traceparent: Option<String>,
5569 #[serde(default, skip_serializing_if = "Option::is_none")]
5572 pub tracestate: Option<String>,
5573}
5574
5575impl ToolInvocation {
5576 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5597 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5598 }
5599
5600 pub fn trace_context(&self) -> TraceContext {
5603 TraceContext {
5604 traceparent: self.traceparent.clone(),
5605 tracestate: self.tracestate.clone(),
5606 }
5607 }
5608}
5609
5610#[derive(Debug, Clone, Serialize, Deserialize)]
5612#[serde(rename_all = "camelCase")]
5613pub struct ToolBinaryResult {
5614 pub data: String,
5616 pub mime_type: String,
5618 pub r#type: String,
5620 #[serde(default, skip_serializing_if = "Option::is_none")]
5622 pub description: Option<String>,
5623}
5624
5625#[derive(Debug, Clone, Serialize, Deserialize)]
5632#[serde(rename_all = "camelCase")]
5633#[non_exhaustive]
5634pub struct ToolResultExpanded {
5635 pub text_result_for_llm: String,
5637 pub result_type: String,
5639 #[serde(default, skip_serializing_if = "Option::is_none")]
5641 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5642 #[serde(skip_serializing_if = "Option::is_none")]
5644 pub session_log: Option<String>,
5645 #[serde(skip_serializing_if = "Option::is_none")]
5647 pub error: Option<String>,
5648 #[serde(default, skip_serializing_if = "Option::is_none")]
5650 pub tool_telemetry: Option<HashMap<String, Value>>,
5651 #[serde(default, skip_serializing_if = "Option::is_none")]
5653 pub tool_references: Option<Vec<String>>,
5654}
5655
5656impl ToolResultExpanded {
5657 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5661 Self {
5662 text_result_for_llm: text_result_for_llm.into(),
5663 result_type: result_type.into(),
5664 binary_results_for_llm: None,
5665 session_log: None,
5666 error: None,
5667 tool_telemetry: None,
5668 tool_references: None,
5669 }
5670 }
5671
5672 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5674 self.binary_results_for_llm = Some(results);
5675 self
5676 }
5677
5678 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5680 self.session_log = Some(session_log.into());
5681 self
5682 }
5683
5684 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5686 self.error = Some(error.into());
5687 self
5688 }
5689
5690 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5692 self.tool_telemetry = Some(telemetry);
5693 self
5694 }
5695
5696 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5698 where
5699 I: IntoIterator<Item = S>,
5700 S: Into<String>,
5701 {
5702 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5703 self
5704 }
5705}
5706
5707#[derive(Debug, Clone, Serialize, Deserialize)]
5709#[serde(untagged)]
5710#[non_exhaustive]
5711pub enum ToolResult {
5712 Text(String),
5714 Expanded(ToolResultExpanded),
5716}
5717
5718#[derive(Debug, Clone, Serialize, Deserialize)]
5720#[serde(rename_all = "camelCase")]
5721pub struct ToolResultResponse {
5722 pub result: ToolResult,
5724}
5725
5726#[derive(Debug, Clone, Serialize, Deserialize)]
5728#[serde(rename_all = "camelCase")]
5729pub struct SessionMetadata {
5730 pub session_id: SessionId,
5732 pub start_time: String,
5734 pub modified_time: String,
5736 #[serde(skip_serializing_if = "Option::is_none")]
5738 pub summary: Option<String>,
5739 pub is_remote: bool,
5741}
5742
5743#[derive(Debug, Clone, Serialize, Deserialize)]
5745#[serde(rename_all = "camelCase")]
5746pub struct ListSessionsResponse {
5747 pub sessions: Vec<SessionMetadata>,
5749}
5750
5751#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5755#[serde(rename_all = "camelCase")]
5756pub struct SessionListFilter {
5757 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5759 pub working_directory: Option<String>,
5760 #[serde(default, skip_serializing_if = "Option::is_none")]
5762 pub git_root: Option<String>,
5763 #[serde(default, skip_serializing_if = "Option::is_none")]
5765 pub repository: Option<String>,
5766 #[serde(default, skip_serializing_if = "Option::is_none")]
5768 pub branch: Option<String>,
5769}
5770
5771#[derive(Debug, Clone, Serialize, Deserialize)]
5773#[serde(rename_all = "camelCase")]
5774pub struct GetSessionMetadataResponse {
5775 #[serde(skip_serializing_if = "Option::is_none")]
5777 pub session: Option<SessionMetadata>,
5778}
5779
5780#[derive(Debug, Clone, Serialize, Deserialize)]
5782#[serde(rename_all = "camelCase")]
5783pub struct GetLastSessionIdResponse {
5784 #[serde(skip_serializing_if = "Option::is_none")]
5786 pub session_id: Option<SessionId>,
5787}
5788
5789#[derive(Debug, Clone, Serialize, Deserialize)]
5791#[serde(rename_all = "camelCase")]
5792pub struct GetForegroundSessionResponse {
5793 #[serde(skip_serializing_if = "Option::is_none")]
5795 pub session_id: Option<SessionId>,
5796}
5797
5798#[derive(Debug, Clone, Serialize, Deserialize)]
5800#[serde(rename_all = "camelCase")]
5801pub struct GetMessagesResponse {
5802 pub events: Vec<SessionEvent>,
5804}
5805
5806#[derive(Debug, Clone, Serialize, Deserialize)]
5808#[serde(rename_all = "camelCase")]
5809pub struct ElicitationResult {
5810 pub action: String,
5812 #[serde(skip_serializing_if = "Option::is_none")]
5814 pub content: Option<Value>,
5815}
5816
5817#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5823#[serde(rename_all = "camelCase")]
5824#[non_exhaustive]
5825pub enum ElicitationMode {
5826 Form,
5828 Url,
5830 #[serde(other)]
5832 Unknown,
5833}
5834
5835#[derive(Debug, Clone, Serialize, Deserialize)]
5842#[serde(rename_all = "camelCase")]
5843pub struct ElicitationRequest {
5844 pub message: String,
5846 #[serde(skip_serializing_if = "Option::is_none")]
5848 pub requested_schema: Option<Value>,
5849 #[serde(skip_serializing_if = "Option::is_none")]
5851 pub mode: Option<ElicitationMode>,
5852 #[serde(skip_serializing_if = "Option::is_none")]
5854 pub elicitation_source: Option<String>,
5855 #[serde(skip_serializing_if = "Option::is_none")]
5857 pub url: Option<String>,
5858}
5859
5860#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5865#[serde(rename_all = "camelCase")]
5866pub struct SessionCapabilities {
5867 #[serde(skip_serializing_if = "Option::is_none")]
5869 pub ui: Option<UiCapabilities>,
5870}
5871
5872#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5874#[serde(rename_all = "camelCase")]
5875pub struct UiCapabilities {
5876 #[serde(skip_serializing_if = "Option::is_none")]
5878 pub elicitation: Option<bool>,
5879 #[serde(skip_serializing_if = "Option::is_none")]
5890 pub mcp_apps: Option<bool>,
5891 #[serde(skip_serializing_if = "Option::is_none")]
5893 pub canvases: Option<bool>,
5894}
5895
5896#[derive(Debug, Clone, Default)]
5898pub struct UiInputOptions<'a> {
5899 pub title: Option<&'a str>,
5901 pub description: Option<&'a str>,
5903 pub min_length: Option<u64>,
5905 pub max_length: Option<u64>,
5907 pub format: Option<InputFormat>,
5909 pub default: Option<&'a str>,
5911}
5912
5913#[derive(Debug, Clone, Copy)]
5915#[non_exhaustive]
5916pub enum InputFormat {
5917 Email,
5919 Uri,
5921 Date,
5923 DateTime,
5925}
5926
5927impl InputFormat {
5928 pub fn as_str(&self) -> &'static str {
5930 match self {
5931 Self::Email => "email",
5932 Self::Uri => "uri",
5933 Self::Date => "date",
5934 Self::DateTime => "date-time",
5935 }
5936 }
5937}
5938
5939pub use crate::generated::api_types::{
5944 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5945 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5946 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5947 PermissionDecisionApproveOnce, PermissionDecisionContext, PermissionDecisionOutcome,
5948 PermissionDecisionReject, PermissionDecisionSource, PermissionDecisionSurface,
5949 PermissionDecisionUserNotAvailable, PermissionResponseCapability,
5950};
5951
5952#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5958#[serde(rename_all = "kebab-case")]
5959#[non_exhaustive]
5960pub enum PermissionRequestKind {
5961 Shell,
5963 Write,
5965 Read,
5967 Url,
5969 Mcp,
5971 CustomTool,
5973 Memory,
5975 Hook,
5977 #[serde(other)]
5980 Unknown,
5981}
5982
5983#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5989#[serde(rename_all = "camelCase")]
5990pub struct PermissionRequestData {
5991 #[serde(default, skip_serializing_if = "Option::is_none")]
5995 pub kind: Option<PermissionRequestKind>,
5996 #[serde(default, skip_serializing_if = "Option::is_none")]
5999 pub tool_call_id: Option<String>,
6000 #[serde(default, skip_serializing_if = "Option::is_none")]
6002 pub managed_approval_required: Option<bool>,
6003 #[serde(default, skip_serializing_if = "is_false")]
6005 pub managed_settings_enabled: bool,
6006 #[serde(flatten)]
6010 pub extra: Value,
6011}
6012
6013#[derive(Debug, Clone, Serialize, Deserialize)]
6015#[serde(rename_all = "camelCase")]
6016pub struct ExitPlanModeData {
6017 #[serde(default)]
6019 pub summary: String,
6020 #[serde(default, skip_serializing_if = "Option::is_none")]
6022 pub plan_content: Option<String>,
6023 #[serde(default)]
6025 pub actions: Vec<String>,
6026 #[serde(default = "default_recommended_action")]
6028 pub recommended_action: String,
6029}
6030
6031fn default_recommended_action() -> String {
6032 "autopilot".to_string()
6033}
6034
6035impl Default for ExitPlanModeData {
6036 fn default() -> Self {
6037 Self {
6038 summary: String::new(),
6039 plan_content: None,
6040 actions: Vec::new(),
6041 recommended_action: default_recommended_action(),
6042 }
6043 }
6044}
6045
6046#[cfg(test)]
6047mod tests {
6048 use std::collections::HashMap;
6049 use std::path::PathBuf;
6050
6051 use serde_json::json;
6052
6053 use super::{
6054 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
6055 AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions,
6056 ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode,
6057 ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
6058 InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
6059 MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig,
6060 ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
6061 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
6062 ToolResultResponse, ensure_attachment_display_names,
6063 };
6064 use crate::generated::session_events::TypedSessionEvent;
6065
6066 #[test]
6067 fn permission_response_capability_is_publicly_exported() {
6068 assert_eq!(
6069 serde_json::to_value(PermissionResponseCapability::Interactive).unwrap(),
6070 json!("interactive")
6071 );
6072 }
6073
6074 #[test]
6075 fn tool_builder_composes() {
6076 let tool = Tool::new("greet")
6077 .with_description("Say hello")
6078 .with_namespaced_name("hello/greet")
6079 .with_instructions("Pass the user's name")
6080 .with_parameters(json!({
6081 "type": "object",
6082 "properties": { "name": { "type": "string" } },
6083 "required": ["name"]
6084 }))
6085 .with_overrides_built_in_tool(true)
6086 .with_skip_permission(true);
6087 assert_eq!(tool.name, "greet");
6088 assert_eq!(tool.description, "Say hello");
6089 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
6090 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
6091 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
6092 assert!(tool.overrides_built_in_tool);
6093 assert!(tool.skip_permission);
6094 }
6095
6096 #[test]
6097 fn tool_defer_serialization() {
6098 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
6099 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
6100 let value = serde_json::to_value(&tool).unwrap();
6101 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
6102
6103 let plain = Tool::new("plain");
6104 let value = serde_json::to_value(&plain).unwrap();
6105 assert!(value.get("defer").is_none());
6106 }
6107
6108 #[test]
6109 fn tool_metadata_serialization() {
6110 use indexmap::IndexMap;
6111
6112 let mut metadata = IndexMap::new();
6113 metadata.insert(
6114 "github.com/copilot:safeForTelemetry".to_string(),
6115 json!({ "name": true, "inputsNames": false }),
6116 );
6117 let tool = Tool::new("lookup").with_metadata(metadata);
6118 let value = serde_json::to_value(&tool).unwrap();
6119 assert_eq!(
6120 value
6121 .get("metadata")
6122 .unwrap()
6123 .get("github.com/copilot:safeForTelemetry")
6124 .unwrap(),
6125 &json!({ "name": true, "inputsNames": false })
6126 );
6127
6128 let plain = Tool::new("plain");
6130 let value = serde_json::to_value(&plain).unwrap();
6131 assert!(value.get("metadata").is_none());
6132 }
6133
6134 #[test]
6135 fn custom_agent_config_builder_with_model() {
6136 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
6137 .with_model("claude-haiku-4.5")
6138 .with_display_name("My Agent");
6139 assert_eq!(agent.name, "my-agent");
6140 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
6141 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
6142 }
6143
6144 #[test]
6145 fn custom_agent_config_serializes_model() {
6146 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
6147 let wire = serde_json::to_value(&agent).unwrap();
6148 assert_eq!(wire["model"], "claude-haiku-4.5");
6149 assert_eq!(wire["name"], "model-agent");
6150 }
6151
6152 #[test]
6153 fn custom_agent_config_omits_model_when_none() {
6154 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
6155 let wire = serde_json::to_value(&agent).unwrap();
6156 assert!(wire.get("model").is_none());
6157 }
6158
6159 #[test]
6160 fn custom_agent_config_builder_with_reasoning_effort() {
6161 let agent =
6162 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6163 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
6164 }
6165
6166 #[test]
6167 fn custom_agent_config_serializes_reasoning_effort() {
6168 let agent =
6169 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6170 let wire = serde_json::to_value(&agent).unwrap();
6171 assert_eq!(wire["reasoningEffort"], "high");
6172 }
6173
6174 #[test]
6175 fn custom_agent_config_omits_reasoning_effort_when_none() {
6176 let agent = CustomAgentConfig::new("default-agent", "prompt");
6177 let wire = serde_json::to_value(&agent).unwrap();
6178 assert!(wire.get("reasoningEffort").is_none());
6179 }
6180
6181 #[test]
6182 #[should_panic(expected = "tool parameter schema must be a JSON object")]
6183 fn tool_with_parameters_panics_on_non_object_value() {
6184 let _ = Tool::new("noop").with_parameters(json!(null));
6185 }
6186
6187 #[test]
6188 fn tool_result_expanded_serializes_binary_results_for_llm() {
6189 let response = ToolResultResponse {
6190 result: ToolResult::Expanded(ToolResultExpanded {
6191 text_result_for_llm: "rendered chart".to_string(),
6192 result_type: "success".to_string(),
6193 binary_results_for_llm: Some(vec![ToolBinaryResult {
6194 data: "aW1n".to_string(),
6195 mime_type: "image/png".to_string(),
6196 r#type: "image".to_string(),
6197 description: Some("chart preview".to_string()),
6198 }]),
6199 session_log: None,
6200 error: None,
6201 tool_telemetry: None,
6202 tool_references: None,
6203 }),
6204 };
6205
6206 let wire = serde_json::to_value(&response).unwrap();
6207
6208 assert_eq!(
6209 wire,
6210 json!({
6211 "result": {
6212 "textResultForLlm": "rendered chart",
6213 "resultType": "success",
6214 "binaryResultsForLlm": [
6215 {
6216 "data": "aW1n",
6217 "mimeType": "image/png",
6218 "type": "image",
6219 "description": "chart preview"
6220 }
6221 ]
6222 }
6223 })
6224 );
6225 }
6226
6227 #[test]
6228 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
6229 let response = ToolResultResponse {
6230 result: ToolResult::Expanded(ToolResultExpanded {
6231 text_result_for_llm: "ok".to_string(),
6232 result_type: "success".to_string(),
6233 binary_results_for_llm: None,
6234 session_log: None,
6235 error: None,
6236 tool_telemetry: None,
6237 tool_references: None,
6238 }),
6239 };
6240
6241 let wire = serde_json::to_value(&response).unwrap();
6242
6243 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6244 assert!(wire["result"].get("binaryResultsForLlm").is_none());
6245 }
6246
6247 #[test]
6248 fn tool_result_expanded_serializes_tool_references() {
6249 let response = ToolResultResponse {
6250 result: ToolResult::Expanded(
6251 ToolResultExpanded::new("found 2 tools", "success")
6252 .with_tool_references(["get_weather", "check_status"]),
6253 ),
6254 };
6255
6256 let wire = serde_json::to_value(&response).unwrap();
6257
6258 assert_eq!(
6259 wire,
6260 json!({
6261 "result": {
6262 "textResultForLlm": "found 2 tools",
6263 "resultType": "success",
6264 "toolReferences": ["get_weather", "check_status"]
6265 }
6266 })
6267 );
6268 }
6269
6270 #[test]
6271 fn tool_result_expanded_omits_tool_references_when_none() {
6272 let response = ToolResultResponse {
6273 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
6274 };
6275
6276 let wire = serde_json::to_value(&response).unwrap();
6277
6278 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6279 assert!(wire["result"].get("toolReferences").is_none());
6280 }
6281
6282 #[test]
6283 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
6284 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
6287 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
6288
6289 assert_eq!(
6290 expanded.tool_references.as_deref(),
6291 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6292 );
6293 }
6294
6295 #[test]
6296 fn tool_result_expanded_deserializes_tool_references() {
6297 let wire = json!({
6298 "textResultForLlm": "found tools",
6299 "resultType": "success",
6300 "toolReferences": ["alpha", "beta"]
6301 });
6302
6303 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
6304
6305 assert_eq!(
6306 expanded.tool_references.as_deref(),
6307 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6308 );
6309 }
6310
6311 #[test]
6312 fn session_config_default_wire_flags_off_without_handlers() {
6313 let cfg = SessionConfig::default();
6314 assert_eq!(cfg.mcp_oauth_token_storage, None);
6315 let (wire, _runtime) = cfg
6319 .into_wire(Some(SessionId::from("default-flags")))
6320 .expect("default config has no duplicate handlers");
6321 assert!(!wire.request_user_input);
6322 assert!(!wire.request_permission);
6323 assert!(!wire.request_elicitation);
6324 assert!(!wire.request_exit_plan_mode);
6325 assert!(!wire.request_auto_mode_switch);
6326 assert!(!wire.hooks);
6327 assert!(!wire.request_mcp_apps);
6328 let json = serde_json::to_value(&wire).unwrap();
6329 assert!(json.get("askUserVariant").is_none());
6330 }
6331
6332 #[test]
6333 fn resume_session_config_new_wire_flags_off_without_handlers() {
6334 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
6335 assert_eq!(cfg.mcp_oauth_token_storage, None);
6336 let (wire, _runtime) = cfg
6337 .into_wire()
6338 .expect("default resume config has no duplicate handlers");
6339 assert!(!wire.request_user_input);
6340 assert!(!wire.request_permission);
6341 assert!(!wire.request_elicitation);
6342 assert!(!wire.request_exit_plan_mode);
6343 assert!(!wire.request_auto_mode_switch);
6344 assert!(!wire.hooks);
6345 assert!(!wire.request_mcp_apps);
6346 let json = serde_json::to_value(&wire).unwrap();
6347 assert!(json.get("askUserVariant").is_none());
6348 }
6349
6350 #[test]
6351 fn custom_agents_local_only_serializes_on_create_and_resume() {
6352 let (create_wire, _) = SessionConfig::default()
6353 .with_custom_agents_local_only(false)
6354 .into_wire(Some(SessionId::from("create-locality")))
6355 .expect("create config has no duplicate handlers");
6356 let create_json = serde_json::to_value(&create_wire).unwrap();
6357 assert_eq!(create_json["customAgentsLocalOnly"], false);
6358
6359 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
6360 .with_custom_agents_local_only(false)
6361 .into_wire()
6362 .expect("resume config has no duplicate handlers");
6363 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6364 assert_eq!(resume_json["customAgentsLocalOnly"], false);
6365
6366 let (unset_create_wire, _) = SessionConfig::default()
6367 .into_wire(Some(SessionId::from("create-unset")))
6368 .expect("create config has no duplicate handlers");
6369 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6370 assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
6371
6372 let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
6373 .into_wire()
6374 .expect("resume config has no duplicate handlers");
6375 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6376 assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
6377 }
6378
6379 #[test]
6380 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6381 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
6382 assert_eq!(cfg.enable_mcp_apps, Some(true));
6383
6384 let (wire, _runtime) = cfg
6385 .into_wire(Some(SessionId::from("enable-mcp-apps")))
6386 .expect("enable_mcp_apps config has no duplicate handlers");
6387 assert!(wire.request_mcp_apps);
6388
6389 let json = serde_json::to_value(&wire).unwrap();
6390 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6391 }
6392
6393 #[test]
6394 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6395 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
6396 .with_enable_mcp_apps(true);
6397 assert_eq!(cfg.enable_mcp_apps, Some(true));
6398
6399 let (wire, _runtime) = cfg
6400 .into_wire()
6401 .expect("resume enable_mcp_apps config has no duplicate handlers");
6402 assert!(wire.request_mcp_apps);
6403
6404 let json = serde_json::to_value(&wire).unwrap();
6405 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6406 }
6407
6408 #[test]
6409 fn github_mcp_tool_config_serializes_for_create_and_resume() {
6410 let github_config = GitHubMcpToolConfig::new()
6411 .with_enable_all_tools(true)
6412 .with_additional_toolsets(["repos"])
6413 .with_additional_tools(["get_issue"])
6414 .with_enable_insiders_mode(true)
6415 .with_disable_form_deferral(true);
6416
6417 let (create_wire, _) = SessionConfig::default()
6418 .with_github_mcp_tool_config(github_config.clone())
6419 .into_wire(Some(SessionId::from("github-mcp")))
6420 .expect("create config has no duplicate handlers");
6421 assert_eq!(
6422 serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6423 serde_json::json!({
6424 "enableAllTools": true,
6425 "additionalToolsets": ["repos"],
6426 "additionalTools": ["get_issue"],
6427 "enableInsidersMode": true,
6428 "disableFormDeferral": true,
6429 })
6430 );
6431
6432 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6433 .with_github_mcp_tool_config(github_config)
6434 .into_wire()
6435 .expect("resume config has no duplicate handlers");
6436 assert!(resume_wire.github_mcp_tool_config.is_some());
6437
6438 let (unset_wire, _) = SessionConfig::default()
6439 .into_wire(Some(SessionId::from("github-mcp-unset")))
6440 .expect("default config has no duplicate handlers");
6441 assert!(
6442 serde_json::to_value(&unset_wire)
6443 .unwrap()
6444 .get("githubMcpToolConfig")
6445 .is_none()
6446 );
6447 }
6448
6449 #[test]
6450 fn memory_configuration_constructors_and_serde() {
6451 assert!(MemoryConfiguration::enabled().enabled);
6452 assert!(!MemoryConfiguration::disabled().enabled);
6453 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6454
6455 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6456 assert_eq!(json, serde_json::json!({ "enabled": true }));
6457 }
6458
6459 #[test]
6460 fn session_config_with_memory_serializes() {
6461 let (wire, _runtime) = SessionConfig::default()
6462 .with_memory(MemoryConfiguration::enabled())
6463 .into_wire(Some(SessionId::from("memory-on")))
6464 .expect("no duplicate handlers");
6465 let json = serde_json::to_value(&wire).unwrap();
6466 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6467
6468 let (wire_off, _) = SessionConfig::default()
6469 .with_memory(MemoryConfiguration::disabled())
6470 .into_wire(Some(SessionId::from("memory-off")))
6471 .expect("no duplicate handlers");
6472 let json_off = serde_json::to_value(&wire_off).unwrap();
6473 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6474
6475 let (empty_wire, _) = SessionConfig::default()
6477 .into_wire(Some(SessionId::from("memory-unset")))
6478 .expect("no duplicate handlers");
6479 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6480 assert!(empty_json.get("memory").is_none());
6481 }
6482
6483 #[test]
6484 fn resume_session_config_with_memory_serializes() {
6485 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6486 .with_memory(MemoryConfiguration::enabled())
6487 .into_wire()
6488 .expect("no duplicate handlers");
6489 let json = serde_json::to_value(&wire).unwrap();
6490 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6491
6492 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6494 .into_wire()
6495 .expect("no duplicate handlers");
6496 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6497 assert!(empty_json.get("memory").is_none());
6498 }
6499
6500 #[test]
6501 fn feature_flags_serialize_on_create_and_resume() {
6502 let feature_flags = HashMap::from([
6503 ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true),
6504 ("DISABLED_TEST_FLAG".to_string(), false),
6505 ]);
6506 let expected = serde_json::json!({
6507 "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true,
6508 "DISABLED_TEST_FLAG": false,
6509 });
6510
6511 let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone());
6512 assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags));
6513 let (create_wire, _) = create_config
6514 .into_wire(Some(SessionId::from("feature-flags-create")))
6515 .expect("no duplicate handlers");
6516 let create_json = serde_json::to_value(&create_wire).unwrap();
6517 assert_eq!(create_json["featureFlags"], expected);
6518
6519 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume"))
6520 .with_feature_flags(feature_flags)
6521 .into_wire()
6522 .expect("no duplicate handlers");
6523 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6524 assert_eq!(resume_json["featureFlags"], expected);
6525
6526 let (unset_create_wire, _) = SessionConfig::default()
6527 .into_wire(Some(SessionId::from("feature-flags-create-unset")))
6528 .expect("no duplicate handlers");
6529 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6530 assert!(unset_create_json.get("featureFlags").is_none());
6531
6532 let (unset_resume_wire, _) =
6533 ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset"))
6534 .into_wire()
6535 .expect("no duplicate handlers");
6536 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6537 assert!(unset_resume_json.get("featureFlags").is_none());
6538 }
6539
6540 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6541 CopilotExpAssignmentResponse {
6542 features: vec!["copilot_exp_flag".to_string()],
6543 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6544 configs: vec![ExpConfigEntry {
6545 id: "cfg-1".to_string(),
6546 parameters: HashMap::from([
6547 ("threshold".to_string(), ExpFlagValue::Integer(5)),
6548 ("enabled".to_string(), ExpFlagValue::Bool(true)),
6549 ]),
6550 }],
6551 assignment_context: context.to_string(),
6552 ..Default::default()
6553 }
6554 }
6555
6556 #[test]
6557 fn exp_flag_value_round_trips_all_variants() {
6558 let values = serde_json::json!({
6559 "s": "text",
6560 "i": 7,
6561 "f": 1.5,
6562 "b": true,
6563 "n": null,
6564 });
6565 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6566 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6567 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6568 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6569 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6570 assert_eq!(parsed["n"], ExpFlagValue::Null);
6571 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6572 }
6573
6574 #[test]
6575 fn session_config_with_exp_assignments_serializes() {
6576 let assignments = sample_exp_assignments("ctx-123");
6577 let expected = serde_json::to_value(&assignments).unwrap();
6578 let (wire, _runtime) = SessionConfig::default()
6579 .with_exp_assignments(assignments)
6580 .into_wire(Some(SessionId::from("exp-on")))
6581 .expect("no duplicate handlers");
6582 let json = serde_json::to_value(&wire).unwrap();
6583 assert_eq!(json["expAssignments"], expected);
6584 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6585 assert_eq!(
6586 json["expAssignments"]["Flights"]["copilot_exp_flag"],
6587 "treatment"
6588 );
6589
6590 let (empty_wire, _) = SessionConfig::default()
6592 .into_wire(Some(SessionId::from("exp-unset")))
6593 .expect("no duplicate handlers");
6594 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6595 assert!(empty_json.get("expAssignments").is_none());
6596 }
6597
6598 #[test]
6599 fn resume_session_config_with_exp_assignments_serializes() {
6600 let assignments = sample_exp_assignments("ctx-456");
6601 let expected = serde_json::to_value(&assignments).unwrap();
6602 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6603 .with_exp_assignments(assignments)
6604 .into_wire()
6605 .expect("no duplicate handlers");
6606 let json = serde_json::to_value(&wire).unwrap();
6607 assert_eq!(json["expAssignments"], expected);
6608
6609 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6611 .into_wire()
6612 .expect("no duplicate handlers");
6613 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6614 assert!(empty_json.get("expAssignments").is_none());
6615 }
6616
6617 #[test]
6618 fn session_config_clone_preserves_exp_assignments() {
6619 let assignments = sample_exp_assignments("ctx-clone");
6620 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6621 let cloned = config.clone();
6622
6623 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6624
6625 let (wire, _runtime) = cloned
6626 .into_wire(Some(SessionId::from("exp-clone")))
6627 .expect("no duplicate handlers");
6628 let json = serde_json::to_value(&wire).unwrap();
6629 assert_eq!(
6630 json["expAssignments"],
6631 serde_json::to_value(&assignments).unwrap()
6632 );
6633 }
6634
6635 #[test]
6636 fn resume_session_config_clone_preserves_exp_assignments() {
6637 let assignments = sample_exp_assignments("ctx-clone-resume");
6638 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6639 .with_exp_assignments(assignments.clone());
6640 let cloned = config.clone();
6641
6642 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6643
6644 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6645 let json = serde_json::to_value(&wire).unwrap();
6646 assert_eq!(
6647 json["expAssignments"],
6648 serde_json::to_value(&assignments).unwrap()
6649 );
6650 }
6651
6652 #[test]
6653 #[allow(clippy::field_reassign_with_default)]
6654 fn session_config_into_wire_serializes_bucket_b_fields() {
6655 use std::path::PathBuf;
6656
6657 use super::{CloudSessionOptions, CloudSessionRepository};
6658
6659 let mut cfg = SessionConfig::default();
6660 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6661 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6662 cfg.github_token = Some("ghs_secret".to_string());
6663 cfg.include_sub_agent_streaming_events = Some(false);
6664 cfg.enable_session_telemetry = Some(false);
6665 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
6666 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
6667 cfg.enable_on_demand_instruction_discovery = Some(false);
6668 cfg.cloud = Some(CloudSessionOptions::with_repository(
6669 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
6670 ));
6671
6672 let (wire, _runtime) = cfg
6673 .into_wire(Some(SessionId::from("custom-id")))
6674 .expect("no duplicate handlers");
6675 let wire_json = serde_json::to_value(&wire).unwrap();
6676 assert_eq!(wire_json["sessionId"], "custom-id");
6677 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6678 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6679 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6680 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6681 assert_eq!(wire_json["enableSessionTelemetry"], false);
6682 assert_eq!(wire_json["reasoningSummary"], "concise");
6683 assert_eq!(wire_json["remoteSession"], "export");
6684 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6685 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6686 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6687 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6688
6689 let (empty_wire, _) = SessionConfig::default()
6691 .into_wire(Some(SessionId::from("empty")))
6692 .expect("default has no duplicate handlers");
6693 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6694 assert!(empty_json.get("gitHubToken").is_none());
6695 assert!(empty_json.get("enableSessionTelemetry").is_none());
6696 assert!(empty_json.get("reasoningSummary").is_none());
6697 assert!(empty_json.get("remoteSession").is_none());
6698 assert!(
6699 empty_json
6700 .get("enableOnDemandInstructionDiscovery")
6701 .is_none()
6702 );
6703 assert!(empty_json.get("cloud").is_none());
6704 }
6705
6706 #[test]
6707 fn session_config_into_wire_serializes_named_providers_and_models() {
6708 let cfg = SessionConfig::default()
6709 .with_providers(vec![
6710 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6711 .with_provider_type("openai")
6712 .with_wire_api("responses")
6713 .with_api_key("sk-test"),
6714 ])
6715 .with_models(vec![
6716 ProviderModelConfig::new("gpt-x", "my-openai")
6717 .with_wire_model("gpt-x-2025")
6718 .with_max_output_tokens(2048),
6719 ]);
6720
6721 let (wire, _) = cfg
6722 .into_wire(Some(SessionId::from("sess-providers")))
6723 .expect("no duplicate handlers");
6724 let wire_json = serde_json::to_value(&wire).unwrap();
6725 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6726 assert_eq!(
6727 wire_json["providers"][0]["baseUrl"],
6728 "https://api.example.com/v1"
6729 );
6730 assert_eq!(wire_json["providers"][0]["type"], "openai");
6731 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6732 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6733 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
6734 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
6735 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
6736 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
6737
6738 let (empty_wire, _) = SessionConfig::default()
6739 .into_wire(Some(SessionId::from("empty")))
6740 .expect("default has no duplicate handlers");
6741 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6742 assert!(empty_json.get("providers").is_none());
6743 assert!(empty_json.get("models").is_none());
6744 }
6745
6746 #[test]
6747 fn resume_config_into_wire_serializes_named_providers_and_models() {
6748 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
6749 .with_providers(vec![
6750 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
6751 .with_provider_type("azure")
6752 .with_azure(AzureProviderOptions {
6753 api_version: Some("2024-10-21".to_string()),
6754 }),
6755 ])
6756 .with_models(vec![
6757 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
6758 ]);
6759
6760 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6761 let wire_json = serde_json::to_value(&wire).unwrap();
6762 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
6763 assert_eq!(wire_json["providers"][0]["type"], "azure");
6764 assert_eq!(
6765 wire_json["providers"][0]["azure"]["apiVersion"],
6766 "2024-10-21"
6767 );
6768 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
6769 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
6770 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
6771
6772 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
6773 .into_wire()
6774 .expect("default has no duplicate handlers");
6775 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6776 assert!(empty_json.get("providers").is_none());
6777 assert!(empty_json.get("models").is_none());
6778 }
6779
6780 #[test]
6781 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
6782 use std::path::PathBuf;
6783
6784 let cfg = SessionConfig {
6785 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
6786 disabled_mcp_servers: Some(vec![
6787 "local-files".to_string(),
6788 "remote-github".to_string(),
6789 ]),
6790 large_output: Some(
6791 LargeToolOutputConfig::new()
6792 .with_enabled(true)
6793 .with_max_size_bytes(1024)
6794 .with_output_directory(PathBuf::from("/tmp/large-output")),
6795 ),
6796 ..Default::default()
6797 };
6798
6799 let (wire, _) = cfg
6800 .into_wire(Some(SessionId::from("sess-1")))
6801 .expect("no duplicate handlers");
6802 let wire_json = serde_json::to_value(&wire).unwrap();
6803 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
6804 assert_eq!(
6805 wire_json["disabledMcpServers"],
6806 serde_json::json!(["local-files", "remote-github"])
6807 );
6808 assert_eq!(wire_json["largeOutput"]["enabled"], true);
6809 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
6810 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
6811
6812 let (empty_wire, _) = SessionConfig::default()
6813 .into_wire(Some(SessionId::from("empty")))
6814 .expect("default has no duplicate handlers");
6815 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6816 assert!(empty_json.get("pluginDirectories").is_none());
6817 assert!(empty_json.get("disabledMcpServers").is_none());
6818 assert!(empty_json.get("largeOutput").is_none());
6819 }
6820
6821 #[test]
6822 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
6823 use std::path::PathBuf;
6824
6825 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6826 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6827 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6828 cfg.github_token = Some("ghs_secret".to_string());
6829 cfg.include_sub_agent_streaming_events = Some(true);
6830 cfg.enable_session_telemetry = Some(false);
6831 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
6832 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
6833 cfg.enable_on_demand_instruction_discovery = Some(false);
6834
6835 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6836 let wire_json = serde_json::to_value(&wire).unwrap();
6837 assert_eq!(wire_json["sessionId"], "sess-1");
6838 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6839 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6840 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6841 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
6842 assert_eq!(wire_json["enableSessionTelemetry"], false);
6843 assert_eq!(wire_json["reasoningSummary"], "detailed");
6844 assert_eq!(wire_json["remoteSession"], "on");
6845 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6846
6847 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6849 .into_wire()
6850 .expect("default resume has no duplicate handlers");
6851 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6852 assert!(empty_json.get("reasoningSummary").is_none());
6853 assert!(empty_json.get("remoteSession").is_none());
6854 assert!(
6855 empty_json
6856 .get("enableOnDemandInstructionDiscovery")
6857 .is_none()
6858 );
6859 }
6860
6861 #[test]
6862 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
6863 use std::path::PathBuf;
6864
6865 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6866 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6867 cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]);
6868 cfg.large_output = Some(
6869 LargeToolOutputConfig::new()
6870 .with_enabled(false)
6871 .with_max_size_bytes(2048)
6872 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6873 );
6874
6875 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6876 let wire_json = serde_json::to_value(&wire).unwrap();
6877 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6878 assert_eq!(
6879 wire_json["disabledMcpServers"],
6880 serde_json::json!(["local-files-r"])
6881 );
6882 assert_eq!(wire_json["largeOutput"]["enabled"], false);
6883 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6884 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6885
6886 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6887 .into_wire()
6888 .expect("default resume has no duplicate handlers");
6889 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6890 assert!(empty_json.get("pluginDirectories").is_none());
6891 assert!(empty_json.get("disabledMcpServers").is_none());
6892 assert!(empty_json.get("largeOutput").is_none());
6893 }
6894
6895 #[test]
6896 fn session_config_clones_disabled_mcp_servers() {
6897 let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]);
6898 let mut create_clone = create.clone();
6899 create_clone
6900 .disabled_mcp_servers
6901 .as_mut()
6902 .expect("configured disabled MCP servers")
6903 .push("remote-github".to_string());
6904 assert_eq!(
6905 create.disabled_mcp_servers.as_deref(),
6906 Some(&["local-files".to_string()][..])
6907 );
6908
6909 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
6910 .with_disabled_mcp_servers(["local-files"]);
6911 let mut resume_clone = resume.clone();
6912 resume_clone
6913 .disabled_mcp_servers
6914 .as_mut()
6915 .expect("configured disabled MCP servers")
6916 .push("remote-github".to_string());
6917 assert_eq!(
6918 resume.disabled_mcp_servers.as_deref(),
6919 Some(&["local-files".to_string()][..])
6920 );
6921 }
6922
6923 #[test]
6924 fn session_config_builder_composes() {
6925 use indexmap::IndexMap;
6926
6927 let cfg = SessionConfig::default()
6928 .with_session_id(SessionId::from("sess-1"))
6929 .with_model("claude-sonnet-4")
6930 .with_client_name("test-app")
6931 .with_reasoning_effort("medium")
6932 .with_reasoning_summary(ReasoningSummary::Concise)
6933 .with_context_tier("long_context")
6934 .with_streaming(true)
6935 .with_tools([Tool::new("greet")])
6936 .with_available_tools(["bash", "view"])
6937 .with_excluded_tools(["dangerous"])
6938 .with_mcp_servers(IndexMap::new())
6939 .with_mcp_oauth_token_storage("persistent")
6940 .with_enable_config_discovery(true)
6941 .with_enable_on_demand_instruction_discovery(true)
6942 .with_skill_directories([PathBuf::from("/tmp/skills")])
6943 .with_disabled_skills(["broken-skill"])
6944 .with_disabled_mcp_servers(["local-files"])
6945 .with_agent("researcher")
6946 .with_config_directory(PathBuf::from("/tmp/config"))
6947 .with_working_directory(PathBuf::from("/tmp/work"))
6948 .with_additional_directories([PathBuf::from("/tmp/shared")])
6949 .with_github_token("ghp_test")
6950 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6951 .with_enable_session_telemetry(false)
6952 .with_include_sub_agent_streaming_events(false)
6953 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6954
6955 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
6956 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
6957 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6958 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
6959 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
6960 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
6961 assert_eq!(cfg.streaming, Some(true));
6962 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6963 assert_eq!(
6964 cfg.available_tools.as_deref(),
6965 Some(&["bash".to_string(), "view".to_string()][..])
6966 );
6967 assert_eq!(
6968 cfg.excluded_tools.as_deref(),
6969 Some(&["dangerous".to_string()][..])
6970 );
6971 assert!(cfg.mcp_servers.is_some());
6972 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6973 assert_eq!(cfg.enable_config_discovery, Some(true));
6974 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
6975 assert_eq!(
6976 cfg.skill_directories.as_deref(),
6977 Some(&[PathBuf::from("/tmp/skills")][..])
6978 );
6979 assert_eq!(
6980 cfg.disabled_skills.as_deref(),
6981 Some(&["broken-skill".to_string()][..])
6982 );
6983 assert_eq!(
6984 cfg.disabled_mcp_servers.as_deref(),
6985 Some(&["local-files".to_string()][..])
6986 );
6987 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6988 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6989 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6990 assert_eq!(
6991 cfg.additional_directories.as_deref(),
6992 Some(&[PathBuf::from("/tmp/shared")][..])
6993 );
6994 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6995 assert_eq!(
6996 cfg.capi,
6997 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6998 );
6999 assert_eq!(cfg.enable_session_telemetry, Some(false));
7000 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
7001 assert_eq!(
7002 cfg.extension_info,
7003 Some(ExtensionInfo::new("github-app", "counter"))
7004 );
7005 }
7006
7007 #[test]
7008 fn resume_session_config_builder_composes() {
7009 use indexmap::IndexMap;
7010
7011 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
7012 .with_client_name("test-app")
7013 .with_reasoning_summary(ReasoningSummary::None)
7014 .with_context_tier("default")
7015 .with_streaming(true)
7016 .with_tools([Tool::new("greet")])
7017 .with_available_tools(["bash", "view"])
7018 .with_excluded_tools(["dangerous"])
7019 .with_mcp_servers(IndexMap::new())
7020 .with_mcp_oauth_token_storage("persistent")
7021 .with_enable_config_discovery(true)
7022 .with_enable_on_demand_instruction_discovery(false)
7023 .with_skill_directories([PathBuf::from("/tmp/skills")])
7024 .with_disabled_skills(["broken-skill"])
7025 .with_disabled_mcp_servers(["local-files"])
7026 .with_agent("researcher")
7027 .with_config_directory(PathBuf::from("/tmp/config"))
7028 .with_working_directory(PathBuf::from("/tmp/work"))
7029 .with_additional_directories([PathBuf::from("/tmp/shared")])
7030 .with_github_token("ghp_test")
7031 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7032 .with_enable_session_telemetry(false)
7033 .with_include_sub_agent_streaming_events(true)
7034 .with_suppress_resume_event(true)
7035 .with_continue_pending_work(true)
7036 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
7037
7038 assert_eq!(cfg.session_id.as_str(), "sess-2");
7039 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
7040 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
7041 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
7042 assert_eq!(cfg.streaming, Some(true));
7043 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
7044 assert_eq!(
7045 cfg.available_tools.as_deref(),
7046 Some(&["bash".to_string(), "view".to_string()][..])
7047 );
7048 assert_eq!(
7049 cfg.excluded_tools.as_deref(),
7050 Some(&["dangerous".to_string()][..])
7051 );
7052 assert!(cfg.mcp_servers.is_some());
7053 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
7054 assert_eq!(cfg.enable_config_discovery, Some(true));
7055 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
7056 assert_eq!(
7057 cfg.skill_directories.as_deref(),
7058 Some(&[PathBuf::from("/tmp/skills")][..])
7059 );
7060 assert_eq!(
7061 cfg.disabled_skills.as_deref(),
7062 Some(&["broken-skill".to_string()][..])
7063 );
7064 assert_eq!(
7065 cfg.disabled_mcp_servers.as_deref(),
7066 Some(&["local-files".to_string()][..])
7067 );
7068 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
7069 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
7070 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
7071 assert_eq!(
7072 cfg.additional_directories.as_deref(),
7073 Some(&[PathBuf::from("/tmp/shared")][..])
7074 );
7075 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
7076 assert_eq!(
7077 cfg.capi,
7078 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7079 );
7080 assert_eq!(cfg.enable_session_telemetry, Some(false));
7081 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
7082 assert_eq!(cfg.suppress_resume_event, Some(true));
7083 assert_eq!(cfg.continue_pending_work, Some(true));
7084 assert_eq!(
7085 cfg.extension_info,
7086 Some(ExtensionInfo::new("github-app", "counter"))
7087 );
7088 }
7089
7090 #[test]
7094 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
7095 let cfg =
7096 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
7097 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7098 let json = serde_json::to_value(&wire).unwrap();
7099 assert_eq!(json["continuePendingWork"], true);
7100
7101 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7103 .into_wire()
7104 .expect("no duplicate handlers");
7105 let json = serde_json::to_value(&wire).unwrap();
7106 assert!(json.get("continuePendingWork").is_none());
7107 }
7108
7109 #[test]
7110 fn session_configs_serialize_additional_directories() {
7111 let create = SessionConfig::default().with_additional_directories([
7112 PathBuf::from("/tmp/shared"),
7113 PathBuf::from("/tmp/generated"),
7114 ]);
7115 let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
7116 let create_json = serde_json::to_value(&create_wire).unwrap();
7117 assert_eq!(
7118 create_json["additionalDirectories"],
7119 serde_json::json!(["/tmp/shared", "/tmp/generated"])
7120 );
7121
7122 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
7123 .with_additional_directories([PathBuf::from("/tmp/resumed")]);
7124 let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
7125 let resume_json = serde_json::to_value(&resume_wire).unwrap();
7126 assert_eq!(
7127 resume_json["additionalDirectories"],
7128 serde_json::json!(["/tmp/resumed"])
7129 );
7130 }
7131
7132 #[test]
7136 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
7137 let cfg =
7138 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
7139 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7140 let json = serde_json::to_value(&wire).unwrap();
7141 assert_eq!(json["disableResume"], true);
7142 assert!(json.get("suppressResumeEvent").is_none());
7143 }
7144
7145 #[test]
7148 fn session_config_serializes_instruction_directories_to_camel_case() {
7149 let cfg =
7150 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
7151 let (wire, _) = cfg
7152 .into_wire(Some(SessionId::from("instr-on")))
7153 .expect("no duplicate handlers");
7154 let json = serde_json::to_value(&wire).unwrap();
7155 assert_eq!(
7156 json["instructionDirectories"],
7157 serde_json::json!(["/tmp/instr"])
7158 );
7159
7160 let (wire, _) = SessionConfig::default()
7162 .into_wire(Some(SessionId::from("instr-off")))
7163 .expect("no duplicate handlers");
7164 let json = serde_json::to_value(&wire).unwrap();
7165 assert!(json.get("instructionDirectories").is_none());
7166 }
7167
7168 #[test]
7171 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
7172 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
7173 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
7174 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7175 let json = serde_json::to_value(&wire).unwrap();
7176 assert_eq!(
7177 json["instructionDirectories"],
7178 serde_json::json!(["/tmp/instr"])
7179 );
7180
7181 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7182 .into_wire()
7183 .expect("no duplicate handlers");
7184 let json = serde_json::to_value(&wire).unwrap();
7185 assert!(json.get("instructionDirectories").is_none());
7186 }
7187
7188 #[test]
7189 fn custom_agent_config_builder_composes() {
7190 use indexmap::IndexMap;
7191
7192 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
7193 .with_display_name("Research Assistant")
7194 .with_description("Investigates technical questions.")
7195 .with_tools(["bash", "view"])
7196 .with_mcp_servers(IndexMap::new())
7197 .with_infer(true)
7198 .with_skills(["rust-coding-skill"]);
7199
7200 assert_eq!(cfg.name, "researcher");
7201 assert_eq!(cfg.prompt, "You are a research assistant.");
7202 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
7203 assert_eq!(
7204 cfg.description.as_deref(),
7205 Some("Investigates technical questions.")
7206 );
7207 assert_eq!(
7208 cfg.tools.as_deref(),
7209 Some(&["bash".to_string(), "view".to_string()][..])
7210 );
7211 assert!(cfg.mcp_servers.is_some());
7212 assert_eq!(cfg.infer, Some(true));
7213 assert_eq!(
7214 cfg.skills.as_deref(),
7215 Some(&["rust-coding-skill".to_string()][..])
7216 );
7217 }
7218
7219 #[test]
7220 fn mcp_servers_serialize_in_insertion_order() {
7221 use indexmap::IndexMap;
7222
7223 let order = [
7229 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
7230 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
7231 ];
7232 let mut servers = IndexMap::new();
7233 for name in order {
7234 servers.insert(
7235 name.to_string(),
7236 McpServerConfig::Stdio(McpStdioServerConfig {
7237 command: "run".to_string(),
7238 ..Default::default()
7239 }),
7240 );
7241 }
7242
7243 let (wire, _runtime) = SessionConfig::default()
7244 .with_mcp_servers(servers)
7245 .into_wire(None)
7246 .expect("into_wire should succeed");
7247 let json = serde_json::to_string(&wire).expect("serialize wire");
7248
7249 let positions: Vec<usize> = order
7250 .iter()
7251 .map(|name| {
7252 json.find(&format!("\"{name}\""))
7253 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
7254 })
7255 .collect();
7256 let mut ascending = positions.clone();
7257 ascending.sort_unstable();
7258 assert_eq!(
7259 positions, ascending,
7260 "mcp server keys must serialize in insertion order: {json}"
7261 );
7262 }
7263
7264 #[test]
7265 fn infinite_session_config_builder_composes() {
7266 let cfg = InfiniteSessionConfig::new()
7267 .with_enabled(true)
7268 .with_background_compaction_threshold(0.75)
7269 .with_buffer_exhaustion_threshold(0.92);
7270
7271 assert_eq!(cfg.enabled, Some(true));
7272 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
7273 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
7274 }
7275
7276 #[test]
7277 fn provider_config_builder_composes() {
7278 use std::collections::HashMap;
7279
7280 let mut headers = HashMap::new();
7281 headers.insert("X-Custom".to_string(), "value".to_string());
7282
7283 let cfg = ProviderConfig::new("https://api.example.com")
7284 .with_provider_type("openai")
7285 .with_wire_api("completions")
7286 .with_transport("websockets")
7287 .with_api_key("sk-test")
7288 .with_bearer_token("bearer-test")
7289 .with_headers(headers)
7290 .with_model_id("gpt-4")
7291 .with_wire_model("azure-gpt-4-deployment")
7292 .with_max_prompt_tokens(8192)
7293 .with_max_output_tokens(2048);
7294
7295 assert_eq!(cfg.base_url, "https://api.example.com");
7296 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
7297 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
7298 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
7299 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
7300 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
7301 assert_eq!(
7302 cfg.headers
7303 .as_ref()
7304 .and_then(|h| h.get("X-Custom"))
7305 .map(String::as_str),
7306 Some("value"),
7307 );
7308 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
7309 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
7310 assert_eq!(cfg.max_prompt_tokens, Some(8192));
7311 assert_eq!(cfg.max_output_tokens, Some(2048));
7312
7313 let wire = serde_json::to_value(&cfg).unwrap();
7315 assert_eq!(wire["modelId"], "gpt-4");
7316 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
7317 assert_eq!(wire["maxPromptTokens"], 8192);
7318 assert_eq!(wire["maxOutputTokens"], 2048);
7319
7320 let unset = ProviderConfig::new("https://api.example.com");
7321 let wire_unset = serde_json::to_value(&unset).unwrap();
7322 assert!(wire_unset.get("modelId").is_none());
7323 assert!(wire_unset.get("wireModel").is_none());
7324 assert!(wire_unset.get("maxPromptTokens").is_none());
7325 assert!(wire_unset.get("maxOutputTokens").is_none());
7326 }
7327
7328 #[test]
7329 fn capi_session_options_builder_composes_and_serializes() {
7330 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
7331
7332 assert_eq!(cfg.enable_web_socket_responses, Some(false));
7333
7334 let wire = serde_json::to_value(&cfg).unwrap();
7335 assert_eq!(
7336 wire,
7337 serde_json::json!({ "enableWebSocketResponses": false })
7338 );
7339
7340 let unset = CapiSessionOptions::new();
7341 let wire_unset = serde_json::to_value(&unset).unwrap();
7342 assert!(wire_unset.get("enableWebSocketResponses").is_none());
7343 assert!(wire_unset.get("autoTier").is_none());
7344 assert_eq!(wire_unset, json!({}));
7345 }
7346
7347 #[test]
7348 fn capi_auto_tier_canonical_values_round_trip_and_forward() {
7349 for (tier, value) in [
7350 (AutoTier::Efficiency, "efficiency"),
7351 (AutoTier::Balance, "balance"),
7352 (AutoTier::Intelligence, "intelligence"),
7353 ] {
7354 let exported: crate::AutoTier = tier.clone();
7355 let capi = CapiSessionOptions::new().with_auto_tier(exported);
7356 assert_eq!(capi.auto_tier, Some(tier));
7357 assert_eq!(
7358 serde_json::to_value(&capi).unwrap(),
7359 json!({"autoTier": value})
7360 );
7361 assert_eq!(
7362 serde_json::from_value::<CapiSessionOptions>(json!({"autoTier": value})).unwrap(),
7363 capi
7364 );
7365
7366 let capi = capi.with_enable_web_socket_responses(false);
7367 let expected = json!({"autoTier": value, "enableWebSocketResponses": false});
7368 let (create, _) = SessionConfig::default()
7369 .with_model("auto")
7370 .with_capi(capi.clone())
7371 .into_wire(Some(SessionId::from("capi-create")))
7372 .unwrap();
7373 assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected);
7374
7375 let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7376 .with_capi(capi)
7377 .into_wire()
7378 .unwrap();
7379 assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected);
7380 }
7381 }
7382
7383 #[test]
7384 fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() {
7385 for value in ["balanced", "Balance", "unknown"] {
7386 assert_eq!(
7387 serde_json::from_value::<AutoTier>(json!(value)).unwrap(),
7388 AutoTier::Unknown
7389 );
7390 }
7391 let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap();
7392 assert_eq!(capi.auto_tier, None);
7393 }
7394
7395 #[test]
7396 fn session_config_with_capi_serializes() {
7397 let (wire, _) = SessionConfig::default()
7398 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7399 .into_wire(Some(SessionId::from("capi-create")))
7400 .expect("no duplicate handlers");
7401 let json = serde_json::to_value(&wire).unwrap();
7402 assert_eq!(
7403 json["capi"],
7404 serde_json::json!({ "enableWebSocketResponses": false })
7405 );
7406
7407 let (empty_wire, _) = SessionConfig::default()
7408 .into_wire(Some(SessionId::from("capi-create-unset")))
7409 .expect("no duplicate handlers");
7410 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7411 assert!(empty_json.get("capi").is_none());
7412 }
7413
7414 #[test]
7415 fn resume_session_config_with_capi_serializes() {
7416 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7417 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7418 .into_wire()
7419 .expect("no duplicate handlers");
7420 let json = serde_json::to_value(&wire).unwrap();
7421 assert_eq!(
7422 json["capi"],
7423 serde_json::json!({ "enableWebSocketResponses": false })
7424 );
7425
7426 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
7427 .into_wire()
7428 .expect("no duplicate handlers");
7429 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7430 assert!(empty_json.get("capi").is_none());
7431 }
7432
7433 #[test]
7434 fn system_message_config_builder_composes() {
7435 use std::collections::HashMap;
7436
7437 let cfg = SystemMessageConfig::new()
7438 .with_mode("replace")
7439 .with_content("Custom system message.")
7440 .with_sections(HashMap::new());
7441
7442 assert_eq!(cfg.mode.as_deref(), Some("replace"));
7443 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
7444 assert!(cfg.sections.is_some());
7445 }
7446
7447 #[test]
7448 fn delivery_mode_serializes_to_kebab_case_strings() {
7449 assert_eq!(
7450 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
7451 "\"enqueue\""
7452 );
7453 assert_eq!(
7454 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
7455 "\"immediate\""
7456 );
7457 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
7458 assert_eq!(parsed, DeliveryMode::Immediate);
7459 }
7460
7461 #[test]
7462 fn agent_mode_serializes_to_kebab_case_strings() {
7463 assert_eq!(
7464 serde_json::to_string(&AgentMode::Interactive).unwrap(),
7465 "\"interactive\""
7466 );
7467 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
7468 assert_eq!(
7469 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
7470 "\"autopilot\""
7471 );
7472 assert_eq!(
7473 serde_json::to_string(&AgentMode::Shell).unwrap(),
7474 "\"shell\""
7475 );
7476 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
7477 assert_eq!(parsed, AgentMode::Plan);
7478 }
7479
7480 #[test]
7481 fn connection_state_distinguishes_variants() {
7482 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
7485 }
7486
7487 #[test]
7493 fn session_event_round_trips_agent_id_on_envelope() {
7494 let wire = json!({
7495 "id": "evt-1",
7496 "timestamp": "2026-04-30T12:00:00Z",
7497 "parentId": null,
7498 "agentId": "sub-agent-42",
7499 "type": "assistant.message",
7500 "data": { "message": "hi" }
7501 });
7502
7503 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
7504 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7505
7506 let roundtripped = serde_json::to_value(&event).unwrap();
7508 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7509
7510 let main_agent_event: SessionEvent = serde_json::from_value(json!({
7512 "id": "evt-2",
7513 "timestamp": "2026-04-30T12:00:01Z",
7514 "parentId": null,
7515 "type": "session.idle",
7516 "data": {}
7517 }))
7518 .unwrap();
7519 assert!(main_agent_event.agent_id.is_none());
7520 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
7521 assert!(roundtripped.get("agentId").is_none());
7522 }
7523
7524 #[test]
7526 fn typed_session_event_round_trips_agent_id_on_envelope() {
7527 let wire = json!({
7528 "id": "evt-1",
7529 "timestamp": "2026-04-30T12:00:00Z",
7530 "parentId": null,
7531 "agentId": "sub-agent-42",
7532 "type": "session.idle",
7533 "data": {}
7534 });
7535
7536 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
7537 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7538
7539 let roundtripped = serde_json::to_value(&event).unwrap();
7540 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7541 }
7542
7543 #[test]
7544 fn connection_state_variants_compile() {
7545 let _ = ConnectionState::Disconnected;
7549 let _ = ConnectionState::Connecting;
7550 let _ = ConnectionState::Connected;
7551 let _ = ConnectionState::Error;
7552 }
7553
7554 #[test]
7555 fn deserializes_runtime_attachment_variants() {
7556 let attachments: Vec<Attachment> = serde_json::from_value(json!([
7557 {
7558 "type": "file",
7559 "path": "/tmp/file.rs",
7560 "displayName": "file.rs",
7561 "lineRange": { "start": 7, "end": 12 }
7562 },
7563 {
7564 "type": "directory",
7565 "path": "/tmp/project",
7566 "displayName": "project"
7567 },
7568 {
7569 "type": "selection",
7570 "filePath": "/tmp/lib.rs",
7571 "displayName": "lib.rs",
7572 "text": "fn main() {}",
7573 "selection": {
7574 "start": { "line": 1, "character": 2 },
7575 "end": { "line": 3, "character": 4 }
7576 }
7577 },
7578 {
7579 "type": "blob",
7580 "data": "Zm9v",
7581 "mimeType": "image/png",
7582 "displayName": "image.png"
7583 },
7584 {
7585 "type": "github_reference",
7586 "number": 42,
7587 "title": "Fix rendering",
7588 "referenceType": "issue",
7589 "state": "open",
7590 "url": "https://github.com/example/repo/issues/42"
7591 }
7592 ]))
7593 .expect("attachments should deserialize");
7594
7595 assert_eq!(attachments.len(), 5);
7596 assert!(matches!(
7597 &attachments[0],
7598 Attachment::File {
7599 path,
7600 display_name,
7601 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7602 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7603 ));
7604 assert!(matches!(
7605 &attachments[1],
7606 Attachment::Directory { path, display_name }
7607 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7608 ));
7609 assert!(matches!(
7610 &attachments[2],
7611 Attachment::Selection {
7612 file_path,
7613 display_name,
7614 selection:
7615 AttachmentSelectionRange {
7616 start: AttachmentSelectionPosition { line: 1, character: 2 },
7617 end: AttachmentSelectionPosition { line: 3, character: 4 },
7618 },
7619 ..
7620 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
7621 ));
7622 assert!(matches!(
7623 &attachments[3],
7624 Attachment::Blob {
7625 data,
7626 mime_type,
7627 display_name,
7628 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
7629 ));
7630 assert!(matches!(
7631 &attachments[4],
7632 Attachment::GitHubReference {
7633 number: 42,
7634 title,
7635 reference_type: GitHubReferenceType::Issue,
7636 state,
7637 url,
7638 } if title == "Fix rendering"
7639 && state == "open"
7640 && url == "https://github.com/example/repo/issues/42"
7641 ));
7642 }
7643
7644 #[test]
7645 fn ensures_display_names_for_variants_that_support_them() {
7646 let mut attachments = vec![
7647 Attachment::File {
7648 path: PathBuf::from("/tmp/file.rs"),
7649 display_name: None,
7650 line_range: None,
7651 },
7652 Attachment::Selection {
7653 file_path: PathBuf::from("/tmp/src/lib.rs"),
7654 display_name: None,
7655 text: "fn main() {}".to_string(),
7656 selection: AttachmentSelectionRange {
7657 start: AttachmentSelectionPosition {
7658 line: 0,
7659 character: 0,
7660 },
7661 end: AttachmentSelectionPosition {
7662 line: 0,
7663 character: 10,
7664 },
7665 },
7666 },
7667 Attachment::Blob {
7668 data: "Zm9v".to_string(),
7669 mime_type: "image/png".to_string(),
7670 display_name: None,
7671 },
7672 Attachment::GitHubReference {
7673 number: 7,
7674 title: "Track regressions".to_string(),
7675 reference_type: GitHubReferenceType::Issue,
7676 state: "open".to_string(),
7677 url: "https://example.com/issues/7".to_string(),
7678 },
7679 ];
7680
7681 ensure_attachment_display_names(&mut attachments);
7682
7683 assert_eq!(attachments[0].display_name(), Some("file.rs"));
7684 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
7685 assert_eq!(attachments[2].display_name(), Some("attachment"));
7686 assert_eq!(attachments[3].display_name(), None);
7687 assert_eq!(
7688 attachments[3].label(),
7689 Some("Track regressions".to_string())
7690 );
7691 }
7692
7693 #[test]
7694 fn github_anchored_attachment_variants_round_trip() {
7695 let cases = vec![
7696 (
7697 "github_commit",
7698 json!({
7699 "type": "github_commit",
7700 "message": "Fix the thing",
7701 "oid": "abc123",
7702 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
7703 "url": "https://github.com/octocat/repo/commit/abc123"
7704 }),
7705 ),
7706 (
7707 "github_release",
7708 json!({
7709 "type": "github_release",
7710 "name": "v1.2.3",
7711 "repo": { "name": "repo", "owner": "octocat" },
7712 "tagName": "v1.2.3",
7713 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
7714 }),
7715 ),
7716 (
7717 "github_actions_job",
7718 json!({
7719 "type": "github_actions_job",
7720 "conclusion": "failure",
7721 "jobId": 99,
7722 "jobName": "build",
7723 "repo": { "name": "repo", "owner": "octocat" },
7724 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
7725 "workflowName": "CI"
7726 }),
7727 ),
7728 (
7729 "github_repository",
7730 json!({
7731 "type": "github_repository",
7732 "description": "An example repository",
7733 "ref": "main",
7734 "repo": { "name": "repo", "owner": "octocat" },
7735 "url": "https://github.com/octocat/repo"
7736 }),
7737 ),
7738 (
7739 "github_file_diff",
7740 json!({
7741 "type": "github_file_diff",
7742 "base": {
7743 "path": "src/lib.rs",
7744 "ref": "main",
7745 "repo": { "name": "repo", "owner": "octocat" }
7746 },
7747 "head": {
7748 "path": "src/lib.rs",
7749 "ref": "feature",
7750 "repo": { "name": "repo", "owner": "octocat" }
7751 },
7752 "url": "https://github.com/octocat/repo/compare/main...feature"
7753 }),
7754 ),
7755 (
7756 "github_tree_comparison",
7757 json!({
7758 "type": "github_tree_comparison",
7759 "base": {
7760 "repo": { "name": "repo", "owner": "octocat" },
7761 "revision": "main"
7762 },
7763 "head": {
7764 "repo": { "name": "repo", "owner": "octocat" },
7765 "revision": "feature"
7766 },
7767 "url": "https://github.com/octocat/repo/compare/main...feature"
7768 }),
7769 ),
7770 (
7771 "github_url",
7772 json!({
7773 "type": "github_url",
7774 "url": "https://github.com/octocat/repo/wiki"
7775 }),
7776 ),
7777 (
7778 "github_file",
7779 json!({
7780 "type": "github_file",
7781 "path": "src/main.rs",
7782 "ref": "main",
7783 "repo": { "name": "repo", "owner": "octocat" },
7784 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
7785 }),
7786 ),
7787 (
7788 "github_snippet",
7789 json!({
7790 "type": "github_snippet",
7791 "lineRange": { "start": 10, "end": 20 },
7792 "path": "src/main.rs",
7793 "ref": "main",
7794 "repo": { "name": "repo", "owner": "octocat" },
7795 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
7796 }),
7797 ),
7798 ];
7799
7800 for (expected_type, input) in cases {
7801 let attachment: Attachment = serde_json::from_value(input.clone())
7802 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
7803
7804 let serialized_string = serde_json::to_string(&attachment)
7809 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
7810
7811 assert_eq!(
7813 serialized_string.matches("\"type\":").count(),
7814 1,
7815 "{expected_type} must serialize a single `type` key"
7816 );
7817
7818 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
7819 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
7820 assert_eq!(
7821 serialized.get("type").and_then(|value| value.as_str()),
7822 Some(expected_type),
7823 "{expected_type} must serialize the correct discriminator"
7824 );
7825
7826 assert_eq!(
7828 serialized, input,
7829 "{expected_type} should round-trip without data loss"
7830 );
7831 let reparsed: Attachment = serde_json::from_value(serialized)
7832 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
7833 assert_eq!(
7834 reparsed, attachment,
7835 "{expected_type} should re-deserialize to the same value"
7836 );
7837 }
7838 }
7839}
7840
7841#[cfg(test)]
7842mod permission_builder_tests {
7843 use std::sync::Arc;
7844
7845 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
7846 use crate::permission;
7847 use crate::types::{
7848 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
7849 SessionId,
7850 };
7851
7852 fn data() -> PermissionRequestData {
7853 PermissionRequestData {
7854 extra: serde_json::json!({"tool": "shell"}),
7855 ..Default::default()
7856 }
7857 }
7858
7859 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7862 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7863 }
7864
7865 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
7866 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
7867 }
7868
7869 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
7870 handler
7871 .handle(SessionId::from("s1"), RequestId::new("1"), data())
7872 .await
7873 }
7874
7875 #[tokio::test]
7876 async fn approve_all_with_handler_present_approves() {
7877 let cfg = SessionConfig::default()
7878 .with_permission_handler(Arc::new(ApproveAllHandler))
7879 .approve_all_permissions();
7880 let h = resolve_create(cfg).expect("policy + handler yields handler");
7881 assert!(matches!(
7882 dispatch(&h).await,
7883 PermissionResult::Decision {
7884 decision: PermissionDecision::ApproveOnce(_),
7885 ..
7886 }
7887 ));
7888 }
7889
7890 #[tokio::test]
7891 async fn approve_all_standalone_produces_handler() {
7892 let cfg = SessionConfig::default().approve_all_permissions();
7893 let h = resolve_create(cfg).expect("policy alone yields handler");
7894 assert!(matches!(
7895 dispatch(&h).await,
7896 PermissionResult::Decision {
7897 decision: PermissionDecision::ApproveOnce(_),
7898 ..
7899 }
7900 ));
7901 }
7902
7903 #[tokio::test]
7906 async fn approve_all_is_order_independent() {
7907 let a = SessionConfig::default()
7908 .with_permission_handler(Arc::new(ApproveAllHandler))
7909 .approve_all_permissions();
7910 let b = SessionConfig::default()
7911 .approve_all_permissions()
7912 .with_permission_handler(Arc::new(ApproveAllHandler));
7913 let ha = resolve_create(a).unwrap();
7914 let hb = resolve_create(b).unwrap();
7915 assert!(matches!(
7916 dispatch(&ha).await,
7917 PermissionResult::Decision {
7918 decision: PermissionDecision::ApproveOnce(_),
7919 ..
7920 }
7921 ));
7922 assert!(matches!(
7923 dispatch(&hb).await,
7924 PermissionResult::Decision {
7925 decision: PermissionDecision::ApproveOnce(_),
7926 ..
7927 }
7928 ));
7929 }
7930
7931 #[tokio::test]
7932 async fn deny_all_is_order_independent() {
7933 let a = SessionConfig::default()
7934 .with_permission_handler(Arc::new(ApproveAllHandler))
7935 .deny_all_permissions();
7936 let b = SessionConfig::default()
7937 .deny_all_permissions()
7938 .with_permission_handler(Arc::new(ApproveAllHandler));
7939 let ha = resolve_create(a).unwrap();
7940 let hb = resolve_create(b).unwrap();
7941 assert!(matches!(
7942 dispatch(&ha).await,
7943 PermissionResult::Decision {
7944 decision: PermissionDecision::Reject(_),
7945 ..
7946 }
7947 ));
7948 assert!(matches!(
7949 dispatch(&hb).await,
7950 PermissionResult::Decision {
7951 decision: PermissionDecision::Reject(_),
7952 ..
7953 }
7954 ));
7955 }
7956
7957 #[tokio::test]
7958 async fn approve_permissions_if_consults_predicate() {
7959 let cfg = SessionConfig::default().approve_permissions_if(|d| {
7960 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7961 });
7962 let h = resolve_create(cfg).unwrap();
7963 assert!(matches!(
7964 dispatch(&h).await,
7965 PermissionResult::Decision {
7966 decision: PermissionDecision::Reject(_),
7967 ..
7968 }
7969 ));
7970 }
7971
7972 #[tokio::test]
7973 async fn approve_permissions_if_is_order_independent() {
7974 let predicate = |d: &PermissionRequestData| {
7975 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7976 };
7977 let a = SessionConfig::default()
7978 .with_permission_handler(Arc::new(ApproveAllHandler))
7979 .approve_permissions_if(predicate);
7980 let b = SessionConfig::default()
7981 .approve_permissions_if(predicate)
7982 .with_permission_handler(Arc::new(ApproveAllHandler));
7983 let ha = resolve_create(a).unwrap();
7984 let hb = resolve_create(b).unwrap();
7985 assert!(matches!(
7986 dispatch(&ha).await,
7987 PermissionResult::Decision {
7988 decision: PermissionDecision::Reject(_),
7989 ..
7990 }
7991 ));
7992 assert!(matches!(
7993 dispatch(&hb).await,
7994 PermissionResult::Decision {
7995 decision: PermissionDecision::Reject(_),
7996 ..
7997 }
7998 ));
7999 }
8000
8001 #[tokio::test]
8002 async fn resume_session_config_approve_all_works() {
8003 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
8004 .with_permission_handler(Arc::new(ApproveAllHandler))
8005 .approve_all_permissions();
8006 let h = resolve_resume(cfg).unwrap();
8007 assert!(matches!(
8008 dispatch(&h).await,
8009 PermissionResult::Decision {
8010 decision: PermissionDecision::ApproveOnce(_),
8011 ..
8012 }
8013 ));
8014 }
8015
8016 #[tokio::test]
8017 async fn resume_session_config_approve_all_is_order_independent() {
8018 let a = ResumeSessionConfig::new(SessionId::from("s1"))
8019 .with_permission_handler(Arc::new(ApproveAllHandler))
8020 .approve_all_permissions();
8021 let b = ResumeSessionConfig::new(SessionId::from("s1"))
8022 .approve_all_permissions()
8023 .with_permission_handler(Arc::new(ApproveAllHandler));
8024 let ha = resolve_resume(a).unwrap();
8025 let hb = resolve_resume(b).unwrap();
8026 assert!(matches!(
8027 dispatch(&ha).await,
8028 PermissionResult::Decision {
8029 decision: PermissionDecision::ApproveOnce(_),
8030 ..
8031 }
8032 ));
8033 assert!(matches!(
8034 dispatch(&hb).await,
8035 PermissionResult::Decision {
8036 decision: PermissionDecision::ApproveOnce(_),
8037 ..
8038 }
8039 ));
8040 }
8041
8042 #[test]
8043 fn session_config_enable_experimental_mode_serializes_when_set() {
8044 let cfg = SessionConfig::default().with_enable_experimental_mode(false);
8045 assert_eq!(cfg.enable_experimental_mode, Some(false));
8046
8047 let (wire, _runtime) = cfg
8048 .into_wire(Some(SessionId::from("experimental-mode")))
8049 .expect("enable_experimental_mode config has no duplicate handlers");
8050 assert_eq!(wire.is_experimental_mode, Some(false));
8051
8052 let json = serde_json::to_value(&wire).unwrap();
8053 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
8054 }
8055
8056 #[test]
8057 fn session_config_enable_experimental_mode_omitted_when_none() {
8058 let cfg = SessionConfig::default();
8059 assert_eq!(cfg.enable_experimental_mode, None);
8060
8061 let (wire, _runtime) = cfg
8062 .into_wire(Some(SessionId::from("no-experimental-mode")))
8063 .expect("default config has no duplicate handlers");
8064 assert_eq!(wire.is_experimental_mode, None);
8065
8066 let json = serde_json::to_value(&wire).unwrap();
8067 assert!(json.get("isExperimentalMode").is_none());
8068 }
8069
8070 #[test]
8071 fn resume_session_config_enable_experimental_mode_serializes_when_set() {
8072 let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
8073 .with_enable_experimental_mode(false);
8074 assert_eq!(cfg.enable_experimental_mode, Some(false));
8075
8076 let (wire, _runtime) = cfg
8077 .into_wire()
8078 .expect("resume enable_experimental_mode config has no duplicate handlers");
8079 assert_eq!(wire.is_experimental_mode, Some(false));
8080
8081 let json = serde_json::to_value(&wire).unwrap();
8082 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
8083 }
8084
8085 #[test]
8086 fn resume_session_config_enable_experimental_mode_omitted_when_none() {
8087 let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
8088 assert_eq!(cfg.enable_experimental_mode, None);
8089
8090 let (wire, _runtime) = cfg
8091 .into_wire()
8092 .expect("default resume config has no duplicate handlers");
8093 assert_eq!(wire.is_experimental_mode, None);
8094
8095 let json = serde_json::to_value(&wire).unwrap();
8096 assert!(json.get("isExperimentalMode").is_none());
8097 }
8098}
8099
8100#[cfg(test)]
8101mod is_terminal_tests {
8102 use super::Tool;
8103
8104 #[test]
8105 fn is_terminal_serializes_as_camel_case_when_set() {
8106 let tool = Tool {
8107 name: "clear_context".to_owned(),
8108 is_terminal: true,
8109 ..Default::default()
8110 };
8111 let value = serde_json::to_value(&tool).expect("tool serializes");
8112 assert_eq!(
8113 value.get("isTerminal"),
8114 Some(&serde_json::Value::Bool(true))
8115 );
8116 }
8117
8118 #[test]
8119 fn is_terminal_is_omitted_when_false() {
8120 let tool = Tool {
8121 name: "plain".to_owned(),
8122 ..Default::default()
8123 };
8124 let value = serde_json::to_value(&tool).expect("tool serializes");
8125 assert!(value.get("isTerminal").is_none());
8126 }
8127
8128 #[test]
8131 fn is_terminal_appears_in_debug_output() {
8132 let terminal = Tool {
8133 name: "clear_context".to_owned(),
8134 is_terminal: true,
8135 ..Default::default()
8136 };
8137 assert!(format!("{terminal:?}").contains("is_terminal: true"));
8138
8139 let plain = Tool {
8140 name: "plain".to_owned(),
8141 ..Default::default()
8142 };
8143 assert!(format!("{plain:?}").contains("is_terminal: false"));
8144 }
8145}