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::api_types::{ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus};
26pub use crate::generated::session_events::AutoTier;
31use crate::generated::session_events::ReasoningSummary;
32pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig};
34use crate::github_token::GitHubTokenProvider;
35use crate::handler::{
36 AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler,
37 PermissionHandler, UserInputHandler,
38};
39use crate::hooks::SessionHooks;
40use crate::provider_token::BearerTokenProvider;
41pub use crate::session_fs::{
42 DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig,
43 SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult,
44 SessionFsSqliteQueryType, SessionFsSqliteTransactionError,
45 SessionFsSqliteTransactionErrorClass, SessionFsSqliteTransactionStatement,
46};
47pub use crate::trace_context::{TraceContext, TraceContextProvider};
48use crate::transforms::SystemMessageTransform;
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53#[allow(dead_code)]
54#[non_exhaustive]
55pub(crate) enum ConnectionState {
56 Disconnected,
58 Connecting,
60 Connected,
62 Error,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
71#[non_exhaustive]
72pub enum SessionLifecycleEventType {
73 #[serde(rename = "session.created")]
75 Created,
76 #[serde(rename = "session.deleted")]
78 Deleted,
79 #[serde(rename = "session.updated")]
81 Updated,
82 #[serde(rename = "session.foreground")]
84 Foreground,
85 #[serde(rename = "session.background")]
87 Background,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct SessionLifecycleEventMetadata {
93 #[serde(rename = "startTime")]
95 pub start_time: String,
96 #[serde(rename = "modifiedTime")]
98 pub modified_time: String,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub summary: Option<String>,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107pub struct SessionLifecycleEvent {
108 #[serde(rename = "type")]
110 pub event_type: SessionLifecycleEventType,
111 #[serde(rename = "sessionId")]
113 pub session_id: SessionId,
114 #[serde(skip_serializing_if = "Option::is_none")]
116 pub metadata: Option<SessionLifecycleEventMetadata>,
117}
118
119#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
125#[serde(transparent)]
126pub struct SessionId(String);
127
128impl SessionId {
129 pub fn new(id: impl Into<String>) -> Self {
131 Self(id.into())
132 }
133
134 pub fn as_str(&self) -> &str {
136 &self.0
137 }
138
139 pub fn into_inner(self) -> String {
141 self.0
142 }
143}
144
145impl std::ops::Deref for SessionId {
146 type Target = str;
147
148 fn deref(&self) -> &str {
149 &self.0
150 }
151}
152
153impl std::fmt::Display for SessionId {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 f.write_str(&self.0)
156 }
157}
158
159impl From<String> for SessionId {
160 fn from(s: String) -> Self {
161 Self(s)
162 }
163}
164
165impl From<&str> for SessionId {
166 fn from(s: &str) -> Self {
167 Self(s.to_owned())
168 }
169}
170
171impl AsRef<str> for SessionId {
172 fn as_ref(&self) -> &str {
173 &self.0
174 }
175}
176
177impl std::borrow::Borrow<str> for SessionId {
178 fn borrow(&self) -> &str {
179 &self.0
180 }
181}
182
183impl From<SessionId> for String {
184 fn from(id: SessionId) -> String {
185 id.0
186 }
187}
188
189impl PartialEq<str> for SessionId {
190 fn eq(&self, other: &str) -> bool {
191 self.0 == other
192 }
193}
194
195impl PartialEq<String> for SessionId {
196 fn eq(&self, other: &String) -> bool {
197 &self.0 == other
198 }
199}
200
201impl PartialEq<SessionId> for String {
202 fn eq(&self, other: &SessionId) -> bool {
203 self == &other.0
204 }
205}
206
207impl PartialEq<&str> for SessionId {
208 fn eq(&self, other: &&str) -> bool {
209 self.0 == *other
210 }
211}
212
213impl PartialEq<&SessionId> for SessionId {
214 fn eq(&self, other: &&SessionId) -> bool {
215 self.0 == other.0
216 }
217}
218
219impl PartialEq<SessionId> for &SessionId {
220 fn eq(&self, other: &SessionId) -> bool {
221 self.0 == other.0
222 }
223}
224
225#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
231#[serde(transparent)]
232pub struct RequestId(String);
233
234impl RequestId {
235 pub fn new(id: impl Into<String>) -> Self {
237 Self(id.into())
238 }
239
240 pub fn into_inner(self) -> String {
242 self.0
243 }
244}
245
246impl std::ops::Deref for RequestId {
247 type Target = str;
248
249 fn deref(&self) -> &str {
250 &self.0
251 }
252}
253
254impl std::fmt::Display for RequestId {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 f.write_str(&self.0)
257 }
258}
259
260impl From<String> for RequestId {
261 fn from(s: String) -> Self {
262 Self(s)
263 }
264}
265
266impl From<&str> for RequestId {
267 fn from(s: &str) -> Self {
268 Self(s.to_owned())
269 }
270}
271
272impl AsRef<str> for RequestId {
273 fn as_ref(&self) -> &str {
274 &self.0
275 }
276}
277
278impl std::borrow::Borrow<str> for RequestId {
279 fn borrow(&self) -> &str {
280 &self.0
281 }
282}
283
284impl From<RequestId> for String {
285 fn from(id: RequestId) -> String {
286 id.0
287 }
288}
289
290impl PartialEq<str> for RequestId {
291 fn eq(&self, other: &str) -> bool {
292 self.0 == other
293 }
294}
295
296impl PartialEq<String> for RequestId {
297 fn eq(&self, other: &String) -> bool {
298 &self.0 == other
299 }
300}
301
302impl PartialEq<RequestId> for String {
303 fn eq(&self, other: &RequestId) -> bool {
304 self == &other.0
305 }
306}
307
308impl PartialEq<&str> for RequestId {
309 fn eq(&self, other: &&str) -> bool {
310 self.0 == *other
311 }
312}
313
314#[derive(Clone, Default, Serialize, Deserialize)]
329#[serde(rename_all = "camelCase")]
330#[non_exhaustive]
331pub struct Tool {
332 pub name: String,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub namespaced_name: Option<String>,
338 #[serde(default)]
340 pub description: String,
341 #[serde(default, skip_serializing_if = "Option::is_none")]
343 pub instructions: Option<String>,
344 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
346 pub parameters: IndexMap<String, Value>,
347 #[serde(default, skip_serializing_if = "is_false")]
351 pub overrides_built_in_tool: bool,
352 #[serde(default, skip_serializing_if = "is_false")]
356 pub skip_permission: bool,
357 #[serde(default, skip_serializing_if = "is_false")]
362 pub is_terminal: bool,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub defer: Option<DeferMode>,
370 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
375 pub metadata: IndexMap<String, Value>,
376 #[serde(skip)]
388 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
389}
390
391#[inline]
392fn is_false(b: &bool) -> bool {
393 !*b
394}
395
396#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
399#[serde(rename_all = "lowercase")]
400pub enum DeferMode {
401 Auto,
403 Never,
405}
406
407impl Tool {
408 pub fn new(name: impl Into<String>) -> Self {
428 Self {
429 name: name.into(),
430 ..Default::default()
431 }
432 }
433
434 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
437 self.namespaced_name = Some(namespaced_name.into());
438 self
439 }
440
441 pub fn with_description(mut self, description: impl Into<String>) -> Self {
443 self.description = description.into();
444 self
445 }
446
447 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
449 self.instructions = Some(instructions.into());
450 self
451 }
452
453 pub fn with_parameters(mut self, parameters: Value) -> Self {
467 self.parameters = crate::tool::tool_parameters(parameters);
468 self
469 }
470
471 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
475 self.overrides_built_in_tool = overrides;
476 self
477 }
478
479 pub fn with_skip_permission(mut self, skip: bool) -> Self {
483 self.skip_permission = skip;
484 self
485 }
486
487 #[must_use]
494 pub fn with_is_terminal(mut self, is_terminal: bool) -> Self {
495 self.is_terminal = is_terminal;
496 self
497 }
498
499 pub fn with_defer(mut self, defer: DeferMode) -> Self {
503 self.defer = Some(defer);
504 self
505 }
506
507 pub fn with_metadata(mut self, metadata: IndexMap<String, Value>) -> Self {
510 self.metadata = metadata;
511 self
512 }
513
514 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
518 self.handler = Some(handler);
519 self
520 }
521
522 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
527 self.handler.as_ref()
528 }
529}
530
531impl std::fmt::Debug for Tool {
532 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
533 f.debug_struct("Tool")
534 .field("name", &self.name)
535 .field("namespaced_name", &self.namespaced_name)
536 .field("description", &self.description)
537 .field("instructions", &self.instructions)
538 .field("parameters", &self.parameters)
539 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
540 .field("skip_permission", &self.skip_permission)
541 .field("is_terminal", &self.is_terminal)
542 .field("defer", &self.defer)
543 .field("metadata", &self.metadata)
544 .field(
545 "handler",
546 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
547 )
548 .finish()
549 }
550}
551
552#[non_exhaustive]
555#[derive(Debug, Clone)]
556pub struct CommandContext {
557 pub session_id: SessionId,
559 pub command: String,
561 pub command_name: String,
563 pub args: String,
565}
566
567#[async_trait::async_trait]
573pub trait CommandHandler: Send + Sync {
574 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
576}
577
578#[non_exhaustive]
584#[derive(Clone)]
585pub struct CommandDefinition {
586 pub name: String,
588 pub description: Option<String>,
590 pub handler: Arc<dyn CommandHandler>,
592}
593
594impl CommandDefinition {
595 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
598 Self {
599 name: name.into(),
600 description: None,
601 handler,
602 }
603 }
604
605 pub fn with_description(mut self, description: impl Into<String>) -> Self {
607 self.description = Some(description.into());
608 self
609 }
610}
611
612impl std::fmt::Debug for CommandDefinition {
613 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
614 f.debug_struct("CommandDefinition")
615 .field("name", &self.name)
616 .field("description", &self.description)
617 .field("handler", &"<set>")
618 .finish()
619 }
620}
621
622impl Serialize for CommandDefinition {
623 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
624 use serde::ser::SerializeStruct;
625 let mut state = serializer.serialize_struct("CommandDefinition", 2)?;
626 state.serialize_field("name", &self.name)?;
627 state.serialize_field("description", self.description.as_deref().unwrap_or(""))?;
628 state.end()
629 }
630}
631
632#[derive(Debug, Clone, Default, Serialize, Deserialize)]
639#[serde(rename_all = "camelCase")]
640#[non_exhaustive]
641pub struct CustomAgentConfig {
642 pub name: String,
644 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub display_name: Option<String>,
647 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub description: Option<String>,
650 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub tools: Option<Vec<String>>,
653 pub prompt: String,
655 #[serde(default, skip_serializing_if = "Option::is_none")]
657 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
658 #[serde(default, skip_serializing_if = "Option::is_none")]
660 pub infer: Option<bool>,
661 #[serde(default, skip_serializing_if = "Option::is_none")]
663 pub skills: Option<Vec<String>>,
664 #[serde(default, skip_serializing_if = "Option::is_none")]
669 pub model: Option<String>,
670 #[serde(default, skip_serializing_if = "Option::is_none")]
675 pub reasoning_effort: Option<String>,
676}
677
678impl CustomAgentConfig {
679 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
686 Self {
687 name: name.into(),
688 prompt: prompt.into(),
689 ..Self::default()
690 }
691 }
692
693 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
695 self.display_name = Some(display_name.into());
696 self
697 }
698
699 pub fn with_description(mut self, description: impl Into<String>) -> Self {
701 self.description = Some(description.into());
702 self
703 }
704
705 pub fn with_tools<I, S>(mut self, tools: I) -> Self
708 where
709 I: IntoIterator<Item = S>,
710 S: Into<String>,
711 {
712 self.tools = Some(tools.into_iter().map(Into::into).collect());
713 self
714 }
715
716 pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
718 self.mcp_servers = Some(mcp_servers);
719 self
720 }
721
722 pub fn with_infer(mut self, infer: bool) -> Self {
724 self.infer = Some(infer);
725 self
726 }
727
728 pub fn with_skills<I, S>(mut self, skills: I) -> Self
730 where
731 I: IntoIterator<Item = S>,
732 S: Into<String>,
733 {
734 self.skills = Some(skills.into_iter().map(Into::into).collect());
735 self
736 }
737
738 pub fn with_model(mut self, model: impl Into<String>) -> Self {
740 self.model = Some(model.into());
741 self
742 }
743
744 pub fn with_reasoning_effort(mut self, reasoning_effort: impl Into<String>) -> Self {
746 self.reasoning_effort = Some(reasoning_effort.into());
747 self
748 }
749}
750
751#[derive(Debug, Clone, Default, Serialize, Deserialize)]
758#[serde(rename_all = "camelCase")]
759pub struct DefaultAgentConfig {
760 #[serde(default, skip_serializing_if = "Option::is_none")]
762 pub excluded_tools: Option<Vec<String>>,
763}
764
765#[derive(Debug, Clone, Default, Serialize, Deserialize)]
771#[serde(rename_all = "camelCase")]
772#[non_exhaustive]
773pub struct LargeToolOutputConfig {
774 #[serde(default, skip_serializing_if = "Option::is_none")]
776 pub enabled: Option<bool>,
777 #[serde(default, skip_serializing_if = "Option::is_none")]
780 pub max_size_bytes: Option<u64>,
781 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
784 pub output_directory: Option<PathBuf>,
785}
786
787impl LargeToolOutputConfig {
788 pub fn new() -> Self {
791 Self::default()
792 }
793
794 pub fn with_enabled(mut self, enabled: bool) -> Self {
796 self.enabled = Some(enabled);
797 self
798 }
799
800 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
802 self.max_size_bytes = Some(max_size_bytes);
803 self
804 }
805
806 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
808 self.output_directory = Some(output_directory.into());
809 self
810 }
811}
812
813#[derive(Debug, Clone, Default, Serialize, Deserialize)]
819#[serde(rename_all = "camelCase")]
820#[non_exhaustive]
821pub struct ToolSearchConfig {
822 #[serde(default, skip_serializing_if = "Option::is_none")]
824 pub enabled: Option<bool>,
825 #[serde(default, skip_serializing_if = "Option::is_none")]
828 pub defer_threshold: Option<u32>,
829}
830
831impl ToolSearchConfig {
832 pub fn new() -> Self {
835 Self::default()
836 }
837
838 pub fn with_enabled(mut self, enabled: bool) -> Self {
840 self.enabled = Some(enabled);
841 self
842 }
843
844 pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
847 self.defer_threshold = Some(defer_threshold);
848 self
849 }
850}
851
852#[derive(Debug, Clone, Default, Serialize, Deserialize)]
857#[serde(rename_all = "camelCase")]
858#[non_exhaustive]
859pub struct GitHubMcpToolConfig {
860 #[serde(default, skip_serializing_if = "Option::is_none")]
862 pub enable_all_tools: Option<bool>,
863 #[serde(default, skip_serializing_if = "Option::is_none")]
865 pub additional_toolsets: Option<Vec<String>>,
866 #[serde(default, skip_serializing_if = "Option::is_none")]
868 pub additional_tools: Option<Vec<String>>,
869 #[serde(default, skip_serializing_if = "Option::is_none")]
871 pub enable_insiders_mode: Option<bool>,
872 #[serde(default, skip_serializing_if = "Option::is_none")]
876 pub disable_form_deferral: Option<bool>,
877}
878
879impl GitHubMcpToolConfig {
880 pub fn new() -> Self {
882 Self::default()
883 }
884
885 pub fn with_enable_all_tools(mut self, value: bool) -> Self {
887 self.enable_all_tools = Some(value);
888 self
889 }
890
891 pub fn with_additional_toolsets<I, S>(mut self, values: I) -> Self
893 where
894 I: IntoIterator<Item = S>,
895 S: Into<String>,
896 {
897 self.additional_toolsets = Some(values.into_iter().map(Into::into).collect());
898 self
899 }
900
901 pub fn with_additional_tools<I, S>(mut self, values: I) -> Self
903 where
904 I: IntoIterator<Item = S>,
905 S: Into<String>,
906 {
907 self.additional_tools = Some(values.into_iter().map(Into::into).collect());
908 self
909 }
910
911 pub fn with_enable_insiders_mode(mut self, value: bool) -> Self {
913 self.enable_insiders_mode = Some(value);
914 self
915 }
916
917 pub fn with_disable_form_deferral(mut self, value: bool) -> Self {
921 self.disable_form_deferral = Some(value);
922 self
923 }
924}
925
926#[derive(Debug, Clone, Default, Serialize, Deserialize)]
933#[serde(rename_all = "camelCase")]
934#[non_exhaustive]
935pub struct InfiniteSessionConfig {
936 #[serde(default, skip_serializing_if = "Option::is_none")]
938 pub enabled: Option<bool>,
939 #[serde(default, skip_serializing_if = "Option::is_none")]
942 pub background_compaction_threshold: Option<f64>,
943 #[serde(default, skip_serializing_if = "Option::is_none")]
946 pub buffer_exhaustion_threshold: Option<f64>,
947}
948
949impl InfiniteSessionConfig {
950 pub fn new() -> Self {
953 Self::default()
954 }
955
956 pub fn with_enabled(mut self, enabled: bool) -> Self {
959 self.enabled = Some(enabled);
960 self
961 }
962
963 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
966 self.background_compaction_threshold = Some(threshold);
967 self
968 }
969
970 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
973 self.buffer_exhaustion_threshold = Some(threshold);
974 self
975 }
976}
977
978#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
989#[serde(rename_all = "camelCase")]
990#[non_exhaustive]
991pub struct MemoryConfiguration {
992 pub enabled: bool,
994}
995
996impl MemoryConfiguration {
997 pub fn enabled() -> Self {
999 Self { enabled: true }
1000 }
1001
1002 pub fn disabled() -> Self {
1004 Self { enabled: false }
1005 }
1006
1007 pub fn with_enabled(mut self, enabled: bool) -> Self {
1009 self.enabled = enabled;
1010 self
1011 }
1012}
1013
1014#[derive(Debug, Clone, Serialize, Deserialize)]
1016#[serde(rename_all = "camelCase")]
1017#[non_exhaustive]
1018pub struct CloudSessionRepository {
1019 pub owner: String,
1021 pub name: String,
1023 #[serde(skip_serializing_if = "Option::is_none")]
1025 pub branch: Option<String>,
1026}
1027
1028impl CloudSessionRepository {
1029 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
1031 Self {
1032 owner: owner.into(),
1033 name: name.into(),
1034 branch: None,
1035 }
1036 }
1037
1038 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
1040 self.branch = Some(branch.into());
1041 self
1042 }
1043}
1044
1045#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1047#[serde(rename_all = "camelCase")]
1048#[non_exhaustive]
1049pub struct CloudSessionOptions {
1050 #[serde(skip_serializing_if = "Option::is_none")]
1052 pub repository: Option<CloudSessionRepository>,
1053}
1054
1055impl CloudSessionOptions {
1056 pub fn with_repository(repository: CloudSessionRepository) -> Self {
1058 Self {
1059 repository: Some(repository),
1060 }
1061 }
1062}
1063
1064#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1066#[serde(rename_all = "camelCase")]
1067pub struct ExtensionInfo {
1068 pub source: String,
1070 pub name: String,
1072}
1073
1074impl ExtensionInfo {
1075 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
1077 Self {
1078 source: source.into(),
1079 name: name.into(),
1080 }
1081 }
1082}
1083
1084#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1095#[serde(rename_all = "camelCase")]
1096pub struct CanvasProviderIdentity {
1097 pub id: String,
1099 #[serde(skip_serializing_if = "Option::is_none")]
1101 pub name: Option<String>,
1102}
1103
1104impl CanvasProviderIdentity {
1105 pub fn new(id: impl Into<String>) -> Self {
1107 Self {
1108 id: id.into(),
1109 name: None,
1110 }
1111 }
1112
1113 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1115 self.name = Some(name.into());
1116 self
1117 }
1118}
1119
1120#[derive(Debug, Clone, Serialize, Deserialize)]
1154#[serde(tag = "type", rename_all = "lowercase")]
1155#[non_exhaustive]
1156pub enum McpServerConfig {
1157 #[serde(alias = "local")]
1161 Stdio(McpStdioServerConfig),
1162 Http(McpHttpServerConfig),
1164 Sse(McpHttpServerConfig),
1166}
1167
1168#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1172#[serde(rename_all = "camelCase")]
1173pub struct McpStdioServerConfig {
1174 #[serde(default, skip_serializing_if = "Option::is_none")]
1180 pub tools: Option<Vec<String>>,
1181 #[serde(default, skip_serializing_if = "Option::is_none")]
1183 pub timeout: Option<i64>,
1184 pub command: String,
1186 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1188 pub args: Vec<String>,
1189 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1192 pub env: HashMap<String, String>,
1193 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1195 pub working_directory: Option<String>,
1196}
1197
1198#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1202#[serde(rename_all = "camelCase")]
1203pub struct McpHttpServerConfig {
1204 #[serde(default, skip_serializing_if = "Option::is_none")]
1210 pub tools: Option<Vec<String>>,
1211 #[serde(default, skip_serializing_if = "Option::is_none")]
1213 pub timeout: Option<i64>,
1214 pub url: String,
1216 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1218 pub headers: HashMap<String, String>,
1219}
1220
1221#[derive(Clone, Default, Serialize, Deserialize)]
1227#[serde(rename_all = "camelCase")]
1228#[non_exhaustive]
1229pub struct ProviderConfig {
1230 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1233 pub provider_type: Option<String>,
1234 #[serde(default, skip_serializing_if = "Option::is_none")]
1237 pub wire_api: Option<String>,
1238 #[serde(default, skip_serializing_if = "Option::is_none")]
1243 pub transport: Option<String>,
1244 pub base_url: String,
1246 #[serde(default, skip_serializing_if = "Option::is_none")]
1248 pub api_key: Option<String>,
1249 #[serde(default, skip_serializing_if = "Option::is_none")]
1253 pub bearer_token: Option<String>,
1254 #[serde(skip)]
1257 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1258 #[serde(default, skip_serializing_if = "Option::is_none")]
1259 pub(crate) has_bearer_token_provider: Option<bool>,
1260 #[serde(default, skip_serializing_if = "Option::is_none")]
1262 pub azure: Option<AzureProviderOptions>,
1263 #[serde(default, skip_serializing_if = "Option::is_none")]
1265 pub headers: Option<HashMap<String, String>>,
1266 #[serde(default, skip_serializing_if = "Option::is_none")]
1270 pub model_id: Option<String>,
1271 #[serde(default, skip_serializing_if = "Option::is_none")]
1278 pub wire_model: Option<String>,
1279 #[serde(default, skip_serializing_if = "Option::is_none")]
1284 pub max_prompt_tokens: Option<i64>,
1285 #[serde(default, skip_serializing_if = "Option::is_none")]
1288 pub max_output_tokens: Option<i64>,
1289}
1290
1291impl std::fmt::Debug for ProviderConfig {
1292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1293 f.debug_struct("ProviderConfig")
1294 .field("provider_type", &self.provider_type)
1295 .field("wire_api", &self.wire_api)
1296 .field("transport", &self.transport)
1297 .field("base_url", &self.base_url)
1298 .field("api_key", &self.api_key)
1299 .field("bearer_token", &self.bearer_token)
1300 .field(
1301 "bearer_token_provider",
1302 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1303 )
1304 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1305 .field("azure", &self.azure)
1306 .field("headers", &self.headers)
1307 .field("model_id", &self.model_id)
1308 .field("wire_model", &self.wire_model)
1309 .field("max_prompt_tokens", &self.max_prompt_tokens)
1310 .field("max_output_tokens", &self.max_output_tokens)
1311 .finish()
1312 }
1313}
1314
1315impl ProviderConfig {
1316 pub fn new(base_url: impl Into<String>) -> Self {
1319 Self {
1320 base_url: base_url.into(),
1321 ..Self::default()
1322 }
1323 }
1324
1325 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1327 self.provider_type = Some(provider_type.into());
1328 self
1329 }
1330
1331 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1333 self.wire_api = Some(wire_api.into());
1334 self
1335 }
1336
1337 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1340 self.transport = Some(transport.into());
1341 self
1342 }
1343
1344 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1346 self.api_key = Some(api_key.into());
1347 self
1348 }
1349
1350 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1353 self.bearer_token = Some(bearer_token.into());
1354 self
1355 }
1356
1357 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1363 self.bearer_token_provider = Some(provider);
1364 self
1365 }
1366
1367 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1369 self.azure = Some(azure);
1370 self
1371 }
1372
1373 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1375 self.headers = Some(headers);
1376 self
1377 }
1378
1379 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1382 self.model_id = Some(model_id.into());
1383 self
1384 }
1385
1386 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1391 self.wire_model = Some(wire_model.into());
1392 self
1393 }
1394
1395 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1399 self.max_prompt_tokens = Some(max);
1400 self
1401 }
1402
1403 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1406 self.max_output_tokens = Some(max);
1407 self
1408 }
1409}
1410
1411#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1424#[serde(rename_all = "camelCase")]
1425#[non_exhaustive]
1426pub struct CapiSessionOptions {
1427 #[serde(default, skip_serializing_if = "Option::is_none")]
1438 pub auto_tier: Option<AutoTier>,
1439
1440 #[serde(default, skip_serializing_if = "Option::is_none")]
1446 pub enable_web_socket_responses: Option<bool>,
1447}
1448
1449impl CapiSessionOptions {
1450 pub fn new() -> Self {
1452 Self::default()
1453 }
1454
1455 pub fn with_auto_tier(mut self, auto_tier: AutoTier) -> Self {
1457 self.auto_tier = Some(auto_tier);
1458 self
1459 }
1460
1461 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1463 self.enable_web_socket_responses = Some(enable);
1464 self
1465 }
1466}
1467
1468#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1470#[serde(rename_all = "camelCase")]
1471pub struct AzureProviderOptions {
1472 #[serde(default, skip_serializing_if = "Option::is_none")]
1474 pub api_version: Option<String>,
1475}
1476
1477#[derive(Clone, Default, Serialize, Deserialize)]
1488#[serde(rename_all = "camelCase")]
1489#[non_exhaustive]
1490pub struct NamedProviderConfig {
1491 pub name: String,
1494 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1497 pub provider_type: Option<String>,
1498 #[serde(default, skip_serializing_if = "Option::is_none")]
1501 pub wire_api: Option<String>,
1502 pub base_url: String,
1504 #[serde(default, skip_serializing_if = "Option::is_none")]
1506 pub api_key: Option<String>,
1507 #[serde(default, skip_serializing_if = "Option::is_none")]
1510 pub bearer_token: Option<String>,
1511 #[serde(skip)]
1514 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1515 #[serde(default, skip_serializing_if = "Option::is_none")]
1516 pub(crate) has_bearer_token_provider: Option<bool>,
1517 #[serde(default, skip_serializing_if = "Option::is_none")]
1519 pub azure: Option<AzureProviderOptions>,
1520 #[serde(default, skip_serializing_if = "Option::is_none")]
1522 pub headers: Option<HashMap<String, String>>,
1523}
1524
1525impl std::fmt::Debug for NamedProviderConfig {
1526 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1527 f.debug_struct("NamedProviderConfig")
1528 .field("name", &self.name)
1529 .field("provider_type", &self.provider_type)
1530 .field("wire_api", &self.wire_api)
1531 .field("base_url", &self.base_url)
1532 .field("api_key", &self.api_key)
1533 .field("bearer_token", &self.bearer_token)
1534 .field(
1535 "bearer_token_provider",
1536 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1537 )
1538 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1539 .field("azure", &self.azure)
1540 .field("headers", &self.headers)
1541 .finish()
1542 }
1543}
1544
1545impl NamedProviderConfig {
1546 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1549 Self {
1550 name: name.into(),
1551 base_url: base_url.into(),
1552 ..Self::default()
1553 }
1554 }
1555
1556 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1558 self.provider_type = Some(provider_type.into());
1559 self
1560 }
1561
1562 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1564 self.wire_api = Some(wire_api.into());
1565 self
1566 }
1567
1568 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1570 self.api_key = Some(api_key.into());
1571 self
1572 }
1573
1574 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1577 self.bearer_token = Some(bearer_token.into());
1578 self
1579 }
1580
1581 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1587 self.bearer_token_provider = Some(provider);
1588 self
1589 }
1590
1591 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1593 self.azure = Some(azure);
1594 self
1595 }
1596
1597 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1599 self.headers = Some(headers);
1600 self
1601 }
1602}
1603
1604fn prepare_bearer_token_providers(
1605 provider: &mut Option<ProviderConfig>,
1606 providers: &mut Option<Vec<NamedProviderConfig>>,
1607) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1608 let mut bearer_token_providers = HashMap::new();
1609
1610 if let Some(provider) = provider.as_mut()
1611 && let Some(token_provider) = provider.bearer_token_provider.take()
1612 {
1613 provider.has_bearer_token_provider = Some(true);
1614 bearer_token_providers.insert("default".to_string(), token_provider);
1615 }
1616
1617 if let Some(providers) = providers.as_mut() {
1618 for provider in providers {
1619 if let Some(token_provider) = provider.bearer_token_provider.take() {
1620 provider.has_bearer_token_provider = Some(true);
1621 bearer_token_providers.insert(provider.name.clone(), token_provider);
1622 }
1623 }
1624 }
1625
1626 bearer_token_providers
1627}
1628
1629#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1637#[serde(rename_all = "camelCase")]
1638#[non_exhaustive]
1639pub struct ProviderModelConfig {
1640 pub id: String,
1643 pub provider: String,
1645 #[serde(default, skip_serializing_if = "Option::is_none")]
1648 pub wire_model: Option<String>,
1649 #[serde(default, skip_serializing_if = "Option::is_none")]
1652 pub model_id: Option<String>,
1653 #[serde(default, skip_serializing_if = "Option::is_none")]
1655 pub name: Option<String>,
1656 #[serde(default, skip_serializing_if = "Option::is_none")]
1658 pub max_prompt_tokens: Option<i64>,
1659 #[serde(default, skip_serializing_if = "Option::is_none")]
1661 pub max_context_window_tokens: Option<i64>,
1662 #[serde(default, skip_serializing_if = "Option::is_none")]
1664 pub max_output_tokens: Option<i64>,
1665 #[serde(default, skip_serializing_if = "Option::is_none")]
1668 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1669}
1670
1671impl ProviderModelConfig {
1672 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1675 Self {
1676 id: id.into(),
1677 provider: provider.into(),
1678 ..Self::default()
1679 }
1680 }
1681
1682 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1684 self.wire_model = Some(wire_model.into());
1685 self
1686 }
1687
1688 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1691 self.model_id = Some(model_id.into());
1692 self
1693 }
1694
1695 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1697 self.name = Some(name.into());
1698 self
1699 }
1700
1701 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1703 self.max_prompt_tokens = Some(max);
1704 self
1705 }
1706
1707 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1709 self.max_context_window_tokens = Some(max);
1710 self
1711 }
1712
1713 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1715 self.max_output_tokens = Some(max);
1716 self
1717 }
1718
1719 pub fn with_capabilities(
1721 mut self,
1722 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1723 ) -> Self {
1724 self.capabilities = Some(capabilities);
1725 self
1726 }
1727}
1728
1729#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1733#[serde(untagged)]
1734pub enum ExpFlagValue {
1735 Bool(bool),
1737 Integer(i64),
1739 Float(f64),
1741 String(String),
1743 Null,
1745}
1746
1747#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1751#[serde(rename_all = "PascalCase")]
1752pub struct ExpConfigEntry {
1753 pub id: String,
1755 pub parameters: HashMap<String, ExpFlagValue>,
1757}
1758
1759#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1765#[serde(rename_all = "PascalCase")]
1766pub struct CopilotExpAssignmentResponse {
1767 #[serde(default)]
1769 pub features: Vec<String>,
1770 #[serde(default)]
1772 pub flights: HashMap<String, String>,
1773 #[serde(default)]
1775 pub configs: Vec<ExpConfigEntry>,
1776 #[serde(default, skip_serializing_if = "Option::is_none")]
1778 pub parameter_groups: Option<Value>,
1779 #[serde(default, skip_serializing_if = "Option::is_none")]
1781 pub flighting_version: Option<i64>,
1782 #[serde(default, skip_serializing_if = "Option::is_none")]
1784 pub impression_id: Option<String>,
1785 #[serde(default)]
1787 pub assignment_context: String,
1788}
1789
1790pub struct DisableBypassPermissionsModes;
1792
1793impl DisableBypassPermissionsModes {
1794 pub const ALLOW_AUTO_ONLY: &'static str = "allow-auto-only";
1796 pub const DISABLE: &'static str = "disable";
1798}
1799
1800#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1809#[serde(rename_all = "camelCase")]
1810#[non_exhaustive]
1811pub struct ManagedSettingsPermissions {
1812 #[serde(default, skip_serializing_if = "Option::is_none")]
1816 pub disable_bypass_permissions_mode: Option<String>,
1817 #[serde(default, skip_serializing_if = "Option::is_none")]
1819 pub deny: Option<Vec<String>>,
1820 #[serde(default, skip_serializing_if = "Option::is_none")]
1822 pub ask: Option<Vec<String>>,
1823 #[serde(default, skip_serializing_if = "Option::is_none")]
1825 pub allow: Option<Vec<String>>,
1826}
1827
1828impl ManagedSettingsPermissions {
1829 pub fn with_disable_bypass_permissions_mode(mut self, value: impl Into<String>) -> Self {
1831 self.disable_bypass_permissions_mode = Some(value.into());
1832 self
1833 }
1834
1835 pub fn with_deny(mut self, rules: Vec<String>) -> Self {
1837 self.deny = Some(rules);
1838 self
1839 }
1840
1841 pub fn with_ask(mut self, rules: Vec<String>) -> Self {
1843 self.ask = Some(rules);
1844 self
1845 }
1846
1847 pub fn with_allow(mut self, rules: Vec<String>) -> Self {
1849 self.allow = Some(rules);
1850 self
1851 }
1852}
1853
1854#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1864#[serde(rename_all = "camelCase")]
1865#[non_exhaustive]
1866pub struct ManagedSettings {
1867 #[serde(default, skip_serializing_if = "Option::is_none")]
1869 pub permissions: Option<ManagedSettingsPermissions>,
1870}
1871
1872impl ManagedSettings {
1873 pub fn with_permissions(mut self, permissions: ManagedSettingsPermissions) -> Self {
1875 self.permissions = Some(permissions);
1876 self
1877 }
1878}
1879
1880#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1882#[serde(rename_all = "lowercase")]
1883#[non_exhaustive]
1884pub enum AskUserVariant {
1885 #[default]
1887 Legacy,
1888 Elicitation,
1890}
1891
1892#[derive(Clone)]
1944#[non_exhaustive]
1945pub struct SessionConfig {
1946 pub session_id: Option<SessionId>,
1948 pub model: Option<String>,
1950 pub allowed_models: Option<Vec<String>>,
1955 pub client_name: Option<String>,
1957 pub reasoning_effort: Option<String>,
1959 pub reasoning_summary: Option<ReasoningSummary>,
1963 pub context_tier: Option<String>,
1966 pub streaming: Option<bool>,
1968 pub system_message: Option<SystemMessageConfig>,
1970 pub ask_user_variant: Option<AskUserVariant>,
1975 pub tools: Option<Vec<Tool>>,
1977 pub canvases: Option<Vec<CanvasDeclaration>>,
1979 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1984 pub request_canvas_renderer: Option<bool>,
1986 pub request_extensions: Option<bool>,
1988 pub extension_sdk_path: Option<String>,
1992 pub extension_info: Option<ExtensionInfo>,
1994 pub canvas_provider: Option<CanvasProviderIdentity>,
1997 pub available_tools: Option<Vec<String>>,
1999 pub excluded_tools: Option<Vec<String>>,
2001 pub excluded_builtin_agents: Option<Vec<String>>,
2007 pub included_builtin_skills: Option<Vec<String>>,
2011 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
2013 pub mcp_oauth_token_storage: Option<String>,
2022 pub auth_client_id_metadata_url: Option<String>,
2029 pub enable_config_discovery: Option<bool>,
2032 pub skip_embedding_retrieval: Option<bool>,
2034 pub embedding_cache_storage: Option<String>,
2037 pub organization_custom_instructions: Option<String>,
2039 pub enable_on_demand_instruction_discovery: Option<bool>,
2041 pub enable_file_hooks: Option<bool>,
2043 pub enable_host_git_operations: Option<bool>,
2045 pub enable_session_store: Option<bool>,
2047 pub enable_skills: Option<bool>,
2049 pub enable_mcp_apps: Option<bool>,
2076 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
2081 pub skill_directories: Option<Vec<PathBuf>>,
2083 pub instruction_directories: Option<Vec<PathBuf>>,
2086 pub plugin_directories: Option<Vec<PathBuf>>,
2088 pub large_output: Option<LargeToolOutputConfig>,
2090 pub tool_search: Option<ToolSearchConfig>,
2094 pub disabled_skills: Option<Vec<String>>,
2097 pub disabled_mcp_servers: Option<Vec<String>>,
2101 pub hooks: Option<bool>,
2105 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2107 pub default_agent: Option<DefaultAgentConfig>,
2111 pub agent: Option<String>,
2114 pub infinite_sessions: Option<InfiniteSessionConfig>,
2117 pub provider: Option<ProviderConfig>,
2121 pub capi: Option<CapiSessionOptions>,
2127 pub providers: Option<Vec<NamedProviderConfig>>,
2134 pub models: Option<Vec<ProviderModelConfig>>,
2140 pub enable_session_telemetry: Option<bool>,
2148 pub enable_citations: Option<bool>,
2150 pub enable_file_change_tracking: Option<bool>,
2153 pub session_limits: Option<SessionLimitsConfig>,
2155 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2158 pub memory: Option<MemoryConfiguration>,
2160 pub config_directory: Option<PathBuf>,
2163 pub working_directory: Option<PathBuf>,
2166 pub additional_directories: Option<Vec<PathBuf>>,
2170 pub github_token: Option<String>,
2176 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2182 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2188 pub cloud: Option<CloudSessionOptions>,
2191 pub include_sub_agent_streaming_events: Option<bool>,
2195 pub commands: Option<Vec<CommandDefinition>>,
2199 pub feature_flags: Option<HashMap<String, bool>>,
2205 #[doc(hidden)]
2212 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
2213 pub enable_managed_settings: Option<bool>,
2221 pub managed_settings: Option<ManagedSettings>,
2230 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2235 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2239 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2242 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2245 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2249 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2252 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2255 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2259 pub(crate) permission_policy: Option<crate::permission::Policy>,
2263 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2268 pub skip_custom_instructions: Option<bool>,
2272 pub custom_agents_local_only: Option<bool>,
2276 pub enable_experimental_mode: Option<bool>,
2281 pub coauthor_enabled: Option<bool>,
2285 pub manage_schedule_enabled: Option<bool>,
2289 pub event_buffer_capacity: Option<usize>,
2306}
2307
2308impl std::fmt::Debug for SessionConfig {
2309 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2310 f.debug_struct("SessionConfig")
2311 .field("session_id", &self.session_id)
2312 .field("model", &self.model)
2313 .field("allowed_models", &self.allowed_models)
2314 .field("client_name", &self.client_name)
2315 .field("reasoning_effort", &self.reasoning_effort)
2316 .field("reasoning_summary", &self.reasoning_summary)
2317 .field("context_tier", &self.context_tier)
2318 .field("streaming", &self.streaming)
2319 .field("system_message", &self.system_message)
2320 .field("ask_user_variant", &self.ask_user_variant)
2321 .field("tools", &self.tools)
2322 .field("canvases", &self.canvases)
2323 .field(
2324 "canvas_handler",
2325 &self.canvas_handler.as_ref().map(|_| "<set>"),
2326 )
2327 .field("request_canvas_renderer", &self.request_canvas_renderer)
2328 .field("request_extensions", &self.request_extensions)
2329 .field("extension_sdk_path", &self.extension_sdk_path)
2330 .field("extension_info", &self.extension_info)
2331 .field("canvas_provider", &self.canvas_provider)
2332 .field("available_tools", &self.available_tools)
2333 .field("excluded_tools", &self.excluded_tools)
2334 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2335 .field("included_builtin_skills", &self.included_builtin_skills)
2336 .field("mcp_servers", &self.mcp_servers)
2337 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2338 .field(
2339 "auth_client_id_metadata_url",
2340 &self.auth_client_id_metadata_url,
2341 )
2342 .field("embedding_cache_storage", &self.embedding_cache_storage)
2343 .field("enable_config_discovery", &self.enable_config_discovery)
2344 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2345 .field(
2346 "organization_custom_instructions",
2347 &self
2348 .organization_custom_instructions
2349 .as_ref()
2350 .map(|_| "<redacted>"),
2351 )
2352 .field(
2353 "enable_on_demand_instruction_discovery",
2354 &self.enable_on_demand_instruction_discovery,
2355 )
2356 .field("enable_file_hooks", &self.enable_file_hooks)
2357 .field(
2358 "enable_host_git_operations",
2359 &self.enable_host_git_operations,
2360 )
2361 .field("enable_session_store", &self.enable_session_store)
2362 .field("enable_skills", &self.enable_skills)
2363 .field("enable_mcp_apps", &self.enable_mcp_apps)
2364 .field("skill_directories", &self.skill_directories)
2365 .field("instruction_directories", &self.instruction_directories)
2366 .field("plugin_directories", &self.plugin_directories)
2367 .field("large_output", &self.large_output)
2368 .field("tool_search", &self.tool_search)
2369 .field("disabled_skills", &self.disabled_skills)
2370 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
2371 .field("hooks", &self.hooks)
2372 .field("custom_agents", &self.custom_agents)
2373 .field("default_agent", &self.default_agent)
2374 .field("agent", &self.agent)
2375 .field("infinite_sessions", &self.infinite_sessions)
2376 .field("provider", &self.provider)
2377 .field("capi", &self.capi)
2378 .field("enable_session_telemetry", &self.enable_session_telemetry)
2379 .field("enable_citations", &self.enable_citations)
2380 .field(
2381 "enable_file_change_tracking",
2382 &self.enable_file_change_tracking,
2383 )
2384 .field("session_limits", &self.session_limits)
2385 .field("model_capabilities", &self.model_capabilities)
2386 .field("memory", &self.memory)
2387 .field("config_directory", &self.config_directory)
2388 .field("working_directory", &self.working_directory)
2389 .field("additional_directories", &self.additional_directories)
2390 .field(
2391 "github_token",
2392 &self.github_token.as_ref().map(|_| "<redacted>"),
2393 )
2394 .field(
2395 "github_token_provider",
2396 &self.github_token_provider.as_ref().map(|_| "<set>"),
2397 )
2398 .field("remote_session", &self.remote_session)
2399 .field("cloud", &self.cloud)
2400 .field(
2401 "include_sub_agent_streaming_events",
2402 &self.include_sub_agent_streaming_events,
2403 )
2404 .field("commands", &self.commands)
2405 .field("feature_flags", &self.feature_flags)
2406 .field("exp_assignments", &self.exp_assignments)
2407 .field("enable_managed_settings", &self.enable_managed_settings)
2408 .field("enable_experimental_mode", &self.enable_experimental_mode)
2409 .field("managed_settings", &self.managed_settings)
2410 .field(
2411 "session_fs_provider",
2412 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2413 )
2414 .field(
2415 "permission_handler",
2416 &self.permission_handler.as_ref().map(|_| "<set>"),
2417 )
2418 .field(
2419 "elicitation_handler",
2420 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2421 )
2422 .field(
2423 "mcp_auth_handler",
2424 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2425 )
2426 .field(
2427 "user_input_handler",
2428 &self.user_input_handler.as_ref().map(|_| "<set>"),
2429 )
2430 .field(
2431 "exit_plan_mode_handler",
2432 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2433 )
2434 .field(
2435 "auto_mode_switch_handler",
2436 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2437 )
2438 .field(
2439 "hooks_handler",
2440 &self.hooks_handler.as_ref().map(|_| "<set>"),
2441 )
2442 .field(
2443 "system_message_transform",
2444 &self.system_message_transform.as_ref().map(|_| "<set>"),
2445 )
2446 .field("event_buffer_capacity", &self.event_buffer_capacity)
2447 .finish()
2448 }
2449}
2450
2451impl Default for SessionConfig {
2452 fn default() -> Self {
2458 Self {
2459 session_id: None,
2460 model: None,
2461 allowed_models: None,
2462 client_name: None,
2463 reasoning_effort: None,
2464 reasoning_summary: None,
2465 context_tier: None,
2466 streaming: None,
2467 system_message: None,
2468 ask_user_variant: None,
2469 tools: None,
2470 canvases: None,
2471 canvas_handler: None,
2472 request_canvas_renderer: None,
2473 request_extensions: None,
2474 extension_sdk_path: None,
2475 extension_info: None,
2476 canvas_provider: None,
2477 available_tools: None,
2478 excluded_tools: None,
2479 excluded_builtin_agents: None,
2480 included_builtin_skills: None,
2481 mcp_servers: None,
2482 mcp_oauth_token_storage: None,
2483 auth_client_id_metadata_url: None,
2484 enable_config_discovery: None,
2485 skip_embedding_retrieval: None,
2486 organization_custom_instructions: None,
2487 enable_on_demand_instruction_discovery: None,
2488 enable_file_hooks: None,
2489 enable_host_git_operations: None,
2490 enable_session_store: None,
2491 enable_skills: None,
2492 embedding_cache_storage: None,
2493 enable_mcp_apps: None,
2494 github_mcp_tool_config: None,
2495 skill_directories: None,
2496 instruction_directories: None,
2497 plugin_directories: None,
2498 large_output: None,
2499 tool_search: None,
2500 disabled_skills: None,
2501 disabled_mcp_servers: None,
2502 hooks: None,
2503 custom_agents: None,
2504 default_agent: None,
2505 agent: None,
2506 infinite_sessions: None,
2507 provider: None,
2508 capi: None,
2509 providers: None,
2510 models: None,
2511 enable_session_telemetry: None,
2512 enable_citations: None,
2513 enable_file_change_tracking: None,
2514 session_limits: None,
2515 model_capabilities: None,
2516 memory: None,
2517 config_directory: None,
2518 working_directory: None,
2519 additional_directories: None,
2520 github_token: None,
2521 github_token_provider: None,
2522 remote_session: None,
2523 cloud: None,
2524 include_sub_agent_streaming_events: None,
2525 commands: None,
2526 feature_flags: None,
2527 exp_assignments: None,
2528 enable_managed_settings: None,
2529 managed_settings: None,
2530 session_fs_provider: None,
2531 permission_handler: None,
2532 elicitation_handler: None,
2533 mcp_auth_handler: None,
2534 user_input_handler: None,
2535 exit_plan_mode_handler: None,
2536 auto_mode_switch_handler: None,
2537 hooks_handler: None,
2538 permission_policy: None,
2539 system_message_transform: None,
2540 skip_custom_instructions: None,
2541 custom_agents_local_only: None,
2542 enable_experimental_mode: None,
2543 coauthor_enabled: None,
2544 manage_schedule_enabled: None,
2545 event_buffer_capacity: None,
2546 }
2547 }
2548}
2549
2550pub(crate) struct SessionConfigRuntime {
2556 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2557 pub permission_policy: Option<crate::permission::Policy>,
2558 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2559 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2560 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2561 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2562 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2563 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2564 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2565 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2566 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2567 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2568 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2569 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
2570 pub commands: Option<Vec<CommandDefinition>>,
2571}
2572
2573impl SessionConfig {
2574 pub(crate) fn into_wire(
2586 mut self,
2587 session_id: Option<SessionId>,
2588 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2589 if self.github_token.is_some() && self.github_token_provider.is_some() {
2590 return Err(crate::Error::with_message(
2591 crate::ErrorKind::InvalidConfig,
2592 "github_token and github_token_provider are mutually exclusive",
2593 ));
2594 }
2595 let permission_active =
2596 self.permission_handler.is_some() || self.permission_policy.is_some();
2597 let request_user_input = self.user_input_handler.is_some();
2598 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2599 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2600 let request_elicitation = self.elicitation_handler.is_some();
2601 let hooks_flag = self.hooks_handler.is_some();
2602
2603 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2604 if let Some(tools) = self.tools.as_mut() {
2605 for tool in tools.iter_mut() {
2606 if let Some(handler) = tool.handler.take()
2607 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2608 {
2609 return Err(crate::Error::with_message(
2610 crate::ErrorKind::InvalidConfig,
2611 format!("duplicate tool handler registered for name {:?}", tool.name),
2612 ));
2613 }
2614 }
2615 }
2616
2617 let wire_commands = self.commands.as_ref().map(|cmds| {
2618 cmds.iter()
2619 .map(|c| crate::wire::CommandWireDefinition {
2620 name: c.name.clone(),
2621 description: c.description.clone().unwrap_or_default(),
2622 })
2623 .collect()
2624 });
2625 let wire_canvases = self.canvases.clone();
2626 let canvas_handler = self.canvas_handler.clone();
2627 let bearer_token_providers =
2628 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2629
2630 let wire = crate::wire::SessionCreateWire {
2631 session_id,
2632 model: self.model,
2633 allowed_models: self.allowed_models,
2634 client_name: self.client_name,
2635 reasoning_effort: self.reasoning_effort,
2636 reasoning_summary: self.reasoning_summary,
2637 context_tier: self.context_tier,
2638 streaming: self.streaming,
2639 system_message: self.system_message,
2640 ask_user_variant: self.ask_user_variant,
2641 tools: self.tools,
2642 canvases: wire_canvases,
2643 request_canvas_renderer: self.request_canvas_renderer,
2644 request_extensions: self.request_extensions,
2645 extension_sdk_path: self.extension_sdk_path,
2646 extension_info: self.extension_info,
2647 canvas_provider: self.canvas_provider,
2648 available_tools: self.available_tools,
2649 excluded_tools: self.excluded_tools,
2650 excluded_builtin_agents: self.excluded_builtin_agents,
2651 tool_filter_precedence: "excluded",
2652 mcp_servers: self.mcp_servers,
2653 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2654 auth_client_id_metadata_url: self.auth_client_id_metadata_url,
2655 embedding_cache_storage: self.embedding_cache_storage,
2656 env_value_mode: "direct",
2657 enable_config_discovery: self.enable_config_discovery,
2658 skip_embedding_retrieval: self.skip_embedding_retrieval,
2659 organization_custom_instructions: self.organization_custom_instructions,
2660 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2661 enable_file_hooks: self.enable_file_hooks,
2662 enable_host_git_operations: self.enable_host_git_operations,
2663 enable_session_store: self.enable_session_store,
2664 enable_skills: self.enable_skills,
2665 request_user_input,
2666 request_permission: permission_active,
2667 request_exit_plan_mode,
2668 request_auto_mode_switch,
2669 request_elicitation,
2670 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2671 github_mcp_tool_config: self.github_mcp_tool_config,
2672 hooks: hooks_flag,
2673 skill_directories: self.skill_directories,
2674 instruction_directories: self.instruction_directories,
2675 plugin_directories: self.plugin_directories,
2676 large_output: self.large_output,
2677 tool_search: self.tool_search,
2678 disabled_skills: self.disabled_skills,
2679 disabled_mcp_servers: self.disabled_mcp_servers,
2680 custom_agents: self.custom_agents,
2681 custom_agents_local_only: self.custom_agents_local_only,
2682 default_agent: self.default_agent,
2683 agent: self.agent,
2684 infinite_sessions: self.infinite_sessions,
2685 provider: self.provider,
2686 capi: self.capi,
2687 providers: self.providers,
2688 models: self.models,
2689 enable_session_telemetry: self.enable_session_telemetry,
2690 enable_citations: self.enable_citations,
2691 enable_file_change_tracking: self.enable_file_change_tracking,
2692 session_limits: self.session_limits,
2693 model_capabilities: self.model_capabilities,
2694 memory: self.memory,
2695 config_dir: self.config_directory,
2696 working_directory: self.working_directory,
2697 additional_directories: self.additional_directories,
2698 github_token: self.github_token,
2699 github_token_provider_registration_id: None,
2700 remote_session: self.remote_session,
2701 cloud: self.cloud,
2702 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2703 enable_github_telemetry_forwarding: None,
2704 commands: wire_commands,
2705 feature_flags: self.feature_flags,
2706 exp_assignments: self.exp_assignments,
2707 enable_managed_settings: self.enable_managed_settings,
2708 is_experimental_mode: self.enable_experimental_mode,
2709 managed_settings: self.managed_settings,
2710 };
2711
2712 let runtime = SessionConfigRuntime {
2713 permission_handler: self.permission_handler,
2714 permission_policy: self.permission_policy,
2715 elicitation_handler: self.elicitation_handler,
2716 mcp_auth_handler: self.mcp_auth_handler,
2717 user_input_handler: self.user_input_handler,
2718 exit_plan_mode_handler: self.exit_plan_mode_handler,
2719 auto_mode_switch_handler: self.auto_mode_switch_handler,
2720 hooks_handler: self.hooks_handler,
2721 system_message_transform: self.system_message_transform,
2722 tool_handlers,
2723 canvas_handler,
2724 session_fs_provider: self.session_fs_provider,
2725 bearer_token_providers,
2726 github_token_provider: self.github_token_provider,
2727 commands: self.commands,
2728 };
2729
2730 Ok((wire, runtime))
2731 }
2732
2733 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2737 self.permission_handler = Some(handler);
2738 self
2739 }
2740
2741 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2744 self.elicitation_handler = Some(handler);
2745 self
2746 }
2747
2748 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2750 self.mcp_auth_handler = Some(handler);
2751 self
2752 }
2753
2754 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2757 self.user_input_handler = Some(handler);
2758 self
2759 }
2760
2761 pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self {
2763 self.ask_user_variant = Some(variant);
2764 self
2765 }
2766
2767 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2769 self.exit_plan_mode_handler = Some(handler);
2770 self
2771 }
2772
2773 pub fn with_auto_mode_switch_handler(
2775 mut self,
2776 handler: Arc<dyn AutoModeSwitchHandler>,
2777 ) -> Self {
2778 self.auto_mode_switch_handler = Some(handler);
2779 self
2780 }
2781
2782 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2787 self.commands = Some(commands);
2788 self
2789 }
2790
2791 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2795 self.session_fs_provider = Some(provider);
2796 self
2797 }
2798
2799 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2802 self.hooks_handler = Some(hooks);
2803 self
2804 }
2805
2806 pub fn with_system_message_transform(
2810 mut self,
2811 transform: Arc<dyn SystemMessageTransform>,
2812 ) -> Self {
2813 self.system_message_transform = Some(transform);
2814 self
2815 }
2816
2817 pub fn approve_all_permissions(mut self) -> Self {
2823 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2824 self
2825 }
2826
2827 pub fn deny_all_permissions(mut self) -> Self {
2830 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2831 self
2832 }
2833
2834 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2839 where
2840 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2841 {
2842 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2843 self
2844 }
2845
2846 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2848 self.session_id = Some(id.into());
2849 self
2850 }
2851
2852 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2854 self.model = Some(model.into());
2855 self
2856 }
2857
2858 pub fn with_allowed_models<I, S>(mut self, models: I) -> Self
2863 where
2864 I: IntoIterator<Item = S>,
2865 S: Into<String>,
2866 {
2867 self.allowed_models = Some(models.into_iter().map(Into::into).collect());
2868 self
2869 }
2870
2871 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2873 self.client_name = Some(name.into());
2874 self
2875 }
2876
2877 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2879 self.reasoning_effort = Some(effort.into());
2880 self
2881 }
2882
2883 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2885 self.reasoning_summary = Some(summary);
2886 self
2887 }
2888
2889 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2891 self.context_tier = Some(tier.into());
2892 self
2893 }
2894
2895 pub fn with_streaming(mut self, streaming: bool) -> Self {
2897 self.streaming = Some(streaming);
2898 self
2899 }
2900
2901 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2903 self.system_message = Some(system_message);
2904 self
2905 }
2906
2907 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2909 self.tools = Some(tools.into_iter().collect());
2910 self
2911 }
2912
2913 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2918 self.canvases = Some(canvases.into_iter().collect());
2919 self
2920 }
2921
2922 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2924 self.canvas_handler = Some(handler);
2925 self
2926 }
2927
2928 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2930 self.request_canvas_renderer = Some(request);
2931 self
2932 }
2933
2934 pub fn with_request_extensions(mut self, request: bool) -> Self {
2936 self.request_extensions = Some(request);
2937 self
2938 }
2939
2940 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2944 self.extension_sdk_path = Some(path.into());
2945 self
2946 }
2947
2948 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2950 self.extension_info = Some(extension_info);
2951 self
2952 }
2953
2954 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2957 self.canvas_provider = Some(canvas_provider);
2958 self
2959 }
2960
2961 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2963 where
2964 I: IntoIterator<Item = S>,
2965 S: Into<String>,
2966 {
2967 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2968 self
2969 }
2970
2971 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2973 where
2974 I: IntoIterator<Item = S>,
2975 S: Into<String>,
2976 {
2977 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2978 self
2979 }
2980
2981 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2983 where
2984 I: IntoIterator<Item = S>,
2985 S: Into<String>,
2986 {
2987 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2988 self
2989 }
2990
2991 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2993 self.mcp_servers = Some(servers);
2994 self
2995 }
2996
2997 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3005 self.mcp_oauth_token_storage = Some(mode.into());
3006 self
3007 }
3008
3009 pub fn with_auth_client_id_metadata_url(mut self, url: impl Into<String>) -> Self {
3011 self.auth_client_id_metadata_url = Some(url.into());
3012 self
3013 }
3014
3015 pub fn with_embedding_cache_storage(
3017 mut self,
3018 embedding_cache_storage: impl Into<String>,
3019 ) -> Self {
3020 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3021 self
3022 }
3023
3024 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3027 self.enable_config_discovery = Some(enable);
3028 self
3029 }
3030
3031 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3033 self.skip_embedding_retrieval = Some(value);
3034 self
3035 }
3036
3037 pub fn with_organization_custom_instructions(
3039 mut self,
3040 instructions: impl Into<String>,
3041 ) -> Self {
3042 self.organization_custom_instructions = Some(instructions.into());
3043 self
3044 }
3045
3046 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3048 self.enable_on_demand_instruction_discovery = Some(value);
3049 self
3050 }
3051
3052 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3054 self.enable_file_hooks = Some(value);
3055 self
3056 }
3057
3058 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3060 self.enable_host_git_operations = Some(value);
3061 self
3062 }
3063
3064 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3066 self.enable_session_store = Some(value);
3067 self
3068 }
3069
3070 pub fn with_enable_skills(mut self, value: bool) -> Self {
3072 self.enable_skills = Some(value);
3073 self
3074 }
3075
3076 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3082 self.enable_mcp_apps = Some(enable);
3083 self
3084 }
3085
3086 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
3088 self.github_mcp_tool_config = Some(config);
3089 self
3090 }
3091
3092 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3094 where
3095 I: IntoIterator<Item = P>,
3096 P: Into<PathBuf>,
3097 {
3098 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3099 self
3100 }
3101
3102 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
3104 where
3105 I: IntoIterator<Item = S>,
3106 S: Into<String>,
3107 {
3108 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
3109 self
3110 }
3111
3112 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3116 where
3117 I: IntoIterator<Item = P>,
3118 P: Into<PathBuf>,
3119 {
3120 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3121 self
3122 }
3123
3124 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3126 where
3127 I: IntoIterator<Item = P>,
3128 P: Into<PathBuf>,
3129 {
3130 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3131 self
3132 }
3133
3134 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3136 self.large_output = Some(config);
3137 self
3138 }
3139
3140 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
3143 self.tool_search = Some(config);
3144 self
3145 }
3146
3147 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3149 where
3150 I: IntoIterator<Item = S>,
3151 S: Into<String>,
3152 {
3153 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3154 self
3155 }
3156
3157 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
3159 where
3160 I: IntoIterator<Item = S>,
3161 S: Into<String>,
3162 {
3163 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
3164 self
3165 }
3166
3167 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3169 mut self,
3170 agents: I,
3171 ) -> Self {
3172 self.custom_agents = Some(agents.into_iter().collect());
3173 self
3174 }
3175
3176 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3178 self.default_agent = Some(agent);
3179 self
3180 }
3181
3182 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3185 self.agent = Some(name.into());
3186 self
3187 }
3188
3189 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3192 self.infinite_sessions = Some(config);
3193 self
3194 }
3195
3196 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3198 self.provider = Some(provider);
3199 self
3200 }
3201
3202 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3204 self.capi = Some(capi);
3205 self
3206 }
3207
3208 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3214 self.providers = Some(providers);
3215 self
3216 }
3217
3218 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3224 self.models = Some(models);
3225 self
3226 }
3227
3228 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3232 self.enable_session_telemetry = Some(enable);
3233 self
3234 }
3235
3236 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3238 self.enable_citations = Some(enable);
3239 self
3240 }
3241
3242 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
3245 self.enable_file_change_tracking = Some(enable);
3246 self
3247 }
3248
3249 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3251 self.session_limits = Some(limits);
3252 self
3253 }
3254
3255 pub fn with_model_capabilities(
3257 mut self,
3258 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3259 ) -> Self {
3260 self.model_capabilities = Some(capabilities);
3261 self
3262 }
3263
3264 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3266 self.memory = Some(memory);
3267 self
3268 }
3269
3270 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3272 self.config_directory = Some(dir.into());
3273 self
3274 }
3275
3276 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3279 self.working_directory = Some(dir.into());
3280 self
3281 }
3282
3283 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
3285 where
3286 I: IntoIterator<Item = P>,
3287 P: Into<PathBuf>,
3288 {
3289 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
3290 self
3291 }
3292
3293 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3298 self.github_token = Some(token.into());
3299 self
3300 }
3301
3302 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
3308 self.github_token_provider = Some(provider);
3309 self
3310 }
3311
3312 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3315 self.include_sub_agent_streaming_events = Some(include);
3316 self
3317 }
3318
3319 pub fn with_remote_session(
3321 mut self,
3322 mode: crate::generated::api_types::RemoteSessionMode,
3323 ) -> Self {
3324 self.remote_session = Some(mode);
3325 self
3326 }
3327
3328 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
3330 self.cloud = Some(cloud);
3331 self
3332 }
3333
3334 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3336 self.skip_custom_instructions = Some(value);
3337 self
3338 }
3339
3340 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3342 self.custom_agents_local_only = Some(value);
3343 self
3344 }
3345
3346 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
3348 self.enable_experimental_mode = Some(enable_experimental_mode);
3349 self
3350 }
3351
3352 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3354 self.coauthor_enabled = Some(value);
3355 self
3356 }
3357
3358 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3360 self.manage_schedule_enabled = Some(value);
3361 self
3362 }
3363
3364 pub fn with_feature_flags(mut self, feature_flags: HashMap<String, bool>) -> Self {
3366 self.feature_flags = Some(feature_flags);
3367 self
3368 }
3369
3370 pub fn with_event_buffer_capacity(mut self, capacity: usize) -> Self {
3378 self.event_buffer_capacity = Some(capacity);
3379 self
3380 }
3381
3382 #[doc(hidden)]
3390 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
3391 self.exp_assignments = Some(assignments);
3392 self
3393 }
3394
3395 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3402 self.enable_managed_settings = Some(enabled);
3403 self
3404 }
3405
3406 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
3411 self.managed_settings = Some(managed_settings);
3412 self
3413 }
3414}
3415#[derive(Clone)]
3422#[non_exhaustive]
3423pub struct ResumeSessionConfig {
3424 pub session_id: SessionId,
3426 pub model: Option<String>,
3429 pub allowed_models: Option<Vec<String>>,
3434 pub client_name: Option<String>,
3436 pub reasoning_effort: Option<String>,
3438 pub reasoning_summary: Option<ReasoningSummary>,
3442 pub context_tier: Option<String>,
3445 pub streaming: Option<bool>,
3447 pub system_message: Option<SystemMessageConfig>,
3450 pub ask_user_variant: Option<AskUserVariant>,
3455 pub tools: Option<Vec<Tool>>,
3457 pub canvases: Option<Vec<CanvasDeclaration>>,
3459 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
3462 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3464 pub request_canvas_renderer: Option<bool>,
3466 pub request_extensions: Option<bool>,
3468 pub extension_sdk_path: Option<String>,
3472 pub extension_info: Option<ExtensionInfo>,
3474 pub canvas_provider: Option<CanvasProviderIdentity>,
3477 pub available_tools: Option<Vec<String>>,
3479 pub excluded_tools: Option<Vec<String>>,
3481 pub excluded_builtin_agents: Option<Vec<String>>,
3487 pub included_builtin_skills: Option<Vec<String>>,
3491 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
3493 pub mcp_oauth_token_storage: Option<String>,
3496 pub auth_client_id_metadata_url: Option<String>,
3502 pub enable_config_discovery: Option<bool>,
3505 pub skip_embedding_retrieval: Option<bool>,
3507 pub embedding_cache_storage: Option<String>,
3509 pub organization_custom_instructions: Option<String>,
3511 pub enable_on_demand_instruction_discovery: Option<bool>,
3513 pub enable_file_hooks: Option<bool>,
3515 pub enable_host_git_operations: Option<bool>,
3517 pub enable_session_store: Option<bool>,
3519 pub enable_skills: Option<bool>,
3521 pub enable_mcp_apps: Option<bool>,
3527 pub github_mcp_tool_config: Option<GitHubMcpToolConfig>,
3532 pub skill_directories: Option<Vec<PathBuf>>,
3534 pub instruction_directories: Option<Vec<PathBuf>>,
3537 pub plugin_directories: Option<Vec<PathBuf>>,
3539 pub large_output: Option<LargeToolOutputConfig>,
3541 pub tool_search: Option<ToolSearchConfig>,
3544 pub disabled_skills: Option<Vec<String>>,
3546 pub disabled_mcp_servers: Option<Vec<String>>,
3549 pub hooks: Option<bool>,
3551 pub custom_agents: Option<Vec<CustomAgentConfig>>,
3553 pub default_agent: Option<DefaultAgentConfig>,
3555 pub agent: Option<String>,
3557 pub infinite_sessions: Option<InfiniteSessionConfig>,
3559 pub provider: Option<ProviderConfig>,
3561 pub capi: Option<CapiSessionOptions>,
3567 pub providers: Option<Vec<NamedProviderConfig>>,
3573 pub models: Option<Vec<ProviderModelConfig>>,
3579 pub enable_session_telemetry: Option<bool>,
3587 pub enable_citations: Option<bool>,
3589 pub enable_file_change_tracking: Option<bool>,
3593 pub session_limits: Option<SessionLimitsConfig>,
3595 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3597 pub memory: Option<MemoryConfiguration>,
3599 pub config_directory: Option<PathBuf>,
3601 pub working_directory: Option<PathBuf>,
3603 pub additional_directories: Option<Vec<PathBuf>>,
3606 pub github_token: Option<String>,
3609 pub github_token_provider: Option<Arc<dyn GitHubTokenProvider>>,
3612 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3615 pub include_sub_agent_streaming_events: Option<bool>,
3617 pub commands: Option<Vec<CommandDefinition>>,
3621 pub feature_flags: Option<HashMap<String, bool>>,
3625 #[doc(hidden)]
3630 pub exp_assignments: Option<CopilotExpAssignmentResponse>,
3631 pub enable_managed_settings: Option<bool>,
3637 pub managed_settings: Option<ManagedSettings>,
3643 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3648 pub suppress_resume_event: Option<bool>,
3651 pub continue_pending_work: Option<bool>,
3659 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3662 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3665 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3667 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3670 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3673 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3676 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3678 pub(crate) permission_policy: Option<crate::permission::Policy>,
3680 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3682 pub skip_custom_instructions: Option<bool>,
3684 pub custom_agents_local_only: Option<bool>,
3686 pub enable_experimental_mode: Option<bool>,
3691 pub coauthor_enabled: Option<bool>,
3693 pub manage_schedule_enabled: Option<bool>,
3695 pub event_buffer_capacity: Option<usize>,
3697}
3698
3699impl std::fmt::Debug for ResumeSessionConfig {
3700 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3701 f.debug_struct("ResumeSessionConfig")
3702 .field("session_id", &self.session_id)
3703 .field("model", &self.model)
3704 .field("allowed_models", &self.allowed_models)
3705 .field("client_name", &self.client_name)
3706 .field("reasoning_effort", &self.reasoning_effort)
3707 .field("reasoning_summary", &self.reasoning_summary)
3708 .field("context_tier", &self.context_tier)
3709 .field("streaming", &self.streaming)
3710 .field("system_message", &self.system_message)
3711 .field("ask_user_variant", &self.ask_user_variant)
3712 .field("tools", &self.tools)
3713 .field("canvases", &self.canvases)
3714 .field(
3715 "canvas_handler",
3716 &self.canvas_handler.as_ref().map(|_| "<set>"),
3717 )
3718 .field("open_canvases", &self.open_canvases)
3719 .field("request_canvas_renderer", &self.request_canvas_renderer)
3720 .field("request_extensions", &self.request_extensions)
3721 .field("extension_sdk_path", &self.extension_sdk_path)
3722 .field("extension_info", &self.extension_info)
3723 .field("canvas_provider", &self.canvas_provider)
3724 .field("available_tools", &self.available_tools)
3725 .field("excluded_tools", &self.excluded_tools)
3726 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3727 .field("included_builtin_skills", &self.included_builtin_skills)
3728 .field("mcp_servers", &self.mcp_servers)
3729 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3730 .field(
3731 "auth_client_id_metadata_url",
3732 &self.auth_client_id_metadata_url,
3733 )
3734 .field("embedding_cache_storage", &self.embedding_cache_storage)
3735 .field("enable_config_discovery", &self.enable_config_discovery)
3736 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3737 .field(
3738 "organization_custom_instructions",
3739 &self
3740 .organization_custom_instructions
3741 .as_ref()
3742 .map(|_| "<redacted>"),
3743 )
3744 .field(
3745 "enable_on_demand_instruction_discovery",
3746 &self.enable_on_demand_instruction_discovery,
3747 )
3748 .field("enable_file_hooks", &self.enable_file_hooks)
3749 .field(
3750 "enable_host_git_operations",
3751 &self.enable_host_git_operations,
3752 )
3753 .field("enable_session_store", &self.enable_session_store)
3754 .field("enable_skills", &self.enable_skills)
3755 .field("enable_mcp_apps", &self.enable_mcp_apps)
3756 .field("skill_directories", &self.skill_directories)
3757 .field("instruction_directories", &self.instruction_directories)
3758 .field("plugin_directories", &self.plugin_directories)
3759 .field("large_output", &self.large_output)
3760 .field("tool_search", &self.tool_search)
3761 .field("disabled_skills", &self.disabled_skills)
3762 .field("disabled_mcp_servers", &self.disabled_mcp_servers)
3763 .field("hooks", &self.hooks)
3764 .field("custom_agents", &self.custom_agents)
3765 .field("default_agent", &self.default_agent)
3766 .field("agent", &self.agent)
3767 .field("infinite_sessions", &self.infinite_sessions)
3768 .field("provider", &self.provider)
3769 .field("capi", &self.capi)
3770 .field("enable_session_telemetry", &self.enable_session_telemetry)
3771 .field("enable_citations", &self.enable_citations)
3772 .field(
3773 "enable_file_change_tracking",
3774 &self.enable_file_change_tracking,
3775 )
3776 .field("session_limits", &self.session_limits)
3777 .field("model_capabilities", &self.model_capabilities)
3778 .field("memory", &self.memory)
3779 .field("config_directory", &self.config_directory)
3780 .field("working_directory", &self.working_directory)
3781 .field("additional_directories", &self.additional_directories)
3782 .field(
3783 "github_token",
3784 &self.github_token.as_ref().map(|_| "<redacted>"),
3785 )
3786 .field(
3787 "github_token_provider",
3788 &self.github_token_provider.as_ref().map(|_| "<set>"),
3789 )
3790 .field("remote_session", &self.remote_session)
3791 .field(
3792 "include_sub_agent_streaming_events",
3793 &self.include_sub_agent_streaming_events,
3794 )
3795 .field("commands", &self.commands)
3796 .field("feature_flags", &self.feature_flags)
3797 .field("exp_assignments", &self.exp_assignments)
3798 .field("enable_managed_settings", &self.enable_managed_settings)
3799 .field("enable_experimental_mode", &self.enable_experimental_mode)
3800 .field("managed_settings", &self.managed_settings)
3801 .field(
3802 "session_fs_provider",
3803 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3804 )
3805 .field(
3806 "permission_handler",
3807 &self.permission_handler.as_ref().map(|_| "<set>"),
3808 )
3809 .field(
3810 "elicitation_handler",
3811 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3812 )
3813 .field(
3814 "user_input_handler",
3815 &self.user_input_handler.as_ref().map(|_| "<set>"),
3816 )
3817 .field(
3818 "exit_plan_mode_handler",
3819 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3820 )
3821 .field(
3822 "auto_mode_switch_handler",
3823 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3824 )
3825 .field(
3826 "hooks_handler",
3827 &self.hooks_handler.as_ref().map(|_| "<set>"),
3828 )
3829 .field(
3830 "system_message_transform",
3831 &self.system_message_transform.as_ref().map(|_| "<set>"),
3832 )
3833 .field("suppress_resume_event", &self.suppress_resume_event)
3834 .field("continue_pending_work", &self.continue_pending_work)
3835 .field("event_buffer_capacity", &self.event_buffer_capacity)
3836 .finish()
3837 }
3838}
3839
3840impl ResumeSessionConfig {
3841 pub(crate) fn into_wire(
3849 mut self,
3850 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3851 if self.github_token.is_some() && self.github_token_provider.is_some() {
3852 return Err(crate::Error::with_message(
3853 crate::ErrorKind::InvalidConfig,
3854 "github_token and github_token_provider are mutually exclusive",
3855 ));
3856 }
3857 let permission_active =
3858 self.permission_handler.is_some() || self.permission_policy.is_some();
3859 let request_user_input = self.user_input_handler.is_some();
3860 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3861 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3862 let request_elicitation = self.elicitation_handler.is_some();
3863 let hooks_flag = self.hooks_handler.is_some();
3864
3865 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3866 if let Some(tools) = self.tools.as_mut() {
3867 for tool in tools.iter_mut() {
3868 if let Some(handler) = tool.handler.take()
3869 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3870 {
3871 return Err(crate::Error::with_message(
3872 crate::ErrorKind::InvalidConfig,
3873 format!("duplicate tool handler registered for name {:?}", tool.name),
3874 ));
3875 }
3876 }
3877 }
3878
3879 let wire_commands = self.commands.as_ref().map(|cmds| {
3880 cmds.iter()
3881 .map(|c| crate::wire::CommandWireDefinition {
3882 name: c.name.clone(),
3883 description: c.description.clone().unwrap_or_default(),
3884 })
3885 .collect()
3886 });
3887 let wire_canvases = self.canvases.clone();
3888 let canvas_handler = self.canvas_handler.clone();
3889 let bearer_token_providers =
3890 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3891
3892 let wire = crate::wire::SessionResumeWire {
3893 session_id: self.session_id,
3894 model: self.model,
3895 allowed_models: self.allowed_models,
3896 client_name: self.client_name,
3897 reasoning_effort: self.reasoning_effort,
3898 reasoning_summary: self.reasoning_summary,
3899 context_tier: self.context_tier,
3900 streaming: self.streaming,
3901 system_message: self.system_message,
3902 ask_user_variant: self.ask_user_variant,
3903 tools: self.tools,
3904 canvases: wire_canvases,
3905 open_canvases: self.open_canvases,
3906 request_canvas_renderer: self.request_canvas_renderer,
3907 request_extensions: self.request_extensions,
3908 extension_sdk_path: self.extension_sdk_path,
3909 extension_info: self.extension_info,
3910 canvas_provider: self.canvas_provider,
3911 available_tools: self.available_tools,
3912 excluded_tools: self.excluded_tools,
3913 excluded_builtin_agents: self.excluded_builtin_agents,
3914 tool_filter_precedence: "excluded",
3915 mcp_servers: self.mcp_servers,
3916 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3917 auth_client_id_metadata_url: self.auth_client_id_metadata_url,
3918 embedding_cache_storage: self.embedding_cache_storage,
3919 env_value_mode: "direct",
3920 enable_config_discovery: self.enable_config_discovery,
3921 skip_embedding_retrieval: self.skip_embedding_retrieval,
3922 organization_custom_instructions: self.organization_custom_instructions,
3923 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3924 enable_file_hooks: self.enable_file_hooks,
3925 enable_host_git_operations: self.enable_host_git_operations,
3926 enable_session_store: self.enable_session_store,
3927 enable_skills: self.enable_skills,
3928 request_user_input,
3929 request_permission: permission_active,
3930 request_exit_plan_mode,
3931 request_auto_mode_switch,
3932 request_elicitation,
3933 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3934 github_mcp_tool_config: self.github_mcp_tool_config,
3935 hooks: hooks_flag,
3936 skill_directories: self.skill_directories,
3937 instruction_directories: self.instruction_directories,
3938 plugin_directories: self.plugin_directories,
3939 large_output: self.large_output,
3940 tool_search: self.tool_search,
3941 disabled_skills: self.disabled_skills,
3942 disabled_mcp_servers: self.disabled_mcp_servers,
3943 custom_agents: self.custom_agents,
3944 custom_agents_local_only: self.custom_agents_local_only,
3945 default_agent: self.default_agent,
3946 agent: self.agent,
3947 infinite_sessions: self.infinite_sessions,
3948 provider: self.provider,
3949 capi: self.capi,
3950 providers: self.providers,
3951 models: self.models,
3952 enable_session_telemetry: self.enable_session_telemetry,
3953 enable_citations: self.enable_citations,
3954 enable_file_change_tracking: self.enable_file_change_tracking,
3955 session_limits: self.session_limits,
3956 model_capabilities: self.model_capabilities,
3957 memory: self.memory,
3958 config_dir: self.config_directory,
3959 working_directory: self.working_directory,
3960 additional_directories: self.additional_directories,
3961 github_token: self.github_token,
3962 github_token_provider_registration_id: None,
3963 remote_session: self.remote_session,
3964 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3965 enable_github_telemetry_forwarding: None,
3966 commands: wire_commands,
3967 feature_flags: self.feature_flags,
3968 exp_assignments: self.exp_assignments,
3969 enable_managed_settings: self.enable_managed_settings,
3970 is_experimental_mode: self.enable_experimental_mode,
3971 managed_settings: self.managed_settings,
3972 suppress_resume_event: self.suppress_resume_event,
3973 continue_pending_work: self.continue_pending_work,
3974 };
3975
3976 let runtime = SessionConfigRuntime {
3977 permission_handler: self.permission_handler,
3978 permission_policy: self.permission_policy,
3979 elicitation_handler: self.elicitation_handler,
3980 mcp_auth_handler: self.mcp_auth_handler,
3981 user_input_handler: self.user_input_handler,
3982 exit_plan_mode_handler: self.exit_plan_mode_handler,
3983 auto_mode_switch_handler: self.auto_mode_switch_handler,
3984 hooks_handler: self.hooks_handler,
3985 system_message_transform: self.system_message_transform,
3986 tool_handlers,
3987 canvas_handler,
3988 session_fs_provider: self.session_fs_provider,
3989 bearer_token_providers,
3990 github_token_provider: self.github_token_provider,
3991 commands: self.commands,
3992 };
3993
3994 Ok((wire, runtime))
3995 }
3996
3997 pub fn new(session_id: SessionId) -> Self {
4002 Self {
4003 session_id,
4004 model: None,
4005 allowed_models: None,
4006 client_name: None,
4007 reasoning_effort: None,
4008 reasoning_summary: None,
4009 context_tier: None,
4010 streaming: None,
4011 system_message: None,
4012 ask_user_variant: None,
4013 tools: None,
4014 canvases: None,
4015 canvas_handler: None,
4016 open_canvases: None,
4017 request_canvas_renderer: None,
4018 request_extensions: None,
4019 extension_sdk_path: None,
4020 extension_info: None,
4021 canvas_provider: None,
4022 available_tools: None,
4023 excluded_tools: None,
4024 excluded_builtin_agents: None,
4025 included_builtin_skills: None,
4026 mcp_servers: None,
4027 mcp_oauth_token_storage: None,
4028 auth_client_id_metadata_url: None,
4029 enable_config_discovery: None,
4030 skip_embedding_retrieval: None,
4031 organization_custom_instructions: None,
4032 enable_on_demand_instruction_discovery: None,
4033 enable_file_hooks: None,
4034 enable_host_git_operations: None,
4035 enable_session_store: None,
4036 enable_skills: None,
4037 embedding_cache_storage: None,
4038 enable_mcp_apps: None,
4039 github_mcp_tool_config: None,
4040 skill_directories: None,
4041 instruction_directories: None,
4042 plugin_directories: None,
4043 large_output: None,
4044 tool_search: None,
4045 disabled_skills: None,
4046 disabled_mcp_servers: None,
4047 hooks: None,
4048 custom_agents: None,
4049 default_agent: None,
4050 agent: None,
4051 infinite_sessions: None,
4052 provider: None,
4053 capi: None,
4054 providers: None,
4055 models: None,
4056 enable_session_telemetry: None,
4057 enable_citations: None,
4058 enable_file_change_tracking: None,
4059 session_limits: None,
4060 model_capabilities: None,
4061 memory: None,
4062 config_directory: None,
4063 working_directory: None,
4064 additional_directories: None,
4065 github_token: None,
4066 github_token_provider: None,
4067 remote_session: None,
4068 include_sub_agent_streaming_events: None,
4069 commands: None,
4070 feature_flags: None,
4071 exp_assignments: None,
4072 enable_managed_settings: None,
4073 managed_settings: None,
4074 session_fs_provider: None,
4075 suppress_resume_event: None,
4076 continue_pending_work: None,
4077 permission_handler: None,
4078 elicitation_handler: None,
4079 mcp_auth_handler: None,
4080 user_input_handler: None,
4081 exit_plan_mode_handler: None,
4082 auto_mode_switch_handler: None,
4083 hooks_handler: None,
4084 permission_policy: None,
4085 system_message_transform: None,
4086 skip_custom_instructions: None,
4087 custom_agents_local_only: None,
4088 enable_experimental_mode: None,
4089 coauthor_enabled: None,
4090 manage_schedule_enabled: None,
4091 event_buffer_capacity: None,
4092 }
4093 }
4094
4095 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
4097 self.permission_handler = Some(handler);
4098 self
4099 }
4100
4101 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
4103 self.elicitation_handler = Some(handler);
4104 self
4105 }
4106
4107 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
4109 self.mcp_auth_handler = Some(handler);
4110 self
4111 }
4112
4113 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
4115 self.user_input_handler = Some(handler);
4116 self
4117 }
4118
4119 pub fn with_ask_user_variant(mut self, variant: AskUserVariant) -> Self {
4121 self.ask_user_variant = Some(variant);
4122 self
4123 }
4124
4125 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
4127 self.exit_plan_mode_handler = Some(handler);
4128 self
4129 }
4130
4131 pub fn with_auto_mode_switch_handler(
4133 mut self,
4134 handler: Arc<dyn AutoModeSwitchHandler>,
4135 ) -> Self {
4136 self.auto_mode_switch_handler = Some(handler);
4137 self
4138 }
4139
4140 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
4143 self.hooks_handler = Some(hooks);
4144 self
4145 }
4146
4147 pub fn with_system_message_transform(
4149 mut self,
4150 transform: Arc<dyn SystemMessageTransform>,
4151 ) -> Self {
4152 self.system_message_transform = Some(transform);
4153 self
4154 }
4155
4156 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
4160 self.commands = Some(commands);
4161 self
4162 }
4163
4164 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
4167 self.session_fs_provider = Some(provider);
4168 self
4169 }
4170
4171 pub fn approve_all_permissions(mut self) -> Self {
4174 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
4175 self
4176 }
4177
4178 pub fn deny_all_permissions(mut self) -> Self {
4181 self.permission_policy = Some(crate::permission::Policy::DenyAll);
4182 self
4183 }
4184
4185 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
4188 where
4189 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
4190 {
4191 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
4192 self
4193 }
4194
4195 pub fn with_model(mut self, model: impl Into<String>) -> Self {
4197 self.model = Some(model.into());
4198 self
4199 }
4200
4201 pub fn with_allowed_models<I, S>(mut self, models: I) -> Self
4206 where
4207 I: IntoIterator<Item = S>,
4208 S: Into<String>,
4209 {
4210 self.allowed_models = Some(models.into_iter().map(Into::into).collect());
4211 self
4212 }
4213
4214 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
4216 self.client_name = Some(name.into());
4217 self
4218 }
4219
4220 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4222 self.reasoning_effort = Some(effort.into());
4223 self
4224 }
4225
4226 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4228 self.reasoning_summary = Some(summary);
4229 self
4230 }
4231
4232 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
4235 self.context_tier = Some(tier.into());
4236 self
4237 }
4238
4239 pub fn with_streaming(mut self, streaming: bool) -> Self {
4241 self.streaming = Some(streaming);
4242 self
4243 }
4244
4245 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
4248 self.system_message = Some(system_message);
4249 self
4250 }
4251
4252 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
4254 self.tools = Some(tools.into_iter().collect());
4255 self
4256 }
4257
4258 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
4260 self.canvases = Some(canvases.into_iter().collect());
4261 self
4262 }
4263
4264 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
4266 self.canvas_handler = Some(handler);
4267 self
4268 }
4269
4270 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
4272 mut self,
4273 open_canvases: I,
4274 ) -> Self {
4275 self.open_canvases = Some(open_canvases.into_iter().collect());
4276 self
4277 }
4278
4279 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
4281 self.request_canvas_renderer = Some(request);
4282 self
4283 }
4284
4285 pub fn with_request_extensions(mut self, request: bool) -> Self {
4287 self.request_extensions = Some(request);
4288 self
4289 }
4290
4291 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
4295 self.extension_sdk_path = Some(path.into());
4296 self
4297 }
4298
4299 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
4301 self.extension_info = Some(extension_info);
4302 self
4303 }
4304
4305 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
4308 self.canvas_provider = Some(canvas_provider);
4309 self
4310 }
4311
4312 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
4314 where
4315 I: IntoIterator<Item = S>,
4316 S: Into<String>,
4317 {
4318 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
4319 self
4320 }
4321
4322 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
4324 where
4325 I: IntoIterator<Item = S>,
4326 S: Into<String>,
4327 {
4328 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
4329 self
4330 }
4331
4332 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
4334 where
4335 I: IntoIterator<Item = S>,
4336 S: Into<String>,
4337 {
4338 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
4339 self
4340 }
4341
4342 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
4344 self.mcp_servers = Some(servers);
4345 self
4346 }
4347
4348 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
4351 self.mcp_oauth_token_storage = Some(mode.into());
4352 self
4353 }
4354
4355 pub fn with_auth_client_id_metadata_url(mut self, url: impl Into<String>) -> Self {
4357 self.auth_client_id_metadata_url = Some(url.into());
4358 self
4359 }
4360
4361 pub fn with_embedding_cache_storage(
4363 mut self,
4364 embedding_cache_storage: impl Into<String>,
4365 ) -> Self {
4366 self.embedding_cache_storage = Some(embedding_cache_storage.into());
4367 self
4368 }
4369
4370 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
4373 self.enable_config_discovery = Some(enable);
4374 self
4375 }
4376
4377 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
4379 self.skip_embedding_retrieval = Some(value);
4380 self
4381 }
4382
4383 pub fn with_organization_custom_instructions(
4385 mut self,
4386 instructions: impl Into<String>,
4387 ) -> Self {
4388 self.organization_custom_instructions = Some(instructions.into());
4389 self
4390 }
4391
4392 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
4394 self.enable_on_demand_instruction_discovery = Some(value);
4395 self
4396 }
4397
4398 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
4400 self.enable_file_hooks = Some(value);
4401 self
4402 }
4403
4404 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
4406 self.enable_host_git_operations = Some(value);
4407 self
4408 }
4409
4410 pub fn with_enable_session_store(mut self, value: bool) -> Self {
4412 self.enable_session_store = Some(value);
4413 self
4414 }
4415
4416 pub fn with_enable_skills(mut self, value: bool) -> Self {
4418 self.enable_skills = Some(value);
4419 self
4420 }
4421
4422 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
4428 self.enable_mcp_apps = Some(enable);
4429 self
4430 }
4431
4432 pub fn with_github_mcp_tool_config(mut self, config: GitHubMcpToolConfig) -> Self {
4434 self.github_mcp_tool_config = Some(config);
4435 self
4436 }
4437
4438 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
4440 where
4441 I: IntoIterator<Item = P>,
4442 P: Into<PathBuf>,
4443 {
4444 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
4445 self
4446 }
4447
4448 pub fn with_included_builtin_skills<I, S>(mut self, names: I) -> Self
4450 where
4451 I: IntoIterator<Item = S>,
4452 S: Into<String>,
4453 {
4454 self.included_builtin_skills = Some(names.into_iter().map(Into::into).collect());
4455 self
4456 }
4457
4458 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
4462 where
4463 I: IntoIterator<Item = P>,
4464 P: Into<PathBuf>,
4465 {
4466 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
4467 self
4468 }
4469
4470 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
4472 where
4473 I: IntoIterator<Item = P>,
4474 P: Into<PathBuf>,
4475 {
4476 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
4477 self
4478 }
4479
4480 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
4482 self.large_output = Some(config);
4483 self
4484 }
4485
4486 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
4489 self.tool_search = Some(config);
4490 self
4491 }
4492
4493 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
4495 where
4496 I: IntoIterator<Item = S>,
4497 S: Into<String>,
4498 {
4499 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
4500 self
4501 }
4502
4503 pub fn with_disabled_mcp_servers<I, S>(mut self, names: I) -> Self
4505 where
4506 I: IntoIterator<Item = S>,
4507 S: Into<String>,
4508 {
4509 self.disabled_mcp_servers = Some(names.into_iter().map(Into::into).collect());
4510 self
4511 }
4512
4513 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
4515 mut self,
4516 agents: I,
4517 ) -> Self {
4518 self.custom_agents = Some(agents.into_iter().collect());
4519 self
4520 }
4521
4522 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
4524 self.default_agent = Some(agent);
4525 self
4526 }
4527
4528 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
4530 self.agent = Some(name.into());
4531 self
4532 }
4533
4534 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
4536 self.infinite_sessions = Some(config);
4537 self
4538 }
4539
4540 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
4542 self.provider = Some(provider);
4543 self
4544 }
4545
4546 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
4548 self.capi = Some(capi);
4549 self
4550 }
4551
4552 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
4558 self.providers = Some(providers);
4559 self
4560 }
4561
4562 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
4568 self.models = Some(models);
4569 self
4570 }
4571
4572 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
4576 self.enable_session_telemetry = Some(enable);
4577 self
4578 }
4579
4580 pub fn with_enable_citations(mut self, enable: bool) -> Self {
4582 self.enable_citations = Some(enable);
4583 self
4584 }
4585
4586 pub fn with_enable_file_change_tracking(mut self, enable: bool) -> Self {
4589 self.enable_file_change_tracking = Some(enable);
4590 self
4591 }
4592
4593 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
4595 self.session_limits = Some(limits);
4596 self
4597 }
4598
4599 pub fn with_model_capabilities(
4601 mut self,
4602 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
4603 ) -> Self {
4604 self.model_capabilities = Some(capabilities);
4605 self
4606 }
4607
4608 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
4610 self.memory = Some(memory);
4611 self
4612 }
4613
4614 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4616 self.config_directory = Some(dir.into());
4617 self
4618 }
4619
4620 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
4622 self.working_directory = Some(dir.into());
4623 self
4624 }
4625
4626 pub fn with_additional_directories<I, P>(mut self, paths: I) -> Self
4628 where
4629 I: IntoIterator<Item = P>,
4630 P: Into<PathBuf>,
4631 {
4632 self.additional_directories = Some(paths.into_iter().map(Into::into).collect());
4633 self
4634 }
4635
4636 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
4640 self.github_token = Some(token.into());
4641 self
4642 }
4643
4644 pub fn with_github_token_provider(mut self, provider: Arc<dyn GitHubTokenProvider>) -> Self {
4650 self.github_token_provider = Some(provider);
4651 self
4652 }
4653
4654 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
4656 self.include_sub_agent_streaming_events = Some(include);
4657 self
4658 }
4659
4660 pub fn with_remote_session(
4662 mut self,
4663 mode: crate::generated::api_types::RemoteSessionMode,
4664 ) -> Self {
4665 self.remote_session = Some(mode);
4666 self
4667 }
4668
4669 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
4672 self.suppress_resume_event = Some(suppress);
4673 self
4674 }
4675
4676 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
4682 self.continue_pending_work = Some(continue_pending);
4683 self
4684 }
4685
4686 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
4688 self.skip_custom_instructions = Some(value);
4689 self
4690 }
4691
4692 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
4694 self.custom_agents_local_only = Some(value);
4695 self
4696 }
4697
4698 pub fn with_enable_experimental_mode(mut self, enable_experimental_mode: bool) -> Self {
4700 self.enable_experimental_mode = Some(enable_experimental_mode);
4701 self
4702 }
4703
4704 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
4706 self.coauthor_enabled = Some(value);
4707 self
4708 }
4709
4710 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
4712 self.manage_schedule_enabled = Some(value);
4713 self
4714 }
4715
4716 pub fn with_feature_flags(mut self, feature_flags: HashMap<String, bool>) -> Self {
4718 self.feature_flags = Some(feature_flags);
4719 self
4720 }
4721
4722 pub fn with_event_buffer_capacity(mut self, capacity: usize) -> Self {
4730 self.event_buffer_capacity = Some(capacity);
4731 self
4732 }
4733
4734 #[doc(hidden)]
4738 pub fn with_exp_assignments(mut self, assignments: CopilotExpAssignmentResponse) -> Self {
4739 self.exp_assignments = Some(assignments);
4740 self
4741 }
4742
4743 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
4746 self.enable_managed_settings = Some(enabled);
4747 self
4748 }
4749
4750 pub fn with_managed_settings(mut self, managed_settings: ManagedSettings) -> Self {
4754 self.managed_settings = Some(managed_settings);
4755 self
4756 }
4757}
4758
4759#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4765#[serde(rename_all = "camelCase")]
4766#[non_exhaustive]
4767pub struct SystemMessageConfig {
4768 #[serde(skip_serializing_if = "Option::is_none")]
4770 pub mode: Option<String>,
4771 #[serde(skip_serializing_if = "Option::is_none")]
4773 pub content: Option<String>,
4774 #[serde(skip_serializing_if = "Option::is_none")]
4776 pub sections: Option<HashMap<String, SectionOverride>>,
4777}
4778
4779impl SystemMessageConfig {
4780 pub fn new() -> Self {
4783 Self::default()
4784 }
4785
4786 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4789 self.mode = Some(mode.into());
4790 self
4791 }
4792
4793 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4796 self.content = Some(content.into());
4797 self
4798 }
4799
4800 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4802 self.sections = Some(sections);
4803 self
4804 }
4805}
4806
4807#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4813#[serde(rename_all = "camelCase")]
4814pub struct SectionOverride {
4815 #[serde(skip_serializing_if = "Option::is_none")]
4818 pub action: Option<String>,
4819 #[serde(skip_serializing_if = "Option::is_none")]
4821 pub content: Option<String>,
4822}
4823
4824#[derive(Debug, Clone, Serialize, Deserialize)]
4826#[serde(rename_all = "camelCase")]
4827pub struct CreateSessionResult {
4828 pub session_id: SessionId,
4830 #[serde(skip_serializing_if = "Option::is_none")]
4832 pub workspace_path: Option<PathBuf>,
4833 #[serde(default, alias = "remote_url")]
4835 pub remote_url: Option<String>,
4836 #[serde(skip_serializing_if = "Option::is_none")]
4838 pub capabilities: Option<SessionCapabilities>,
4839}
4840
4841#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4843#[serde(rename_all = "camelCase")]
4844pub(crate) struct ResumeSessionResult {
4845 #[serde(default)]
4847 pub session_id: Option<SessionId>,
4848 #[serde(default, skip_serializing_if = "Option::is_none")]
4850 pub workspace_path: Option<PathBuf>,
4851 #[serde(default, alias = "remote_url")]
4853 pub remote_url: Option<String>,
4854 #[serde(default, skip_serializing_if = "Option::is_none")]
4856 pub capabilities: Option<SessionCapabilities>,
4857 #[serde(
4859 default,
4860 alias = "openCanvasInstances",
4861 skip_serializing_if = "Option::is_none"
4862 )]
4863 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4864}
4865
4866#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4868#[serde(rename_all = "lowercase")]
4869pub enum LogLevel {
4870 #[default]
4872 Info,
4873 Warning,
4875 Error,
4877}
4878
4879#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4884#[serde(rename_all = "camelCase")]
4885pub struct LogOptions {
4886 #[serde(skip_serializing_if = "Option::is_none")]
4888 pub level: Option<LogLevel>,
4889 #[serde(skip_serializing_if = "Option::is_none")]
4892 pub ephemeral: Option<bool>,
4893}
4894
4895impl LogOptions {
4896 pub fn with_level(mut self, level: LogLevel) -> Self {
4898 self.level = Some(level);
4899 self
4900 }
4901
4902 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4904 self.ephemeral = Some(ephemeral);
4905 self
4906 }
4907}
4908
4909#[derive(Debug, Clone, Default)]
4913pub struct SetModelOptions {
4914 pub reasoning_effort: Option<String>,
4917 pub reasoning_summary: Option<ReasoningSummary>,
4921 pub context_tier: Option<ContextTier>,
4924 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4928 pub auto_tier: Option<AutoTierPreference>,
4936}
4937
4938#[derive(Debug, Clone, PartialEq, Eq)]
4947pub enum AutoTierPreference {
4948 Tier(AutoTier),
4950 Reset,
4952}
4953
4954impl SetModelOptions {
4955 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4957 self.reasoning_effort = Some(effort.into());
4958 self
4959 }
4960
4961 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4963 self.reasoning_summary = Some(summary);
4964 self
4965 }
4966
4967 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4969 self.context_tier = Some(tier);
4970 self
4971 }
4972
4973 pub fn with_model_capabilities(
4975 mut self,
4976 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4977 ) -> Self {
4978 self.model_capabilities = Some(caps);
4979 self
4980 }
4981
4982 pub fn with_auto_tier(mut self, tier: AutoTier) -> Self {
4984 self.auto_tier = Some(AutoTierPreference::Tier(tier));
4985 self
4986 }
4987
4988 pub fn with_reset_auto_tier(mut self) -> Self {
4991 self.auto_tier = Some(AutoTierPreference::Reset);
4992 self
4993 }
4994}
4995
4996#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
5003#[serde(rename_all = "camelCase")]
5004pub struct PingResponse {
5005 #[serde(default)]
5007 pub message: String,
5008 #[serde(default)]
5010 pub timestamp: String,
5011 #[serde(skip_serializing_if = "Option::is_none")]
5013 pub protocol_version: Option<u32>,
5014}
5015
5016#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5018#[serde(rename_all = "camelCase")]
5019pub struct AttachmentLineRange {
5020 pub start: u32,
5022 pub end: u32,
5024}
5025
5026#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5028#[serde(rename_all = "camelCase")]
5029pub struct AttachmentSelectionPosition {
5030 pub line: u32,
5032 pub character: u32,
5034}
5035
5036#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5038#[serde(rename_all = "camelCase")]
5039pub struct AttachmentSelectionRange {
5040 pub start: AttachmentSelectionPosition,
5042 pub end: AttachmentSelectionPosition,
5044}
5045
5046#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
5048#[serde(rename_all = "snake_case")]
5049#[non_exhaustive]
5050pub enum GitHubReferenceType {
5051 Issue,
5053 Pr,
5055 Discussion,
5057}
5058
5059#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5065#[serde(rename_all = "camelCase")]
5066pub struct GitHubRepoPointer {
5067 #[serde(skip_serializing_if = "Option::is_none")]
5069 pub id: Option<i64>,
5070 pub name: String,
5072 pub owner: String,
5074}
5075
5076#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5078#[serde(rename_all = "camelCase")]
5079pub struct GitHubFileDiffSide {
5080 pub path: String,
5082 pub r#ref: String,
5084 pub repo: GitHubRepoPointer,
5086}
5087
5088#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5090#[serde(rename_all = "camelCase")]
5091pub struct GitHubTreeComparisonSide {
5092 pub repo: GitHubRepoPointer,
5094 pub revision: String,
5096}
5097
5098#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5100#[serde(rename_all = "camelCase")]
5101pub struct GitHubSnippetLineRange {
5102 pub start: i64,
5104 pub end: i64,
5106}
5107
5108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5110#[serde(
5111 tag = "type",
5112 rename_all = "camelCase",
5113 rename_all_fields = "camelCase"
5114)]
5115#[non_exhaustive]
5116pub enum Attachment {
5117 File {
5119 path: PathBuf,
5121 #[serde(skip_serializing_if = "Option::is_none")]
5123 display_name: Option<String>,
5124 #[serde(skip_serializing_if = "Option::is_none")]
5126 line_range: Option<AttachmentLineRange>,
5127 },
5128 Directory {
5130 path: PathBuf,
5132 #[serde(skip_serializing_if = "Option::is_none")]
5134 display_name: Option<String>,
5135 },
5136 Selection {
5138 file_path: PathBuf,
5140 text: String,
5142 #[serde(skip_serializing_if = "Option::is_none")]
5144 display_name: Option<String>,
5145 selection: AttachmentSelectionRange,
5147 },
5148 Blob {
5150 data: String,
5152 mime_type: String,
5154 #[serde(skip_serializing_if = "Option::is_none")]
5156 display_name: Option<String>,
5157 },
5158 #[serde(rename = "github_reference")]
5160 GitHubReference {
5161 number: u64,
5163 title: String,
5165 reference_type: GitHubReferenceType,
5167 state: String,
5169 url: String,
5171 },
5172 #[serde(rename = "github_commit")]
5174 GitHubCommit {
5175 message: String,
5177 oid: String,
5179 repo: GitHubRepoPointer,
5181 url: String,
5183 },
5184 #[serde(rename = "github_release")]
5186 GitHubRelease {
5187 name: String,
5189 repo: GitHubRepoPointer,
5191 tag_name: String,
5193 url: String,
5195 },
5196 #[serde(rename = "github_actions_job")]
5198 GitHubActionsJob {
5199 #[serde(skip_serializing_if = "Option::is_none")]
5202 conclusion: Option<String>,
5203 job_id: i64,
5205 job_name: String,
5207 repo: GitHubRepoPointer,
5209 url: String,
5211 workflow_name: String,
5213 },
5214 #[serde(rename = "github_repository")]
5216 GitHubRepository {
5217 #[serde(skip_serializing_if = "Option::is_none")]
5219 description: Option<String>,
5220 #[serde(skip_serializing_if = "Option::is_none")]
5223 r#ref: Option<String>,
5224 repo: GitHubRepoPointer,
5226 url: String,
5228 },
5229 #[serde(rename = "github_file_diff")]
5231 GitHubFileDiff {
5232 #[serde(skip_serializing_if = "Option::is_none")]
5234 base: Option<GitHubFileDiffSide>,
5235 #[serde(skip_serializing_if = "Option::is_none")]
5237 head: Option<GitHubFileDiffSide>,
5238 url: String,
5240 },
5241 #[serde(rename = "github_tree_comparison")]
5243 GitHubTreeComparison {
5244 base: GitHubTreeComparisonSide,
5246 head: GitHubTreeComparisonSide,
5248 url: String,
5250 },
5251 #[serde(rename = "github_url")]
5253 GitHubUrl {
5254 url: String,
5256 },
5257 #[serde(rename = "github_file")]
5259 GitHubFile {
5260 path: String,
5262 r#ref: String,
5264 repo: GitHubRepoPointer,
5266 url: String,
5268 },
5269 #[serde(rename = "github_snippet")]
5271 GitHubSnippet {
5272 line_range: GitHubSnippetLineRange,
5274 path: String,
5276 r#ref: String,
5278 repo: GitHubRepoPointer,
5280 url: String,
5282 },
5283}
5284
5285impl Attachment {
5286 pub fn display_name(&self) -> Option<&str> {
5288 match self {
5289 Self::File { display_name, .. }
5290 | Self::Directory { display_name, .. }
5291 | Self::Selection { display_name, .. }
5292 | Self::Blob { display_name, .. } => display_name.as_deref(),
5293 Self::GitHubReference { .. }
5294 | Self::GitHubCommit { .. }
5295 | Self::GitHubRelease { .. }
5296 | Self::GitHubActionsJob { .. }
5297 | Self::GitHubRepository { .. }
5298 | Self::GitHubFileDiff { .. }
5299 | Self::GitHubTreeComparison { .. }
5300 | Self::GitHubUrl { .. }
5301 | Self::GitHubFile { .. }
5302 | Self::GitHubSnippet { .. } => None,
5303 }
5304 }
5305
5306 pub fn label(&self) -> Option<String> {
5308 if let Some(display_name) = self
5309 .display_name()
5310 .map(str::trim)
5311 .filter(|name| !name.is_empty())
5312 {
5313 return Some(display_name.to_string());
5314 }
5315
5316 match self {
5317 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
5318 format!("#{}", number)
5319 } else {
5320 title.trim().to_string()
5321 }),
5322 _ => self.derived_display_name(),
5323 }
5324 }
5325
5326 pub fn ensure_display_name(&mut self) {
5328 if self
5329 .display_name()
5330 .map(str::trim)
5331 .is_some_and(|name| !name.is_empty())
5332 {
5333 return;
5334 }
5335
5336 let Some(derived_display_name) = self.derived_display_name() else {
5337 return;
5338 };
5339
5340 match self {
5341 Self::File { display_name, .. }
5342 | Self::Directory { display_name, .. }
5343 | Self::Selection { display_name, .. }
5344 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
5345 Self::GitHubReference { .. }
5346 | Self::GitHubCommit { .. }
5347 | Self::GitHubRelease { .. }
5348 | Self::GitHubActionsJob { .. }
5349 | Self::GitHubRepository { .. }
5350 | Self::GitHubFileDiff { .. }
5351 | Self::GitHubTreeComparison { .. }
5352 | Self::GitHubUrl { .. }
5353 | Self::GitHubFile { .. }
5354 | Self::GitHubSnippet { .. } => {}
5355 }
5356 }
5357
5358 fn derived_display_name(&self) -> Option<String> {
5359 match self {
5360 Self::File { path, .. } | Self::Directory { path, .. } => {
5361 Some(attachment_name_from_path(path))
5362 }
5363 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
5364 Self::Blob { .. } => Some("attachment".to_string()),
5365 Self::GitHubReference { .. }
5366 | Self::GitHubCommit { .. }
5367 | Self::GitHubRelease { .. }
5368 | Self::GitHubActionsJob { .. }
5369 | Self::GitHubRepository { .. }
5370 | Self::GitHubFileDiff { .. }
5371 | Self::GitHubTreeComparison { .. }
5372 | Self::GitHubUrl { .. }
5373 | Self::GitHubFile { .. }
5374 | Self::GitHubSnippet { .. } => None,
5375 }
5376 }
5377}
5378
5379fn attachment_name_from_path(path: &Path) -> String {
5380 path.file_name()
5381 .map(|name| name.to_string_lossy().into_owned())
5382 .filter(|name| !name.is_empty())
5383 .unwrap_or_else(|| {
5384 let full = path.to_string_lossy();
5385 if full.is_empty() {
5386 "attachment".to_string()
5387 } else {
5388 full.into_owned()
5389 }
5390 })
5391}
5392
5393pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
5395 for attachment in attachments {
5396 attachment.ensure_display_name();
5397 }
5398}
5399
5400#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5405#[non_exhaustive]
5406pub enum MessageSource {
5407 User,
5409 System,
5411 Agent(String),
5413}
5414
5415impl std::fmt::Display for MessageSource {
5416 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5417 match self {
5418 Self::User => f.write_str("user"),
5419 Self::System => f.write_str("system"),
5420 Self::Agent(id) => write!(f, "agent-{id}"),
5421 }
5422 }
5423}
5424
5425impl Serialize for MessageSource {
5426 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
5427 serializer.collect_str(self)
5428 }
5429}
5430
5431impl<'de> Deserialize<'de> for MessageSource {
5432 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
5433 let value = String::deserialize(deserializer)?;
5434 match value.as_str() {
5435 "user" => Ok(Self::User),
5436 "system" => Ok(Self::System),
5437 value => value
5438 .strip_prefix("agent-")
5439 .map(|id| Self::Agent(id.to_owned()))
5440 .ok_or_else(|| serde::de::Error::custom("expected user, system, or agent-<id>")),
5441 }
5442 }
5443}
5444
5445#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5450#[serde(rename_all = "lowercase")]
5451#[non_exhaustive]
5452pub enum DeliveryMode {
5453 Enqueue,
5455 Immediate,
5457}
5458
5459#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5464#[serde(rename_all = "lowercase")]
5465#[non_exhaustive]
5466pub enum AgentMode {
5467 Interactive,
5469 Plan,
5471 Autopilot,
5473 Shell,
5475}
5476
5477#[derive(Debug, Clone)]
5506#[non_exhaustive]
5507pub struct MessageOptions {
5508 pub prompt: String,
5510 pub source: Option<MessageSource>,
5513 pub mode: Option<DeliveryMode>,
5519 pub agent_mode: Option<AgentMode>,
5523 pub attachments: Option<Vec<Attachment>>,
5525 pub wait_timeout: Option<Duration>,
5528 pub request_headers: Option<HashMap<String, String>>,
5532 pub traceparent: Option<String>,
5539 pub tracestate: Option<String>,
5543 pub display_prompt: Option<String>,
5545}
5546
5547impl MessageOptions {
5548 pub fn new(prompt: impl Into<String>) -> Self {
5550 Self {
5551 prompt: prompt.into(),
5552 source: None,
5553 mode: None,
5554 agent_mode: None,
5555 attachments: None,
5556 wait_timeout: None,
5557 request_headers: None,
5558 traceparent: None,
5559 tracestate: None,
5560 display_prompt: None,
5561 }
5562 }
5563
5564 pub fn with_source(mut self, source: MessageSource) -> Self {
5566 self.source = Some(source);
5567 self
5568 }
5569
5570 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
5576 self.mode = Some(mode);
5577 self
5578 }
5579
5580 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
5584 self.agent_mode = Some(agent_mode);
5585 self
5586 }
5587
5588 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
5590 self.attachments = Some(attachments);
5591 self
5592 }
5593
5594 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
5596 self.wait_timeout = Some(timeout);
5597 self
5598 }
5599
5600 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
5602 self.request_headers = Some(headers);
5603 self
5604 }
5605
5606 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
5611 self.traceparent = ctx.traceparent;
5612 self.tracestate = ctx.tracestate;
5613 self
5614 }
5615
5616 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
5618 self.traceparent = Some(traceparent.into());
5619 self
5620 }
5621
5622 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
5624 self.tracestate = Some(tracestate.into());
5625 self
5626 }
5627
5628 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
5630 self.display_prompt = Some(display_prompt.into());
5631 self
5632 }
5633}
5634
5635impl From<&str> for MessageOptions {
5636 fn from(prompt: &str) -> Self {
5637 Self::new(prompt)
5638 }
5639}
5640
5641impl From<String> for MessageOptions {
5642 fn from(prompt: String) -> Self {
5643 Self::new(prompt)
5644 }
5645}
5646
5647impl From<&String> for MessageOptions {
5648 fn from(prompt: &String) -> Self {
5649 Self::new(prompt.clone())
5650 }
5651}
5652
5653#[derive(Debug, Clone, Serialize, Deserialize)]
5655#[serde(rename_all = "camelCase")]
5656#[non_exhaustive]
5657pub struct GetStatusResponse {
5658 pub version: String,
5660 pub protocol_version: u32,
5662}
5663
5664#[derive(Debug, Clone, Serialize, Deserialize)]
5666#[serde(rename_all = "camelCase")]
5667#[non_exhaustive]
5668pub struct GetAuthStatusResponse {
5669 pub is_authenticated: bool,
5671 #[serde(skip_serializing_if = "Option::is_none")]
5674 pub auth_type: Option<String>,
5675 #[serde(skip_serializing_if = "Option::is_none")]
5677 pub host: Option<String>,
5678 #[serde(skip_serializing_if = "Option::is_none")]
5680 pub login: Option<String>,
5681 #[serde(skip_serializing_if = "Option::is_none")]
5683 pub status_message: Option<String>,
5684}
5685
5686#[derive(Debug, Clone, Serialize, Deserialize)]
5690#[serde(rename_all = "camelCase")]
5691pub struct SessionEventNotification {
5692 pub session_id: SessionId,
5694 pub event: SessionEvent,
5696}
5697
5698#[derive(Debug, Clone, Serialize, Deserialize)]
5705#[serde(rename_all = "camelCase")]
5706pub struct SessionEvent {
5707 pub id: String,
5709 pub timestamp: String,
5711 pub parent_id: Option<String>,
5713 #[serde(skip_serializing_if = "Option::is_none")]
5715 pub ephemeral: Option<bool>,
5716 #[serde(skip_serializing_if = "Option::is_none")]
5719 pub agent_id: Option<String>,
5720 #[serde(skip_serializing_if = "Option::is_none")]
5722 pub debug_cli_received_at_ms: Option<i64>,
5723 #[serde(skip_serializing_if = "Option::is_none")]
5725 pub debug_ws_forwarded_at_ms: Option<i64>,
5726 #[serde(rename = "type")]
5728 pub event_type: String,
5729 pub data: Value,
5731}
5732
5733impl SessionEvent {
5734 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
5739 use serde::de::IntoDeserializer;
5740 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
5741 self.event_type.as_str().into_deserializer();
5742 crate::generated::SessionEventType::deserialize(deserializer)
5743 .unwrap_or(crate::generated::SessionEventType::Unknown)
5744 }
5745
5746 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
5752 serde_json::from_value(self.data.clone()).ok()
5753 }
5754
5755 pub fn is_transient_error(&self) -> bool {
5759 self.event_type == "session.error"
5760 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
5761 }
5762}
5763
5764#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5769#[serde(rename_all = "camelCase")]
5770#[non_exhaustive]
5771pub struct ToolInvocation {
5772 pub session_id: SessionId,
5774 pub tool_call_id: String,
5776 pub tool_name: String,
5778 pub arguments: Value,
5780 #[serde(skip)]
5788 pub available_tools: Option<Vec<CurrentToolMetadata>>,
5789 #[serde(default, skip_serializing_if = "Option::is_none")]
5794 pub traceparent: Option<String>,
5795 #[serde(default, skip_serializing_if = "Option::is_none")]
5798 pub tracestate: Option<String>,
5799}
5800
5801impl ToolInvocation {
5802 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
5823 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
5824 }
5825
5826 pub fn trace_context(&self) -> TraceContext {
5829 TraceContext {
5830 traceparent: self.traceparent.clone(),
5831 tracestate: self.tracestate.clone(),
5832 }
5833 }
5834}
5835
5836#[derive(Debug, Clone, Serialize, Deserialize)]
5838#[serde(rename_all = "camelCase")]
5839pub struct ToolBinaryResult {
5840 pub data: String,
5842 pub mime_type: String,
5844 pub r#type: String,
5846 #[serde(default, skip_serializing_if = "Option::is_none")]
5848 pub description: Option<String>,
5849}
5850
5851#[derive(Debug, Clone, Serialize, Deserialize)]
5858#[serde(rename_all = "camelCase")]
5859#[non_exhaustive]
5860pub struct ToolResultExpanded {
5861 pub text_result_for_llm: String,
5863 pub result_type: String,
5865 #[serde(default, skip_serializing_if = "Option::is_none")]
5867 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5868 #[serde(skip_serializing_if = "Option::is_none")]
5870 pub session_log: Option<String>,
5871 #[serde(skip_serializing_if = "Option::is_none")]
5873 pub error: Option<String>,
5874 #[serde(default, skip_serializing_if = "Option::is_none")]
5876 pub tool_telemetry: Option<HashMap<String, Value>>,
5877 #[serde(default, skip_serializing_if = "Option::is_none")]
5879 pub tool_references: Option<Vec<String>>,
5880}
5881
5882impl ToolResultExpanded {
5883 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5887 Self {
5888 text_result_for_llm: text_result_for_llm.into(),
5889 result_type: result_type.into(),
5890 binary_results_for_llm: None,
5891 session_log: None,
5892 error: None,
5893 tool_telemetry: None,
5894 tool_references: None,
5895 }
5896 }
5897
5898 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5900 self.binary_results_for_llm = Some(results);
5901 self
5902 }
5903
5904 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5906 self.session_log = Some(session_log.into());
5907 self
5908 }
5909
5910 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5912 self.error = Some(error.into());
5913 self
5914 }
5915
5916 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5918 self.tool_telemetry = Some(telemetry);
5919 self
5920 }
5921
5922 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5924 where
5925 I: IntoIterator<Item = S>,
5926 S: Into<String>,
5927 {
5928 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5929 self
5930 }
5931}
5932
5933#[derive(Debug, Clone, Serialize, Deserialize)]
5935#[serde(untagged)]
5936#[non_exhaustive]
5937pub enum ToolResult {
5938 Text(String),
5940 Expanded(ToolResultExpanded),
5942}
5943
5944#[derive(Debug, Clone, Serialize, Deserialize)]
5946#[serde(rename_all = "camelCase")]
5947pub struct ToolResultResponse {
5948 pub result: ToolResult,
5950}
5951
5952#[derive(Debug, Clone, Serialize, Deserialize)]
5954#[serde(rename_all = "camelCase")]
5955pub struct SessionMetadata {
5956 pub session_id: SessionId,
5958 pub start_time: String,
5960 pub modified_time: String,
5962 #[serde(skip_serializing_if = "Option::is_none")]
5964 pub summary: Option<String>,
5965 pub is_remote: bool,
5967}
5968
5969#[derive(Debug, Clone, Serialize, Deserialize)]
5971#[serde(rename_all = "camelCase")]
5972pub struct ListSessionsResponse {
5973 pub sessions: Vec<SessionMetadata>,
5975}
5976
5977#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5981#[serde(rename_all = "camelCase")]
5982pub struct SessionListFilter {
5983 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5985 pub working_directory: Option<String>,
5986 #[serde(default, skip_serializing_if = "Option::is_none")]
5988 pub git_root: Option<String>,
5989 #[serde(default, skip_serializing_if = "Option::is_none")]
5991 pub repository: Option<String>,
5992 #[serde(default, skip_serializing_if = "Option::is_none")]
5994 pub branch: Option<String>,
5995}
5996
5997#[derive(Debug, Clone, Serialize, Deserialize)]
5999#[serde(rename_all = "camelCase")]
6000pub struct GetSessionMetadataResponse {
6001 #[serde(skip_serializing_if = "Option::is_none")]
6003 pub session: Option<SessionMetadata>,
6004}
6005
6006#[derive(Debug, Clone, Serialize, Deserialize)]
6008#[serde(rename_all = "camelCase")]
6009pub struct GetLastSessionIdResponse {
6010 #[serde(skip_serializing_if = "Option::is_none")]
6012 pub session_id: Option<SessionId>,
6013}
6014
6015#[derive(Debug, Clone, Serialize, Deserialize)]
6017#[serde(rename_all = "camelCase")]
6018pub struct GetForegroundSessionResponse {
6019 #[serde(skip_serializing_if = "Option::is_none")]
6021 pub session_id: Option<SessionId>,
6022}
6023
6024#[derive(Debug, Clone, Serialize, Deserialize)]
6026#[serde(rename_all = "camelCase")]
6027pub struct GetMessagesResponse {
6028 pub events: Vec<SessionEvent>,
6030}
6031
6032#[derive(Debug, Clone, Serialize, Deserialize)]
6034#[serde(rename_all = "camelCase")]
6035pub struct ElicitationResult {
6036 pub action: String,
6038 #[serde(skip_serializing_if = "Option::is_none")]
6040 pub content: Option<Value>,
6041}
6042
6043#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6049#[serde(rename_all = "camelCase")]
6050#[non_exhaustive]
6051pub enum ElicitationMode {
6052 Form,
6054 Url,
6056 #[serde(other)]
6058 Unknown,
6059}
6060
6061#[derive(Debug, Clone, Serialize, Deserialize)]
6068#[serde(rename_all = "camelCase")]
6069pub struct ElicitationRequest {
6070 pub message: String,
6072 #[serde(skip_serializing_if = "Option::is_none")]
6074 pub requested_schema: Option<Value>,
6075 #[serde(skip_serializing_if = "Option::is_none")]
6077 pub mode: Option<ElicitationMode>,
6078 #[serde(skip_serializing_if = "Option::is_none")]
6080 pub elicitation_source: Option<String>,
6081 #[serde(skip_serializing_if = "Option::is_none")]
6083 pub url: Option<String>,
6084}
6085
6086#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6091#[serde(rename_all = "camelCase")]
6092pub struct SessionCapabilities {
6093 #[serde(skip_serializing_if = "Option::is_none")]
6095 pub ui: Option<UiCapabilities>,
6096}
6097
6098#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6100#[serde(rename_all = "camelCase")]
6101pub struct UiCapabilities {
6102 #[serde(skip_serializing_if = "Option::is_none")]
6104 pub elicitation: Option<bool>,
6105 #[serde(skip_serializing_if = "Option::is_none")]
6116 pub mcp_apps: Option<bool>,
6117 #[serde(skip_serializing_if = "Option::is_none")]
6119 pub canvases: Option<bool>,
6120}
6121
6122#[derive(Debug, Clone, Default)]
6124pub struct UiInputOptions<'a> {
6125 pub title: Option<&'a str>,
6127 pub description: Option<&'a str>,
6129 pub min_length: Option<u64>,
6131 pub max_length: Option<u64>,
6133 pub format: Option<InputFormat>,
6135 pub default: Option<&'a str>,
6137}
6138
6139#[derive(Debug, Clone, Copy)]
6141#[non_exhaustive]
6142pub enum InputFormat {
6143 Email,
6145 Uri,
6147 Date,
6149 DateTime,
6151}
6152
6153impl InputFormat {
6154 pub fn as_str(&self) -> &'static str {
6156 match self {
6157 Self::Email => "email",
6158 Self::Uri => "uri",
6159 Self::Date => "date",
6160 Self::DateTime => "date-time",
6161 }
6162 }
6163}
6164
6165pub use crate::generated::api_types::{
6170 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
6171 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
6172 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
6173 PermissionDecisionApproveOnce, PermissionDecisionContext, PermissionDecisionOutcome,
6174 PermissionDecisionReject, PermissionDecisionSource, PermissionDecisionSurface,
6175 PermissionDecisionUserNotAvailable, PermissionResponseCapability,
6176};
6177
6178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6184#[serde(rename_all = "kebab-case")]
6185#[non_exhaustive]
6186pub enum PermissionRequestKind {
6187 Shell,
6189 Write,
6191 Read,
6193 Url,
6195 Mcp,
6197 CustomTool,
6199 Memory,
6201 Hook,
6203 #[serde(other)]
6206 Unknown,
6207}
6208
6209#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6215#[serde(rename_all = "camelCase")]
6216pub struct PermissionRequestData {
6217 #[serde(default, skip_serializing_if = "Option::is_none")]
6221 pub kind: Option<PermissionRequestKind>,
6222 #[serde(default, skip_serializing_if = "Option::is_none")]
6225 pub tool_call_id: Option<String>,
6226 #[serde(default, skip_serializing_if = "Option::is_none")]
6228 pub managed_approval_required: Option<bool>,
6229 #[serde(default, skip_serializing_if = "is_false")]
6231 pub managed_settings_enabled: bool,
6232 #[serde(flatten)]
6236 pub extra: Value,
6237}
6238
6239#[derive(Debug, Clone, Serialize, Deserialize)]
6241#[serde(rename_all = "camelCase")]
6242pub struct ExitPlanModeData {
6243 #[serde(default)]
6245 pub summary: String,
6246 #[serde(default, skip_serializing_if = "Option::is_none")]
6248 pub plan_content: Option<String>,
6249 #[serde(default)]
6251 pub actions: Vec<String>,
6252 #[serde(default = "default_recommended_action")]
6254 pub recommended_action: String,
6255}
6256
6257fn default_recommended_action() -> String {
6258 "autopilot".to_string()
6259}
6260
6261impl Default for ExitPlanModeData {
6262 fn default() -> Self {
6263 Self {
6264 summary: String::new(),
6265 plan_content: None,
6266 actions: Vec::new(),
6267 recommended_action: default_recommended_action(),
6268 }
6269 }
6270}
6271
6272#[cfg(test)]
6273mod tests {
6274 use std::collections::HashMap;
6275 use std::path::PathBuf;
6276
6277 use serde_json::json;
6278
6279 use super::{
6280 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
6281 AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions,
6282 ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode,
6283 ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType,
6284 InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig,
6285 MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig,
6286 ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
6287 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
6288 ToolResultResponse, ensure_attachment_display_names,
6289 };
6290 use crate::generated::session_events::TypedSessionEvent;
6291
6292 #[test]
6293 fn permission_response_capability_is_publicly_exported() {
6294 assert_eq!(
6295 serde_json::to_value(PermissionResponseCapability::Interactive).unwrap(),
6296 json!("interactive")
6297 );
6298 }
6299
6300 #[test]
6301 fn tool_builder_composes() {
6302 let tool = Tool::new("greet")
6303 .with_description("Say hello")
6304 .with_namespaced_name("hello/greet")
6305 .with_instructions("Pass the user's name")
6306 .with_parameters(json!({
6307 "type": "object",
6308 "properties": { "name": { "type": "string" } },
6309 "required": ["name"]
6310 }))
6311 .with_overrides_built_in_tool(true)
6312 .with_skip_permission(true);
6313 assert_eq!(tool.name, "greet");
6314 assert_eq!(tool.description, "Say hello");
6315 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
6316 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
6317 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
6318 assert!(tool.overrides_built_in_tool);
6319 assert!(tool.skip_permission);
6320 }
6321
6322 #[test]
6323 fn tool_defer_serialization() {
6324 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
6325 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
6326 let value = serde_json::to_value(&tool).unwrap();
6327 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
6328
6329 let plain = Tool::new("plain");
6330 let value = serde_json::to_value(&plain).unwrap();
6331 assert!(value.get("defer").is_none());
6332 }
6333
6334 #[test]
6335 fn tool_metadata_serialization() {
6336 use indexmap::IndexMap;
6337
6338 let mut metadata = IndexMap::new();
6339 metadata.insert(
6340 "github.com/copilot:safeForTelemetry".to_string(),
6341 json!({ "name": true, "inputsNames": false }),
6342 );
6343 let tool = Tool::new("lookup").with_metadata(metadata);
6344 let value = serde_json::to_value(&tool).unwrap();
6345 assert_eq!(
6346 value
6347 .get("metadata")
6348 .unwrap()
6349 .get("github.com/copilot:safeForTelemetry")
6350 .unwrap(),
6351 &json!({ "name": true, "inputsNames": false })
6352 );
6353
6354 let plain = Tool::new("plain");
6356 let value = serde_json::to_value(&plain).unwrap();
6357 assert!(value.get("metadata").is_none());
6358 }
6359
6360 #[test]
6361 fn custom_agent_config_builder_with_model() {
6362 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
6363 .with_model("claude-haiku-4.5")
6364 .with_display_name("My Agent");
6365 assert_eq!(agent.name, "my-agent");
6366 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
6367 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
6368 }
6369
6370 #[test]
6371 fn custom_agent_config_serializes_model() {
6372 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
6373 let wire = serde_json::to_value(&agent).unwrap();
6374 assert_eq!(wire["model"], "claude-haiku-4.5");
6375 assert_eq!(wire["name"], "model-agent");
6376 }
6377
6378 #[test]
6379 fn custom_agent_config_omits_model_when_none() {
6380 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
6381 let wire = serde_json::to_value(&agent).unwrap();
6382 assert!(wire.get("model").is_none());
6383 }
6384
6385 #[test]
6386 fn custom_agent_config_builder_with_reasoning_effort() {
6387 let agent =
6388 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6389 assert_eq!(agent.reasoning_effort.as_deref(), Some("high"));
6390 }
6391
6392 #[test]
6393 fn custom_agent_config_serializes_reasoning_effort() {
6394 let agent =
6395 CustomAgentConfig::new("reasoning-agent", "prompt").with_reasoning_effort("high");
6396 let wire = serde_json::to_value(&agent).unwrap();
6397 assert_eq!(wire["reasoningEffort"], "high");
6398 }
6399
6400 #[test]
6401 fn custom_agent_config_omits_reasoning_effort_when_none() {
6402 let agent = CustomAgentConfig::new("default-agent", "prompt");
6403 let wire = serde_json::to_value(&agent).unwrap();
6404 assert!(wire.get("reasoningEffort").is_none());
6405 }
6406
6407 #[test]
6408 #[should_panic(expected = "tool parameter schema must be a JSON object")]
6409 fn tool_with_parameters_panics_on_non_object_value() {
6410 let _ = Tool::new("noop").with_parameters(json!(null));
6411 }
6412
6413 #[test]
6414 fn tool_result_expanded_serializes_binary_results_for_llm() {
6415 let response = ToolResultResponse {
6416 result: ToolResult::Expanded(ToolResultExpanded {
6417 text_result_for_llm: "rendered chart".to_string(),
6418 result_type: "success".to_string(),
6419 binary_results_for_llm: Some(vec![ToolBinaryResult {
6420 data: "aW1n".to_string(),
6421 mime_type: "image/png".to_string(),
6422 r#type: "image".to_string(),
6423 description: Some("chart preview".to_string()),
6424 }]),
6425 session_log: None,
6426 error: None,
6427 tool_telemetry: None,
6428 tool_references: None,
6429 }),
6430 };
6431
6432 let wire = serde_json::to_value(&response).unwrap();
6433
6434 assert_eq!(
6435 wire,
6436 json!({
6437 "result": {
6438 "textResultForLlm": "rendered chart",
6439 "resultType": "success",
6440 "binaryResultsForLlm": [
6441 {
6442 "data": "aW1n",
6443 "mimeType": "image/png",
6444 "type": "image",
6445 "description": "chart preview"
6446 }
6447 ]
6448 }
6449 })
6450 );
6451 }
6452
6453 #[test]
6454 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
6455 let response = ToolResultResponse {
6456 result: ToolResult::Expanded(ToolResultExpanded {
6457 text_result_for_llm: "ok".to_string(),
6458 result_type: "success".to_string(),
6459 binary_results_for_llm: None,
6460 session_log: None,
6461 error: None,
6462 tool_telemetry: None,
6463 tool_references: None,
6464 }),
6465 };
6466
6467 let wire = serde_json::to_value(&response).unwrap();
6468
6469 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6470 assert!(wire["result"].get("binaryResultsForLlm").is_none());
6471 }
6472
6473 #[test]
6474 fn tool_result_expanded_serializes_tool_references() {
6475 let response = ToolResultResponse {
6476 result: ToolResult::Expanded(
6477 ToolResultExpanded::new("found 2 tools", "success")
6478 .with_tool_references(["get_weather", "check_status"]),
6479 ),
6480 };
6481
6482 let wire = serde_json::to_value(&response).unwrap();
6483
6484 assert_eq!(
6485 wire,
6486 json!({
6487 "result": {
6488 "textResultForLlm": "found 2 tools",
6489 "resultType": "success",
6490 "toolReferences": ["get_weather", "check_status"]
6491 }
6492 })
6493 );
6494 }
6495
6496 #[test]
6497 fn tool_result_expanded_omits_tool_references_when_none() {
6498 let response = ToolResultResponse {
6499 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
6500 };
6501
6502 let wire = serde_json::to_value(&response).unwrap();
6503
6504 assert_eq!(wire["result"]["textResultForLlm"], "ok");
6505 assert!(wire["result"].get("toolReferences").is_none());
6506 }
6507
6508 #[test]
6509 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
6510 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
6513 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
6514
6515 assert_eq!(
6516 expanded.tool_references.as_deref(),
6517 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6518 );
6519 }
6520
6521 #[test]
6522 fn tool_result_expanded_deserializes_tool_references() {
6523 let wire = json!({
6524 "textResultForLlm": "found tools",
6525 "resultType": "success",
6526 "toolReferences": ["alpha", "beta"]
6527 });
6528
6529 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
6530
6531 assert_eq!(
6532 expanded.tool_references.as_deref(),
6533 Some(["alpha".to_string(), "beta".to_string()].as_slice())
6534 );
6535 }
6536
6537 #[test]
6538 fn session_config_default_wire_flags_off_without_handlers() {
6539 let cfg = SessionConfig::default();
6540 assert_eq!(cfg.mcp_oauth_token_storage, None);
6541 assert_eq!(cfg.allowed_models, None);
6542 let (wire, _runtime) = cfg
6546 .into_wire(Some(SessionId::from("default-flags")))
6547 .expect("default config has no duplicate handlers");
6548 assert!(!wire.request_user_input);
6549 assert!(!wire.request_permission);
6550 assert!(!wire.request_elicitation);
6551 assert!(!wire.request_exit_plan_mode);
6552 assert!(!wire.request_auto_mode_switch);
6553 assert!(!wire.hooks);
6554 assert!(!wire.request_mcp_apps);
6555 let json = serde_json::to_value(&wire).unwrap();
6556 assert!(json.get("askUserVariant").is_none());
6557 assert!(json.get("allowedModels").is_none());
6558 }
6559
6560 #[test]
6561 fn resume_session_config_new_wire_flags_off_without_handlers() {
6562 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
6563 assert_eq!(cfg.mcp_oauth_token_storage, None);
6564 assert_eq!(cfg.allowed_models, None);
6565 let (wire, _runtime) = cfg
6566 .into_wire()
6567 .expect("default resume config has no duplicate handlers");
6568 assert!(!wire.request_user_input);
6569 assert!(!wire.request_permission);
6570 assert!(!wire.request_elicitation);
6571 assert!(!wire.request_exit_plan_mode);
6572 assert!(!wire.request_auto_mode_switch);
6573 assert!(!wire.hooks);
6574 assert!(!wire.request_mcp_apps);
6575 let json = serde_json::to_value(&wire).unwrap();
6576 assert!(json.get("askUserVariant").is_none());
6577 assert!(json.get("allowedModels").is_none());
6578 }
6579
6580 #[test]
6581 fn session_configs_build_debug_and_serialize_allowed_models() {
6582 let create = SessionConfig::default().with_allowed_models(["gpt-5.4", "claude-sonnet-4"]);
6583 assert_eq!(
6584 create.allowed_models.as_deref(),
6585 Some(&["gpt-5.4".to_string(), "claude-sonnet-4".to_string()][..])
6586 );
6587 assert!(format!("{create:?}").contains("allowed_models"));
6588
6589 let (create_wire, _) = create
6590 .into_wire(Some(SessionId::from("create-allowed-models")))
6591 .expect("allowed model config has no duplicate handlers");
6592 let create_json = serde_json::to_value(&create_wire).unwrap();
6593 assert_eq!(
6594 create_json["allowedModels"],
6595 json!(["gpt-5.4", "claude-sonnet-4"])
6596 );
6597
6598 let resume = ResumeSessionConfig::new(SessionId::from("resume-allowed-models"))
6599 .with_allowed_models(vec!["gpt-5.4".to_string(), "gpt-5-mini".to_string()]);
6600 assert_eq!(
6601 resume.allowed_models.as_deref(),
6602 Some(&["gpt-5.4".to_string(), "gpt-5-mini".to_string()][..])
6603 );
6604 assert!(format!("{resume:?}").contains("allowed_models"));
6605
6606 let (resume_wire, _) = resume
6607 .into_wire()
6608 .expect("resume allowed model config has no duplicate handlers");
6609 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6610 assert_eq!(
6611 resume_json["allowedModels"],
6612 json!(["gpt-5.4", "gpt-5-mini"])
6613 );
6614 }
6615
6616 #[test]
6617 fn custom_agents_local_only_serializes_on_create_and_resume() {
6618 let (create_wire, _) = SessionConfig::default()
6619 .with_custom_agents_local_only(false)
6620 .into_wire(Some(SessionId::from("create-locality")))
6621 .expect("create config has no duplicate handlers");
6622 let create_json = serde_json::to_value(&create_wire).unwrap();
6623 assert_eq!(create_json["customAgentsLocalOnly"], false);
6624
6625 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-locality"))
6626 .with_custom_agents_local_only(false)
6627 .into_wire()
6628 .expect("resume config has no duplicate handlers");
6629 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6630 assert_eq!(resume_json["customAgentsLocalOnly"], false);
6631
6632 let (unset_create_wire, _) = SessionConfig::default()
6633 .into_wire(Some(SessionId::from("create-unset")))
6634 .expect("create config has no duplicate handlers");
6635 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6636 assert!(unset_create_json.get("customAgentsLocalOnly").is_none());
6637
6638 let (unset_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-unset"))
6639 .into_wire()
6640 .expect("resume config has no duplicate handlers");
6641 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6642 assert!(unset_resume_json.get("customAgentsLocalOnly").is_none());
6643 }
6644
6645 #[test]
6646 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6647 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
6648 assert_eq!(cfg.enable_mcp_apps, Some(true));
6649
6650 let (wire, _runtime) = cfg
6651 .into_wire(Some(SessionId::from("enable-mcp-apps")))
6652 .expect("enable_mcp_apps config has no duplicate handlers");
6653 assert!(wire.request_mcp_apps);
6654
6655 let json = serde_json::to_value(&wire).unwrap();
6656 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6657 }
6658
6659 #[test]
6660 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
6661 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
6662 .with_enable_mcp_apps(true);
6663 assert_eq!(cfg.enable_mcp_apps, Some(true));
6664
6665 let (wire, _runtime) = cfg
6666 .into_wire()
6667 .expect("resume enable_mcp_apps config has no duplicate handlers");
6668 assert!(wire.request_mcp_apps);
6669
6670 let json = serde_json::to_value(&wire).unwrap();
6671 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
6672 }
6673
6674 #[test]
6675 fn github_mcp_tool_config_serializes_for_create_and_resume() {
6676 let github_config = GitHubMcpToolConfig::new()
6677 .with_enable_all_tools(true)
6678 .with_additional_toolsets(["repos"])
6679 .with_additional_tools(["get_issue"])
6680 .with_enable_insiders_mode(true)
6681 .with_disable_form_deferral(true);
6682
6683 let (create_wire, _) = SessionConfig::default()
6684 .with_github_mcp_tool_config(github_config.clone())
6685 .into_wire(Some(SessionId::from("github-mcp")))
6686 .expect("create config has no duplicate handlers");
6687 assert_eq!(
6688 serde_json::to_value(&create_wire).unwrap()["githubMcpToolConfig"],
6689 serde_json::json!({
6690 "enableAllTools": true,
6691 "additionalToolsets": ["repos"],
6692 "additionalTools": ["get_issue"],
6693 "enableInsidersMode": true,
6694 "disableFormDeferral": true,
6695 })
6696 );
6697
6698 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("github-mcp"))
6699 .with_github_mcp_tool_config(github_config)
6700 .into_wire()
6701 .expect("resume config has no duplicate handlers");
6702 assert!(resume_wire.github_mcp_tool_config.is_some());
6703
6704 let (unset_wire, _) = SessionConfig::default()
6705 .into_wire(Some(SessionId::from("github-mcp-unset")))
6706 .expect("default config has no duplicate handlers");
6707 assert!(
6708 serde_json::to_value(&unset_wire)
6709 .unwrap()
6710 .get("githubMcpToolConfig")
6711 .is_none()
6712 );
6713 }
6714
6715 #[test]
6716 fn memory_configuration_constructors_and_serde() {
6717 assert!(MemoryConfiguration::enabled().enabled);
6718 assert!(!MemoryConfiguration::disabled().enabled);
6719 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
6720
6721 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
6722 assert_eq!(json, serde_json::json!({ "enabled": true }));
6723 }
6724
6725 #[test]
6726 fn session_config_with_memory_serializes() {
6727 let (wire, _runtime) = SessionConfig::default()
6728 .with_memory(MemoryConfiguration::enabled())
6729 .into_wire(Some(SessionId::from("memory-on")))
6730 .expect("no duplicate handlers");
6731 let json = serde_json::to_value(&wire).unwrap();
6732 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6733
6734 let (wire_off, _) = SessionConfig::default()
6735 .with_memory(MemoryConfiguration::disabled())
6736 .into_wire(Some(SessionId::from("memory-off")))
6737 .expect("no duplicate handlers");
6738 let json_off = serde_json::to_value(&wire_off).unwrap();
6739 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
6740
6741 let (empty_wire, _) = SessionConfig::default()
6743 .into_wire(Some(SessionId::from("memory-unset")))
6744 .expect("no duplicate handlers");
6745 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6746 assert!(empty_json.get("memory").is_none());
6747 }
6748
6749 #[test]
6750 fn resume_session_config_with_memory_serializes() {
6751 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
6752 .with_memory(MemoryConfiguration::enabled())
6753 .into_wire()
6754 .expect("no duplicate handlers");
6755 let json = serde_json::to_value(&wire).unwrap();
6756 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
6757
6758 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
6760 .into_wire()
6761 .expect("no duplicate handlers");
6762 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6763 assert!(empty_json.get("memory").is_none());
6764 }
6765
6766 #[test]
6767 fn feature_flags_serialize_on_create_and_resume() {
6768 let feature_flags = HashMap::from([
6769 ("BACKGROUND_TASK_NOTIFICATION_PAYLOADS".to_string(), true),
6770 ("DISABLED_TEST_FLAG".to_string(), false),
6771 ]);
6772 let expected = serde_json::json!({
6773 "BACKGROUND_TASK_NOTIFICATION_PAYLOADS": true,
6774 "DISABLED_TEST_FLAG": false,
6775 });
6776
6777 let create_config = SessionConfig::default().with_feature_flags(feature_flags.clone());
6778 assert_eq!(create_config.feature_flags.as_ref(), Some(&feature_flags));
6779 let (create_wire, _) = create_config
6780 .into_wire(Some(SessionId::from("feature-flags-create")))
6781 .expect("no duplicate handlers");
6782 let create_json = serde_json::to_value(&create_wire).unwrap();
6783 assert_eq!(create_json["featureFlags"], expected);
6784
6785 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("feature-flags-resume"))
6786 .with_feature_flags(feature_flags)
6787 .into_wire()
6788 .expect("no duplicate handlers");
6789 let resume_json = serde_json::to_value(&resume_wire).unwrap();
6790 assert_eq!(resume_json["featureFlags"], expected);
6791
6792 let (unset_create_wire, _) = SessionConfig::default()
6793 .into_wire(Some(SessionId::from("feature-flags-create-unset")))
6794 .expect("no duplicate handlers");
6795 let unset_create_json = serde_json::to_value(&unset_create_wire).unwrap();
6796 assert!(unset_create_json.get("featureFlags").is_none());
6797
6798 let (unset_resume_wire, _) =
6799 ResumeSessionConfig::new(SessionId::from("feature-flags-resume-unset"))
6800 .into_wire()
6801 .expect("no duplicate handlers");
6802 let unset_resume_json = serde_json::to_value(&unset_resume_wire).unwrap();
6803 assert!(unset_resume_json.get("featureFlags").is_none());
6804 }
6805
6806 fn sample_exp_assignments(context: &str) -> CopilotExpAssignmentResponse {
6807 CopilotExpAssignmentResponse {
6808 features: vec!["copilot_exp_flag".to_string()],
6809 flights: HashMap::from([("copilot_exp_flag".to_string(), "treatment".to_string())]),
6810 configs: vec![ExpConfigEntry {
6811 id: "cfg-1".to_string(),
6812 parameters: HashMap::from([
6813 ("threshold".to_string(), ExpFlagValue::Integer(5)),
6814 ("enabled".to_string(), ExpFlagValue::Bool(true)),
6815 ]),
6816 }],
6817 assignment_context: context.to_string(),
6818 ..Default::default()
6819 }
6820 }
6821
6822 #[test]
6823 fn exp_flag_value_round_trips_all_variants() {
6824 let values = serde_json::json!({
6825 "s": "text",
6826 "i": 7,
6827 "f": 1.5,
6828 "b": true,
6829 "n": null,
6830 });
6831 let parsed: HashMap<String, ExpFlagValue> = serde_json::from_value(values.clone()).unwrap();
6832 assert_eq!(parsed["s"], ExpFlagValue::String("text".to_string()));
6833 assert_eq!(parsed["i"], ExpFlagValue::Integer(7));
6834 assert_eq!(parsed["f"], ExpFlagValue::Float(1.5));
6835 assert_eq!(parsed["b"], ExpFlagValue::Bool(true));
6836 assert_eq!(parsed["n"], ExpFlagValue::Null);
6837 assert_eq!(serde_json::to_value(&parsed).unwrap(), values);
6838 }
6839
6840 #[test]
6841 fn session_config_with_exp_assignments_serializes() {
6842 let assignments = sample_exp_assignments("ctx-123");
6843 let expected = serde_json::to_value(&assignments).unwrap();
6844 let (wire, _runtime) = SessionConfig::default()
6845 .with_exp_assignments(assignments)
6846 .into_wire(Some(SessionId::from("exp-on")))
6847 .expect("no duplicate handlers");
6848 let json = serde_json::to_value(&wire).unwrap();
6849 assert_eq!(json["expAssignments"], expected);
6850 assert_eq!(json["expAssignments"]["AssignmentContext"], "ctx-123");
6851 assert_eq!(
6852 json["expAssignments"]["Flights"]["copilot_exp_flag"],
6853 "treatment"
6854 );
6855
6856 let (empty_wire, _) = SessionConfig::default()
6858 .into_wire(Some(SessionId::from("exp-unset")))
6859 .expect("no duplicate handlers");
6860 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6861 assert!(empty_json.get("expAssignments").is_none());
6862 }
6863
6864 #[test]
6865 fn resume_session_config_with_exp_assignments_serializes() {
6866 let assignments = sample_exp_assignments("ctx-456");
6867 let expected = serde_json::to_value(&assignments).unwrap();
6868 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
6869 .with_exp_assignments(assignments)
6870 .into_wire()
6871 .expect("no duplicate handlers");
6872 let json = serde_json::to_value(&wire).unwrap();
6873 assert_eq!(json["expAssignments"], expected);
6874
6875 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
6877 .into_wire()
6878 .expect("no duplicate handlers");
6879 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6880 assert!(empty_json.get("expAssignments").is_none());
6881 }
6882
6883 #[test]
6884 fn session_config_clone_preserves_exp_assignments() {
6885 let assignments = sample_exp_assignments("ctx-clone");
6886 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
6887 let cloned = config.clone();
6888
6889 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6890
6891 let (wire, _runtime) = cloned
6892 .into_wire(Some(SessionId::from("exp-clone")))
6893 .expect("no duplicate handlers");
6894 let json = serde_json::to_value(&wire).unwrap();
6895 assert_eq!(
6896 json["expAssignments"],
6897 serde_json::to_value(&assignments).unwrap()
6898 );
6899 }
6900
6901 #[test]
6902 fn resume_session_config_clone_preserves_exp_assignments() {
6903 let assignments = sample_exp_assignments("ctx-clone-resume");
6904 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
6905 .with_exp_assignments(assignments.clone());
6906 let cloned = config.clone();
6907
6908 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
6909
6910 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
6911 let json = serde_json::to_value(&wire).unwrap();
6912 assert_eq!(
6913 json["expAssignments"],
6914 serde_json::to_value(&assignments).unwrap()
6915 );
6916 }
6917
6918 #[test]
6919 #[allow(clippy::field_reassign_with_default)]
6920 fn session_config_into_wire_serializes_bucket_b_fields() {
6921 use std::path::PathBuf;
6922
6923 use super::{CloudSessionOptions, CloudSessionRepository};
6924
6925 let mut cfg = SessionConfig::default();
6926 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6927 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6928 cfg.github_token = Some("ghs_secret".to_string());
6929 cfg.include_sub_agent_streaming_events = Some(false);
6930 cfg.enable_session_telemetry = Some(false);
6931 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
6932 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
6933 cfg.enable_on_demand_instruction_discovery = Some(false);
6934 cfg.cloud = Some(CloudSessionOptions::with_repository(
6935 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
6936 ));
6937
6938 let (wire, _runtime) = cfg
6939 .into_wire(Some(SessionId::from("custom-id")))
6940 .expect("no duplicate handlers");
6941 let wire_json = serde_json::to_value(&wire).unwrap();
6942 assert_eq!(wire_json["sessionId"], "custom-id");
6943 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6944 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6945 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6946 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
6947 assert_eq!(wire_json["enableSessionTelemetry"], false);
6948 assert_eq!(wire_json["reasoningSummary"], "concise");
6949 assert_eq!(wire_json["remoteSession"], "export");
6950 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6951 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
6952 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
6953 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
6954
6955 let (empty_wire, _) = SessionConfig::default()
6957 .into_wire(Some(SessionId::from("empty")))
6958 .expect("default has no duplicate handlers");
6959 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6960 assert!(empty_json.get("gitHubToken").is_none());
6961 assert!(empty_json.get("enableSessionTelemetry").is_none());
6962 assert!(empty_json.get("reasoningSummary").is_none());
6963 assert!(empty_json.get("remoteSession").is_none());
6964 assert!(
6965 empty_json
6966 .get("enableOnDemandInstructionDiscovery")
6967 .is_none()
6968 );
6969 assert!(empty_json.get("cloud").is_none());
6970 }
6971
6972 #[test]
6973 fn session_config_into_wire_serializes_named_providers_and_models() {
6974 let cfg = SessionConfig::default()
6975 .with_providers(vec![
6976 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
6977 .with_provider_type("openai")
6978 .with_wire_api("responses")
6979 .with_api_key("sk-test"),
6980 ])
6981 .with_models(vec![
6982 ProviderModelConfig::new("gpt-x", "my-openai")
6983 .with_wire_model("gpt-x-2025")
6984 .with_max_output_tokens(2048),
6985 ]);
6986
6987 let (wire, _) = cfg
6988 .into_wire(Some(SessionId::from("sess-providers")))
6989 .expect("no duplicate handlers");
6990 let wire_json = serde_json::to_value(&wire).unwrap();
6991 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
6992 assert_eq!(
6993 wire_json["providers"][0]["baseUrl"],
6994 "https://api.example.com/v1"
6995 );
6996 assert_eq!(wire_json["providers"][0]["type"], "openai");
6997 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
6998 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
6999 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
7000 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
7001 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
7002 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
7003
7004 let (empty_wire, _) = SessionConfig::default()
7005 .into_wire(Some(SessionId::from("empty")))
7006 .expect("default has no duplicate handlers");
7007 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7008 assert!(empty_json.get("providers").is_none());
7009 assert!(empty_json.get("models").is_none());
7010 }
7011
7012 #[test]
7013 fn resume_config_into_wire_serializes_named_providers_and_models() {
7014 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
7015 .with_providers(vec![
7016 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
7017 .with_provider_type("azure")
7018 .with_azure(AzureProviderOptions {
7019 api_version: Some("2024-10-21".to_string()),
7020 }),
7021 ])
7022 .with_models(vec![
7023 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
7024 ]);
7025
7026 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7027 let wire_json = serde_json::to_value(&wire).unwrap();
7028 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
7029 assert_eq!(wire_json["providers"][0]["type"], "azure");
7030 assert_eq!(
7031 wire_json["providers"][0]["azure"]["apiVersion"],
7032 "2024-10-21"
7033 );
7034 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
7035 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
7036 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
7037
7038 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
7039 .into_wire()
7040 .expect("default has no duplicate handlers");
7041 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7042 assert!(empty_json.get("providers").is_none());
7043 assert!(empty_json.get("models").is_none());
7044 }
7045
7046 #[test]
7047 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
7048 use std::path::PathBuf;
7049
7050 let cfg = SessionConfig {
7051 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
7052 disabled_mcp_servers: Some(vec![
7053 "local-files".to_string(),
7054 "remote-github".to_string(),
7055 ]),
7056 large_output: Some(
7057 LargeToolOutputConfig::new()
7058 .with_enabled(true)
7059 .with_max_size_bytes(1024)
7060 .with_output_directory(PathBuf::from("/tmp/large-output")),
7061 ),
7062 ..Default::default()
7063 };
7064
7065 let (wire, _) = cfg
7066 .into_wire(Some(SessionId::from("sess-1")))
7067 .expect("no duplicate handlers");
7068 let wire_json = serde_json::to_value(&wire).unwrap();
7069 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
7070 assert_eq!(
7071 wire_json["disabledMcpServers"],
7072 serde_json::json!(["local-files", "remote-github"])
7073 );
7074 assert_eq!(wire_json["largeOutput"]["enabled"], true);
7075 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
7076 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
7077
7078 let (empty_wire, _) = SessionConfig::default()
7079 .into_wire(Some(SessionId::from("empty")))
7080 .expect("default has no duplicate handlers");
7081 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7082 assert!(empty_json.get("pluginDirectories").is_none());
7083 assert!(empty_json.get("disabledMcpServers").is_none());
7084 assert!(empty_json.get("largeOutput").is_none());
7085 }
7086
7087 #[test]
7088 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
7089 use std::path::PathBuf;
7090
7091 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
7092 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
7093 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
7094 cfg.github_token = Some("ghs_secret".to_string());
7095 cfg.include_sub_agent_streaming_events = Some(true);
7096 cfg.enable_session_telemetry = Some(false);
7097 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
7098 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
7099 cfg.enable_on_demand_instruction_discovery = Some(false);
7100
7101 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7102 let wire_json = serde_json::to_value(&wire).unwrap();
7103 assert_eq!(wire_json["sessionId"], "sess-1");
7104 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
7105 assert_eq!(wire_json["configDir"], "/tmp/cfg");
7106 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
7107 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
7108 assert_eq!(wire_json["enableSessionTelemetry"], false);
7109 assert_eq!(wire_json["reasoningSummary"], "detailed");
7110 assert_eq!(wire_json["remoteSession"], "on");
7111 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
7112
7113 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7115 .into_wire()
7116 .expect("default resume has no duplicate handlers");
7117 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7118 assert!(empty_json.get("reasoningSummary").is_none());
7119 assert!(empty_json.get("remoteSession").is_none());
7120 assert!(
7121 empty_json
7122 .get("enableOnDemandInstructionDiscovery")
7123 .is_none()
7124 );
7125 }
7126
7127 #[test]
7128 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
7129 use std::path::PathBuf;
7130
7131 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
7132 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
7133 cfg.disabled_mcp_servers = Some(vec!["local-files-r".to_string()]);
7134 cfg.large_output = Some(
7135 LargeToolOutputConfig::new()
7136 .with_enabled(false)
7137 .with_max_size_bytes(2048)
7138 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
7139 );
7140
7141 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7142 let wire_json = serde_json::to_value(&wire).unwrap();
7143 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
7144 assert_eq!(
7145 wire_json["disabledMcpServers"],
7146 serde_json::json!(["local-files-r"])
7147 );
7148 assert_eq!(wire_json["largeOutput"]["enabled"], false);
7149 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
7150 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
7151
7152 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7153 .into_wire()
7154 .expect("default resume has no duplicate handlers");
7155 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7156 assert!(empty_json.get("pluginDirectories").is_none());
7157 assert!(empty_json.get("disabledMcpServers").is_none());
7158 assert!(empty_json.get("largeOutput").is_none());
7159 }
7160
7161 #[test]
7162 fn auth_client_id_metadata_url_reaches_create_and_resume_wire_payloads() {
7163 let url = "https://example.com/oauth/client-metadata.json";
7164
7165 let (create_wire, _) = SessionConfig::default()
7166 .with_auth_client_id_metadata_url(url)
7167 .into_wire(None)
7168 .expect("default create has no duplicate handlers");
7169 let create_json = serde_json::to_value(&create_wire).unwrap();
7170 assert_eq!(create_json["authClientIdMetadataUrl"], url);
7171
7172 let (resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-1"))
7173 .with_auth_client_id_metadata_url(url)
7174 .into_wire()
7175 .expect("default resume has no duplicate handlers");
7176 let resume_json = serde_json::to_value(&resume_wire).unwrap();
7177 assert_eq!(resume_json["authClientIdMetadataUrl"], url);
7178
7179 let (empty_create_wire, _) = SessionConfig::default()
7180 .into_wire(None)
7181 .expect("default create has no duplicate handlers");
7182 let empty_create_json = serde_json::to_value(&empty_create_wire).unwrap();
7183 assert!(empty_create_json.get("authClientIdMetadataUrl").is_none());
7184
7185 let (empty_resume_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7186 .into_wire()
7187 .expect("default resume has no duplicate handlers");
7188 let empty_resume_json = serde_json::to_value(&empty_resume_wire).unwrap();
7189 assert!(empty_resume_json.get("authClientIdMetadataUrl").is_none());
7190 }
7191
7192 #[test]
7193 fn session_config_clones_disabled_mcp_servers() {
7194 let create = SessionConfig::default().with_disabled_mcp_servers(["local-files"]);
7195 let mut create_clone = create.clone();
7196 create_clone
7197 .disabled_mcp_servers
7198 .as_mut()
7199 .expect("configured disabled MCP servers")
7200 .push("remote-github".to_string());
7201 assert_eq!(
7202 create.disabled_mcp_servers.as_deref(),
7203 Some(&["local-files".to_string()][..])
7204 );
7205
7206 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
7207 .with_disabled_mcp_servers(["local-files"]);
7208 let mut resume_clone = resume.clone();
7209 resume_clone
7210 .disabled_mcp_servers
7211 .as_mut()
7212 .expect("configured disabled MCP servers")
7213 .push("remote-github".to_string());
7214 assert_eq!(
7215 resume.disabled_mcp_servers.as_deref(),
7216 Some(&["local-files".to_string()][..])
7217 );
7218 }
7219
7220 #[test]
7221 fn session_config_builder_composes() {
7222 use indexmap::IndexMap;
7223
7224 let cfg = SessionConfig::default()
7225 .with_session_id(SessionId::from("sess-1"))
7226 .with_model("claude-sonnet-4")
7227 .with_client_name("test-app")
7228 .with_reasoning_effort("medium")
7229 .with_reasoning_summary(ReasoningSummary::Concise)
7230 .with_context_tier("long_context")
7231 .with_streaming(true)
7232 .with_tools([Tool::new("greet")])
7233 .with_available_tools(["bash", "view"])
7234 .with_excluded_tools(["dangerous"])
7235 .with_mcp_servers(IndexMap::new())
7236 .with_mcp_oauth_token_storage("persistent")
7237 .with_enable_config_discovery(true)
7238 .with_enable_on_demand_instruction_discovery(true)
7239 .with_skill_directories([PathBuf::from("/tmp/skills")])
7240 .with_disabled_skills(["broken-skill"])
7241 .with_disabled_mcp_servers(["local-files"])
7242 .with_agent("researcher")
7243 .with_config_directory(PathBuf::from("/tmp/config"))
7244 .with_working_directory(PathBuf::from("/tmp/work"))
7245 .with_additional_directories([PathBuf::from("/tmp/shared")])
7246 .with_github_token("ghp_test")
7247 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7248 .with_enable_session_telemetry(false)
7249 .with_include_sub_agent_streaming_events(false)
7250 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
7251
7252 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
7253 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
7254 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
7255 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
7256 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
7257 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
7258 assert_eq!(cfg.streaming, Some(true));
7259 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
7260 assert_eq!(
7261 cfg.available_tools.as_deref(),
7262 Some(&["bash".to_string(), "view".to_string()][..])
7263 );
7264 assert_eq!(
7265 cfg.excluded_tools.as_deref(),
7266 Some(&["dangerous".to_string()][..])
7267 );
7268 assert!(cfg.mcp_servers.is_some());
7269 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
7270 assert_eq!(cfg.enable_config_discovery, Some(true));
7271 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
7272 assert_eq!(
7273 cfg.skill_directories.as_deref(),
7274 Some(&[PathBuf::from("/tmp/skills")][..])
7275 );
7276 assert_eq!(
7277 cfg.disabled_skills.as_deref(),
7278 Some(&["broken-skill".to_string()][..])
7279 );
7280 assert_eq!(
7281 cfg.disabled_mcp_servers.as_deref(),
7282 Some(&["local-files".to_string()][..])
7283 );
7284 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
7285 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
7286 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
7287 assert_eq!(
7288 cfg.additional_directories.as_deref(),
7289 Some(&[PathBuf::from("/tmp/shared")][..])
7290 );
7291 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
7292 assert_eq!(
7293 cfg.capi,
7294 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7295 );
7296 assert_eq!(cfg.enable_session_telemetry, Some(false));
7297 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
7298 assert_eq!(
7299 cfg.extension_info,
7300 Some(ExtensionInfo::new("github-app", "counter"))
7301 );
7302 }
7303
7304 #[test]
7305 fn resume_session_config_builder_composes() {
7306 use indexmap::IndexMap;
7307
7308 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
7309 .with_client_name("test-app")
7310 .with_reasoning_summary(ReasoningSummary::None)
7311 .with_context_tier("default")
7312 .with_streaming(true)
7313 .with_tools([Tool::new("greet")])
7314 .with_available_tools(["bash", "view"])
7315 .with_excluded_tools(["dangerous"])
7316 .with_mcp_servers(IndexMap::new())
7317 .with_mcp_oauth_token_storage("persistent")
7318 .with_enable_config_discovery(true)
7319 .with_enable_on_demand_instruction_discovery(false)
7320 .with_skill_directories([PathBuf::from("/tmp/skills")])
7321 .with_disabled_skills(["broken-skill"])
7322 .with_disabled_mcp_servers(["local-files"])
7323 .with_agent("researcher")
7324 .with_config_directory(PathBuf::from("/tmp/config"))
7325 .with_working_directory(PathBuf::from("/tmp/work"))
7326 .with_additional_directories([PathBuf::from("/tmp/shared")])
7327 .with_github_token("ghp_test")
7328 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7329 .with_enable_session_telemetry(false)
7330 .with_include_sub_agent_streaming_events(true)
7331 .with_suppress_resume_event(true)
7332 .with_continue_pending_work(true)
7333 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
7334
7335 assert_eq!(cfg.session_id.as_str(), "sess-2");
7336 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
7337 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
7338 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
7339 assert_eq!(cfg.streaming, Some(true));
7340 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
7341 assert_eq!(
7342 cfg.available_tools.as_deref(),
7343 Some(&["bash".to_string(), "view".to_string()][..])
7344 );
7345 assert_eq!(
7346 cfg.excluded_tools.as_deref(),
7347 Some(&["dangerous".to_string()][..])
7348 );
7349 assert!(cfg.mcp_servers.is_some());
7350 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
7351 assert_eq!(cfg.enable_config_discovery, Some(true));
7352 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
7353 assert_eq!(
7354 cfg.skill_directories.as_deref(),
7355 Some(&[PathBuf::from("/tmp/skills")][..])
7356 );
7357 assert_eq!(
7358 cfg.disabled_skills.as_deref(),
7359 Some(&["broken-skill".to_string()][..])
7360 );
7361 assert_eq!(
7362 cfg.disabled_mcp_servers.as_deref(),
7363 Some(&["local-files".to_string()][..])
7364 );
7365 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
7366 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
7367 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
7368 assert_eq!(
7369 cfg.additional_directories.as_deref(),
7370 Some(&[PathBuf::from("/tmp/shared")][..])
7371 );
7372 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
7373 assert_eq!(
7374 cfg.capi,
7375 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7376 );
7377 assert_eq!(cfg.enable_session_telemetry, Some(false));
7378 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
7379 assert_eq!(cfg.suppress_resume_event, Some(true));
7380 assert_eq!(cfg.continue_pending_work, Some(true));
7381 assert_eq!(
7382 cfg.extension_info,
7383 Some(ExtensionInfo::new("github-app", "counter"))
7384 );
7385 }
7386
7387 #[test]
7391 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
7392 let cfg =
7393 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
7394 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7395 let json = serde_json::to_value(&wire).unwrap();
7396 assert_eq!(json["continuePendingWork"], true);
7397
7398 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7400 .into_wire()
7401 .expect("no duplicate handlers");
7402 let json = serde_json::to_value(&wire).unwrap();
7403 assert!(json.get("continuePendingWork").is_none());
7404 }
7405
7406 #[test]
7407 fn session_configs_serialize_additional_directories() {
7408 let create = SessionConfig::default().with_additional_directories([
7409 PathBuf::from("/tmp/shared"),
7410 PathBuf::from("/tmp/generated"),
7411 ]);
7412 let (create_wire, _) = create.into_wire(None).expect("no duplicate handlers");
7413 let create_json = serde_json::to_value(&create_wire).unwrap();
7414 assert_eq!(
7415 create_json["additionalDirectories"],
7416 serde_json::json!(["/tmp/shared", "/tmp/generated"])
7417 );
7418
7419 let resume = ResumeSessionConfig::new(SessionId::from("sess-1"))
7420 .with_additional_directories([PathBuf::from("/tmp/resumed")]);
7421 let (resume_wire, _) = resume.into_wire().expect("no duplicate handlers");
7422 let resume_json = serde_json::to_value(&resume_wire).unwrap();
7423 assert_eq!(
7424 resume_json["additionalDirectories"],
7425 serde_json::json!(["/tmp/resumed"])
7426 );
7427 }
7428
7429 #[test]
7433 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
7434 let cfg =
7435 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
7436 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7437 let json = serde_json::to_value(&wire).unwrap();
7438 assert_eq!(json["disableResume"], true);
7439 assert!(json.get("suppressResumeEvent").is_none());
7440 }
7441
7442 #[test]
7445 fn session_config_serializes_instruction_directories_to_camel_case() {
7446 let cfg =
7447 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
7448 let (wire, _) = cfg
7449 .into_wire(Some(SessionId::from("instr-on")))
7450 .expect("no duplicate handlers");
7451 let json = serde_json::to_value(&wire).unwrap();
7452 assert_eq!(
7453 json["instructionDirectories"],
7454 serde_json::json!(["/tmp/instr"])
7455 );
7456
7457 let (wire, _) = SessionConfig::default()
7459 .into_wire(Some(SessionId::from("instr-off")))
7460 .expect("no duplicate handlers");
7461 let json = serde_json::to_value(&wire).unwrap();
7462 assert!(json.get("instructionDirectories").is_none());
7463 }
7464
7465 #[test]
7468 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
7469 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
7470 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
7471 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
7472 let json = serde_json::to_value(&wire).unwrap();
7473 assert_eq!(
7474 json["instructionDirectories"],
7475 serde_json::json!(["/tmp/instr"])
7476 );
7477
7478 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
7479 .into_wire()
7480 .expect("no duplicate handlers");
7481 let json = serde_json::to_value(&wire).unwrap();
7482 assert!(json.get("instructionDirectories").is_none());
7483 }
7484
7485 #[test]
7486 fn custom_agent_config_builder_composes() {
7487 use indexmap::IndexMap;
7488
7489 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
7490 .with_display_name("Research Assistant")
7491 .with_description("Investigates technical questions.")
7492 .with_tools(["bash", "view"])
7493 .with_mcp_servers(IndexMap::new())
7494 .with_infer(true)
7495 .with_skills(["rust-coding-skill"]);
7496
7497 assert_eq!(cfg.name, "researcher");
7498 assert_eq!(cfg.prompt, "You are a research assistant.");
7499 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
7500 assert_eq!(
7501 cfg.description.as_deref(),
7502 Some("Investigates technical questions.")
7503 );
7504 assert_eq!(
7505 cfg.tools.as_deref(),
7506 Some(&["bash".to_string(), "view".to_string()][..])
7507 );
7508 assert!(cfg.mcp_servers.is_some());
7509 assert_eq!(cfg.infer, Some(true));
7510 assert_eq!(
7511 cfg.skills.as_deref(),
7512 Some(&["rust-coding-skill".to_string()][..])
7513 );
7514 }
7515
7516 #[test]
7517 fn mcp_servers_serialize_in_insertion_order() {
7518 use indexmap::IndexMap;
7519
7520 let order = [
7526 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
7527 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
7528 ];
7529 let mut servers = IndexMap::new();
7530 for name in order {
7531 servers.insert(
7532 name.to_string(),
7533 McpServerConfig::Stdio(McpStdioServerConfig {
7534 command: "run".to_string(),
7535 ..Default::default()
7536 }),
7537 );
7538 }
7539
7540 let (wire, _runtime) = SessionConfig::default()
7541 .with_mcp_servers(servers)
7542 .into_wire(None)
7543 .expect("into_wire should succeed");
7544 let json = serde_json::to_string(&wire).expect("serialize wire");
7545
7546 let positions: Vec<usize> = order
7547 .iter()
7548 .map(|name| {
7549 json.find(&format!("\"{name}\""))
7550 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
7551 })
7552 .collect();
7553 let mut ascending = positions.clone();
7554 ascending.sort_unstable();
7555 assert_eq!(
7556 positions, ascending,
7557 "mcp server keys must serialize in insertion order: {json}"
7558 );
7559 }
7560
7561 #[test]
7562 fn infinite_session_config_builder_composes() {
7563 let cfg = InfiniteSessionConfig::new()
7564 .with_enabled(true)
7565 .with_background_compaction_threshold(0.75)
7566 .with_buffer_exhaustion_threshold(0.92);
7567
7568 assert_eq!(cfg.enabled, Some(true));
7569 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
7570 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
7571 }
7572
7573 #[test]
7574 fn provider_config_builder_composes() {
7575 use std::collections::HashMap;
7576
7577 let mut headers = HashMap::new();
7578 headers.insert("X-Custom".to_string(), "value".to_string());
7579
7580 let cfg = ProviderConfig::new("https://api.example.com")
7581 .with_provider_type("openai")
7582 .with_wire_api("completions")
7583 .with_transport("websockets")
7584 .with_api_key("sk-test")
7585 .with_bearer_token("bearer-test")
7586 .with_headers(headers)
7587 .with_model_id("gpt-4")
7588 .with_wire_model("azure-gpt-4-deployment")
7589 .with_max_prompt_tokens(8192)
7590 .with_max_output_tokens(2048);
7591
7592 assert_eq!(cfg.base_url, "https://api.example.com");
7593 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
7594 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
7595 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
7596 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
7597 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
7598 assert_eq!(
7599 cfg.headers
7600 .as_ref()
7601 .and_then(|h| h.get("X-Custom"))
7602 .map(String::as_str),
7603 Some("value"),
7604 );
7605 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
7606 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
7607 assert_eq!(cfg.max_prompt_tokens, Some(8192));
7608 assert_eq!(cfg.max_output_tokens, Some(2048));
7609
7610 let wire = serde_json::to_value(&cfg).unwrap();
7612 assert_eq!(wire["modelId"], "gpt-4");
7613 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
7614 assert_eq!(wire["maxPromptTokens"], 8192);
7615 assert_eq!(wire["maxOutputTokens"], 2048);
7616
7617 let unset = ProviderConfig::new("https://api.example.com");
7618 let wire_unset = serde_json::to_value(&unset).unwrap();
7619 assert!(wire_unset.get("modelId").is_none());
7620 assert!(wire_unset.get("wireModel").is_none());
7621 assert!(wire_unset.get("maxPromptTokens").is_none());
7622 assert!(wire_unset.get("maxOutputTokens").is_none());
7623 }
7624
7625 #[test]
7626 fn capi_session_options_builder_composes_and_serializes() {
7627 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
7628
7629 assert_eq!(cfg.enable_web_socket_responses, Some(false));
7630
7631 let wire = serde_json::to_value(&cfg).unwrap();
7632 assert_eq!(
7633 wire,
7634 serde_json::json!({ "enableWebSocketResponses": false })
7635 );
7636
7637 let unset = CapiSessionOptions::new();
7638 let wire_unset = serde_json::to_value(&unset).unwrap();
7639 assert!(wire_unset.get("enableWebSocketResponses").is_none());
7640 assert!(wire_unset.get("autoTier").is_none());
7641 assert_eq!(wire_unset, json!({}));
7642 }
7643
7644 #[test]
7645 fn capi_auto_tier_canonical_values_round_trip_and_forward() {
7646 for (tier, value) in [
7647 (AutoTier::Efficiency, "efficiency"),
7648 (AutoTier::Balance, "balance"),
7649 (AutoTier::Intelligence, "intelligence"),
7650 (AutoTier::Fast, "fast"),
7651 ] {
7652 let exported: crate::AutoTier = tier.clone();
7653 let capi = CapiSessionOptions::new().with_auto_tier(exported);
7654 assert_eq!(capi.auto_tier, Some(tier));
7655 assert_eq!(
7656 serde_json::to_value(&capi).unwrap(),
7657 json!({"autoTier": value})
7658 );
7659 assert_eq!(
7660 serde_json::from_value::<CapiSessionOptions>(json!({"autoTier": value})).unwrap(),
7661 capi
7662 );
7663
7664 let capi = capi.with_enable_web_socket_responses(false);
7665 let expected = json!({"autoTier": value, "enableWebSocketResponses": false});
7666 let (create, _) = SessionConfig::default()
7667 .with_model("auto")
7668 .with_capi(capi.clone())
7669 .into_wire(Some(SessionId::from("capi-create")))
7670 .unwrap();
7671 assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected);
7672
7673 let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7674 .with_capi(capi)
7675 .into_wire()
7676 .unwrap();
7677 assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected);
7678 }
7679 }
7680
7681 #[test]
7682 fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() {
7683 for value in ["balanced", "Balance", "unknown"] {
7684 assert_eq!(
7685 serde_json::from_value::<AutoTier>(json!(value)).unwrap(),
7686 AutoTier::Unknown
7687 );
7688 }
7689 let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap();
7690 assert_eq!(capi.auto_tier, None);
7691 }
7692
7693 #[test]
7694 fn session_config_with_capi_serializes() {
7695 let (wire, _) = SessionConfig::default()
7696 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7697 .into_wire(Some(SessionId::from("capi-create")))
7698 .expect("no duplicate handlers");
7699 let json = serde_json::to_value(&wire).unwrap();
7700 assert_eq!(
7701 json["capi"],
7702 serde_json::json!({ "enableWebSocketResponses": false })
7703 );
7704
7705 let (empty_wire, _) = SessionConfig::default()
7706 .into_wire(Some(SessionId::from("capi-create-unset")))
7707 .expect("no duplicate handlers");
7708 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7709 assert!(empty_json.get("capi").is_none());
7710 }
7711
7712 #[test]
7713 fn resume_session_config_with_capi_serializes() {
7714 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
7715 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
7716 .into_wire()
7717 .expect("no duplicate handlers");
7718 let json = serde_json::to_value(&wire).unwrap();
7719 assert_eq!(
7720 json["capi"],
7721 serde_json::json!({ "enableWebSocketResponses": false })
7722 );
7723
7724 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
7725 .into_wire()
7726 .expect("no duplicate handlers");
7727 let empty_json = serde_json::to_value(&empty_wire).unwrap();
7728 assert!(empty_json.get("capi").is_none());
7729 }
7730
7731 #[test]
7732 fn system_message_config_builder_composes() {
7733 use std::collections::HashMap;
7734
7735 let cfg = SystemMessageConfig::new()
7736 .with_mode("replace")
7737 .with_content("Custom system message.")
7738 .with_sections(HashMap::new());
7739
7740 assert_eq!(cfg.mode.as_deref(), Some("replace"));
7741 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
7742 assert!(cfg.sections.is_some());
7743 }
7744
7745 #[test]
7746 fn delivery_mode_serializes_to_kebab_case_strings() {
7747 assert_eq!(
7748 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
7749 "\"enqueue\""
7750 );
7751 assert_eq!(
7752 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
7753 "\"immediate\""
7754 );
7755 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
7756 assert_eq!(parsed, DeliveryMode::Immediate);
7757 }
7758
7759 #[test]
7760 fn agent_mode_serializes_to_kebab_case_strings() {
7761 assert_eq!(
7762 serde_json::to_string(&AgentMode::Interactive).unwrap(),
7763 "\"interactive\""
7764 );
7765 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
7766 assert_eq!(
7767 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
7768 "\"autopilot\""
7769 );
7770 assert_eq!(
7771 serde_json::to_string(&AgentMode::Shell).unwrap(),
7772 "\"shell\""
7773 );
7774 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
7775 assert_eq!(parsed, AgentMode::Plan);
7776 }
7777
7778 #[test]
7779 fn connection_state_distinguishes_variants() {
7780 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
7783 }
7784
7785 #[test]
7791 fn session_event_round_trips_agent_id_on_envelope() {
7792 let wire = json!({
7793 "id": "evt-1",
7794 "timestamp": "2026-04-30T12:00:00Z",
7795 "parentId": null,
7796 "agentId": "sub-agent-42",
7797 "type": "assistant.message",
7798 "data": { "message": "hi" }
7799 });
7800
7801 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
7802 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7803
7804 let roundtripped = serde_json::to_value(&event).unwrap();
7806 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7807
7808 let main_agent_event: SessionEvent = serde_json::from_value(json!({
7810 "id": "evt-2",
7811 "timestamp": "2026-04-30T12:00:01Z",
7812 "parentId": null,
7813 "type": "session.idle",
7814 "data": {}
7815 }))
7816 .unwrap();
7817 assert!(main_agent_event.agent_id.is_none());
7818 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
7819 assert!(roundtripped.get("agentId").is_none());
7820 }
7821
7822 #[test]
7824 fn typed_session_event_round_trips_agent_id_on_envelope() {
7825 let wire = json!({
7826 "id": "evt-1",
7827 "timestamp": "2026-04-30T12:00:00Z",
7828 "parentId": null,
7829 "agentId": "sub-agent-42",
7830 "type": "session.idle",
7831 "data": {}
7832 });
7833
7834 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
7835 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
7836
7837 let roundtripped = serde_json::to_value(&event).unwrap();
7838 assert_eq!(roundtripped["agentId"], "sub-agent-42");
7839 }
7840
7841 #[test]
7842 fn connection_state_variants_compile() {
7843 let _ = ConnectionState::Disconnected;
7847 let _ = ConnectionState::Connecting;
7848 let _ = ConnectionState::Connected;
7849 let _ = ConnectionState::Error;
7850 }
7851
7852 #[test]
7853 fn deserializes_runtime_attachment_variants() {
7854 let attachments: Vec<Attachment> = serde_json::from_value(json!([
7855 {
7856 "type": "file",
7857 "path": "/tmp/file.rs",
7858 "displayName": "file.rs",
7859 "lineRange": { "start": 7, "end": 12 }
7860 },
7861 {
7862 "type": "directory",
7863 "path": "/tmp/project",
7864 "displayName": "project"
7865 },
7866 {
7867 "type": "selection",
7868 "filePath": "/tmp/lib.rs",
7869 "displayName": "lib.rs",
7870 "text": "fn main() {}",
7871 "selection": {
7872 "start": { "line": 1, "character": 2 },
7873 "end": { "line": 3, "character": 4 }
7874 }
7875 },
7876 {
7877 "type": "blob",
7878 "data": "Zm9v",
7879 "mimeType": "image/png",
7880 "displayName": "image.png"
7881 },
7882 {
7883 "type": "github_reference",
7884 "number": 42,
7885 "title": "Fix rendering",
7886 "referenceType": "issue",
7887 "state": "open",
7888 "url": "https://github.com/example/repo/issues/42"
7889 }
7890 ]))
7891 .expect("attachments should deserialize");
7892
7893 assert_eq!(attachments.len(), 5);
7894 assert!(matches!(
7895 &attachments[0],
7896 Attachment::File {
7897 path,
7898 display_name,
7899 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
7900 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
7901 ));
7902 assert!(matches!(
7903 &attachments[1],
7904 Attachment::Directory { path, display_name }
7905 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
7906 ));
7907 assert!(matches!(
7908 &attachments[2],
7909 Attachment::Selection {
7910 file_path,
7911 display_name,
7912 selection:
7913 AttachmentSelectionRange {
7914 start: AttachmentSelectionPosition { line: 1, character: 2 },
7915 end: AttachmentSelectionPosition { line: 3, character: 4 },
7916 },
7917 ..
7918 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
7919 ));
7920 assert!(matches!(
7921 &attachments[3],
7922 Attachment::Blob {
7923 data,
7924 mime_type,
7925 display_name,
7926 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
7927 ));
7928 assert!(matches!(
7929 &attachments[4],
7930 Attachment::GitHubReference {
7931 number: 42,
7932 title,
7933 reference_type: GitHubReferenceType::Issue,
7934 state,
7935 url,
7936 } if title == "Fix rendering"
7937 && state == "open"
7938 && url == "https://github.com/example/repo/issues/42"
7939 ));
7940 }
7941
7942 #[test]
7943 fn ensures_display_names_for_variants_that_support_them() {
7944 let mut attachments = vec![
7945 Attachment::File {
7946 path: PathBuf::from("/tmp/file.rs"),
7947 display_name: None,
7948 line_range: None,
7949 },
7950 Attachment::Selection {
7951 file_path: PathBuf::from("/tmp/src/lib.rs"),
7952 display_name: None,
7953 text: "fn main() {}".to_string(),
7954 selection: AttachmentSelectionRange {
7955 start: AttachmentSelectionPosition {
7956 line: 0,
7957 character: 0,
7958 },
7959 end: AttachmentSelectionPosition {
7960 line: 0,
7961 character: 10,
7962 },
7963 },
7964 },
7965 Attachment::Blob {
7966 data: "Zm9v".to_string(),
7967 mime_type: "image/png".to_string(),
7968 display_name: None,
7969 },
7970 Attachment::GitHubReference {
7971 number: 7,
7972 title: "Track regressions".to_string(),
7973 reference_type: GitHubReferenceType::Issue,
7974 state: "open".to_string(),
7975 url: "https://example.com/issues/7".to_string(),
7976 },
7977 ];
7978
7979 ensure_attachment_display_names(&mut attachments);
7980
7981 assert_eq!(attachments[0].display_name(), Some("file.rs"));
7982 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
7983 assert_eq!(attachments[2].display_name(), Some("attachment"));
7984 assert_eq!(attachments[3].display_name(), None);
7985 assert_eq!(
7986 attachments[3].label(),
7987 Some("Track regressions".to_string())
7988 );
7989 }
7990
7991 #[test]
7992 fn github_anchored_attachment_variants_round_trip() {
7993 let cases = vec![
7994 (
7995 "github_commit",
7996 json!({
7997 "type": "github_commit",
7998 "message": "Fix the thing",
7999 "oid": "abc123",
8000 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
8001 "url": "https://github.com/octocat/repo/commit/abc123"
8002 }),
8003 ),
8004 (
8005 "github_release",
8006 json!({
8007 "type": "github_release",
8008 "name": "v1.2.3",
8009 "repo": { "name": "repo", "owner": "octocat" },
8010 "tagName": "v1.2.3",
8011 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
8012 }),
8013 ),
8014 (
8015 "github_actions_job",
8016 json!({
8017 "type": "github_actions_job",
8018 "conclusion": "failure",
8019 "jobId": 99,
8020 "jobName": "build",
8021 "repo": { "name": "repo", "owner": "octocat" },
8022 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
8023 "workflowName": "CI"
8024 }),
8025 ),
8026 (
8027 "github_repository",
8028 json!({
8029 "type": "github_repository",
8030 "description": "An example repository",
8031 "ref": "main",
8032 "repo": { "name": "repo", "owner": "octocat" },
8033 "url": "https://github.com/octocat/repo"
8034 }),
8035 ),
8036 (
8037 "github_file_diff",
8038 json!({
8039 "type": "github_file_diff",
8040 "base": {
8041 "path": "src/lib.rs",
8042 "ref": "main",
8043 "repo": { "name": "repo", "owner": "octocat" }
8044 },
8045 "head": {
8046 "path": "src/lib.rs",
8047 "ref": "feature",
8048 "repo": { "name": "repo", "owner": "octocat" }
8049 },
8050 "url": "https://github.com/octocat/repo/compare/main...feature"
8051 }),
8052 ),
8053 (
8054 "github_tree_comparison",
8055 json!({
8056 "type": "github_tree_comparison",
8057 "base": {
8058 "repo": { "name": "repo", "owner": "octocat" },
8059 "revision": "main"
8060 },
8061 "head": {
8062 "repo": { "name": "repo", "owner": "octocat" },
8063 "revision": "feature"
8064 },
8065 "url": "https://github.com/octocat/repo/compare/main...feature"
8066 }),
8067 ),
8068 (
8069 "github_url",
8070 json!({
8071 "type": "github_url",
8072 "url": "https://github.com/octocat/repo/wiki"
8073 }),
8074 ),
8075 (
8076 "github_file",
8077 json!({
8078 "type": "github_file",
8079 "path": "src/main.rs",
8080 "ref": "main",
8081 "repo": { "name": "repo", "owner": "octocat" },
8082 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
8083 }),
8084 ),
8085 (
8086 "github_snippet",
8087 json!({
8088 "type": "github_snippet",
8089 "lineRange": { "start": 10, "end": 20 },
8090 "path": "src/main.rs",
8091 "ref": "main",
8092 "repo": { "name": "repo", "owner": "octocat" },
8093 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
8094 }),
8095 ),
8096 ];
8097
8098 for (expected_type, input) in cases {
8099 let attachment: Attachment = serde_json::from_value(input.clone())
8100 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
8101
8102 let serialized_string = serde_json::to_string(&attachment)
8107 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
8108
8109 assert_eq!(
8111 serialized_string.matches("\"type\":").count(),
8112 1,
8113 "{expected_type} must serialize a single `type` key"
8114 );
8115
8116 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
8117 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
8118 assert_eq!(
8119 serialized.get("type").and_then(|value| value.as_str()),
8120 Some(expected_type),
8121 "{expected_type} must serialize the correct discriminator"
8122 );
8123
8124 assert_eq!(
8126 serialized, input,
8127 "{expected_type} should round-trip without data loss"
8128 );
8129 let reparsed: Attachment = serde_json::from_value(serialized)
8130 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
8131 assert_eq!(
8132 reparsed, attachment,
8133 "{expected_type} should re-deserialize to the same value"
8134 );
8135 }
8136 }
8137}
8138
8139#[cfg(test)]
8140mod permission_builder_tests {
8141 use std::sync::Arc;
8142
8143 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
8144 use crate::permission;
8145 use crate::types::{
8146 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
8147 SessionId,
8148 };
8149
8150 fn data() -> PermissionRequestData {
8151 PermissionRequestData {
8152 extra: serde_json::json!({"tool": "shell"}),
8153 ..Default::default()
8154 }
8155 }
8156
8157 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
8160 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
8161 }
8162
8163 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
8164 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
8165 }
8166
8167 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
8168 handler
8169 .handle(SessionId::from("s1"), RequestId::new("1"), data())
8170 .await
8171 }
8172
8173 #[tokio::test]
8174 async fn approve_all_with_handler_present_approves() {
8175 let cfg = SessionConfig::default()
8176 .with_permission_handler(Arc::new(ApproveAllHandler))
8177 .approve_all_permissions();
8178 let h = resolve_create(cfg).expect("policy + handler yields handler");
8179 assert!(matches!(
8180 dispatch(&h).await,
8181 PermissionResult::Decision {
8182 decision: PermissionDecision::ApproveOnce(_),
8183 ..
8184 }
8185 ));
8186 }
8187
8188 #[tokio::test]
8189 async fn approve_all_standalone_produces_handler() {
8190 let cfg = SessionConfig::default().approve_all_permissions();
8191 let h = resolve_create(cfg).expect("policy alone yields handler");
8192 assert!(matches!(
8193 dispatch(&h).await,
8194 PermissionResult::Decision {
8195 decision: PermissionDecision::ApproveOnce(_),
8196 ..
8197 }
8198 ));
8199 }
8200
8201 #[tokio::test]
8204 async fn approve_all_is_order_independent() {
8205 let a = SessionConfig::default()
8206 .with_permission_handler(Arc::new(ApproveAllHandler))
8207 .approve_all_permissions();
8208 let b = SessionConfig::default()
8209 .approve_all_permissions()
8210 .with_permission_handler(Arc::new(ApproveAllHandler));
8211 let ha = resolve_create(a).unwrap();
8212 let hb = resolve_create(b).unwrap();
8213 assert!(matches!(
8214 dispatch(&ha).await,
8215 PermissionResult::Decision {
8216 decision: PermissionDecision::ApproveOnce(_),
8217 ..
8218 }
8219 ));
8220 assert!(matches!(
8221 dispatch(&hb).await,
8222 PermissionResult::Decision {
8223 decision: PermissionDecision::ApproveOnce(_),
8224 ..
8225 }
8226 ));
8227 }
8228
8229 #[tokio::test]
8230 async fn deny_all_is_order_independent() {
8231 let a = SessionConfig::default()
8232 .with_permission_handler(Arc::new(ApproveAllHandler))
8233 .deny_all_permissions();
8234 let b = SessionConfig::default()
8235 .deny_all_permissions()
8236 .with_permission_handler(Arc::new(ApproveAllHandler));
8237 let ha = resolve_create(a).unwrap();
8238 let hb = resolve_create(b).unwrap();
8239 assert!(matches!(
8240 dispatch(&ha).await,
8241 PermissionResult::Decision {
8242 decision: PermissionDecision::Reject(_),
8243 ..
8244 }
8245 ));
8246 assert!(matches!(
8247 dispatch(&hb).await,
8248 PermissionResult::Decision {
8249 decision: PermissionDecision::Reject(_),
8250 ..
8251 }
8252 ));
8253 }
8254
8255 #[tokio::test]
8256 async fn approve_permissions_if_consults_predicate() {
8257 let cfg = SessionConfig::default().approve_permissions_if(|d| {
8258 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
8259 });
8260 let h = resolve_create(cfg).unwrap();
8261 assert!(matches!(
8262 dispatch(&h).await,
8263 PermissionResult::Decision {
8264 decision: PermissionDecision::Reject(_),
8265 ..
8266 }
8267 ));
8268 }
8269
8270 #[tokio::test]
8271 async fn approve_permissions_if_is_order_independent() {
8272 let predicate = |d: &PermissionRequestData| {
8273 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
8274 };
8275 let a = SessionConfig::default()
8276 .with_permission_handler(Arc::new(ApproveAllHandler))
8277 .approve_permissions_if(predicate);
8278 let b = SessionConfig::default()
8279 .approve_permissions_if(predicate)
8280 .with_permission_handler(Arc::new(ApproveAllHandler));
8281 let ha = resolve_create(a).unwrap();
8282 let hb = resolve_create(b).unwrap();
8283 assert!(matches!(
8284 dispatch(&ha).await,
8285 PermissionResult::Decision {
8286 decision: PermissionDecision::Reject(_),
8287 ..
8288 }
8289 ));
8290 assert!(matches!(
8291 dispatch(&hb).await,
8292 PermissionResult::Decision {
8293 decision: PermissionDecision::Reject(_),
8294 ..
8295 }
8296 ));
8297 }
8298
8299 #[tokio::test]
8300 async fn resume_session_config_approve_all_works() {
8301 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
8302 .with_permission_handler(Arc::new(ApproveAllHandler))
8303 .approve_all_permissions();
8304 let h = resolve_resume(cfg).unwrap();
8305 assert!(matches!(
8306 dispatch(&h).await,
8307 PermissionResult::Decision {
8308 decision: PermissionDecision::ApproveOnce(_),
8309 ..
8310 }
8311 ));
8312 }
8313
8314 #[tokio::test]
8315 async fn resume_session_config_approve_all_is_order_independent() {
8316 let a = ResumeSessionConfig::new(SessionId::from("s1"))
8317 .with_permission_handler(Arc::new(ApproveAllHandler))
8318 .approve_all_permissions();
8319 let b = ResumeSessionConfig::new(SessionId::from("s1"))
8320 .approve_all_permissions()
8321 .with_permission_handler(Arc::new(ApproveAllHandler));
8322 let ha = resolve_resume(a).unwrap();
8323 let hb = resolve_resume(b).unwrap();
8324 assert!(matches!(
8325 dispatch(&ha).await,
8326 PermissionResult::Decision {
8327 decision: PermissionDecision::ApproveOnce(_),
8328 ..
8329 }
8330 ));
8331 assert!(matches!(
8332 dispatch(&hb).await,
8333 PermissionResult::Decision {
8334 decision: PermissionDecision::ApproveOnce(_),
8335 ..
8336 }
8337 ));
8338 }
8339
8340 #[test]
8341 fn session_config_enable_experimental_mode_serializes_when_set() {
8342 let cfg = SessionConfig::default().with_enable_experimental_mode(false);
8343 assert_eq!(cfg.enable_experimental_mode, Some(false));
8344
8345 let (wire, _runtime) = cfg
8346 .into_wire(Some(SessionId::from("experimental-mode")))
8347 .expect("enable_experimental_mode config has no duplicate handlers");
8348 assert_eq!(wire.is_experimental_mode, Some(false));
8349
8350 let json = serde_json::to_value(&wire).unwrap();
8351 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
8352 }
8353
8354 #[test]
8355 fn session_config_enable_experimental_mode_omitted_when_none() {
8356 let cfg = SessionConfig::default();
8357 assert_eq!(cfg.enable_experimental_mode, None);
8358
8359 let (wire, _runtime) = cfg
8360 .into_wire(Some(SessionId::from("no-experimental-mode")))
8361 .expect("default config has no duplicate handlers");
8362 assert_eq!(wire.is_experimental_mode, None);
8363
8364 let json = serde_json::to_value(&wire).unwrap();
8365 assert!(json.get("isExperimentalMode").is_none());
8366 }
8367
8368 #[test]
8369 fn resume_session_config_enable_experimental_mode_serializes_when_set() {
8370 let cfg = ResumeSessionConfig::new(SessionId::from("resume-experimental-mode"))
8371 .with_enable_experimental_mode(false);
8372 assert_eq!(cfg.enable_experimental_mode, Some(false));
8373
8374 let (wire, _runtime) = cfg
8375 .into_wire()
8376 .expect("resume enable_experimental_mode config has no duplicate handlers");
8377 assert_eq!(wire.is_experimental_mode, Some(false));
8378
8379 let json = serde_json::to_value(&wire).unwrap();
8380 assert_eq!(json["isExperimentalMode"], serde_json::Value::Bool(false));
8381 }
8382
8383 #[test]
8384 fn resume_session_config_enable_experimental_mode_omitted_when_none() {
8385 let cfg = ResumeSessionConfig::new(SessionId::from("resume-no-experimental-mode"));
8386 assert_eq!(cfg.enable_experimental_mode, None);
8387
8388 let (wire, _runtime) = cfg
8389 .into_wire()
8390 .expect("default resume config has no duplicate handlers");
8391 assert_eq!(wire.is_experimental_mode, None);
8392
8393 let json = serde_json::to_value(&wire).unwrap();
8394 assert!(json.get("isExperimentalMode").is_none());
8395 }
8396}
8397
8398#[cfg(test)]
8399mod is_terminal_tests {
8400 use super::Tool;
8401
8402 #[test]
8403 fn is_terminal_serializes_as_camel_case_when_set() {
8404 let tool = Tool {
8405 name: "clear_context".to_owned(),
8406 is_terminal: true,
8407 ..Default::default()
8408 };
8409 let value = serde_json::to_value(&tool).expect("tool serializes");
8410 assert_eq!(
8411 value.get("isTerminal"),
8412 Some(&serde_json::Value::Bool(true))
8413 );
8414 }
8415
8416 #[test]
8417 fn is_terminal_is_omitted_when_false() {
8418 let tool = Tool {
8419 name: "plain".to_owned(),
8420 ..Default::default()
8421 };
8422 let value = serde_json::to_value(&tool).expect("tool serializes");
8423 assert!(value.get("isTerminal").is_none());
8424 }
8425
8426 #[test]
8429 fn is_terminal_appears_in_debug_output() {
8430 let terminal = Tool {
8431 name: "clear_context".to_owned(),
8432 is_terminal: true,
8433 ..Default::default()
8434 };
8435 assert!(format!("{terminal:?}").contains("is_terminal: true"));
8436
8437 let plain = Tool {
8438 name: "plain".to_owned(),
8439 ..Default::default()
8440 };
8441 assert!(format!("{plain:?}").contains("is_terminal: false"));
8442 }
8443}