1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11
12use indexmap::IndexMap;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16use crate::canvas::{CanvasDeclaration, CanvasHandler};
17pub use crate::copilot_request_handler::{
18 CopilotHttpRequest, CopilotHttpResponse, CopilotHttpResponseBody, CopilotRequestContext,
19 CopilotRequestError, CopilotRequestHandler, CopilotRequestTransport, CopilotWebSocketForwarder,
20 CopilotWebSocketForwarderBuilder, CopilotWebSocketHandler, CopilotWebSocketMessage,
21 CopilotWebSocketResponse, WebSocketTransform, forward_http,
22};
23use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance};
24use crate::generated::session_events::ReasoningSummary;
25pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig};
27use crate::handler::{
28 AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler,
29 PermissionHandler, UserInputHandler,
30};
31use crate::hooks::SessionHooks;
32use crate::provider_token::BearerTokenProvider;
33pub use crate::session_fs::{
34 DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig,
35 SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult,
36 SessionFsSqliteQueryType,
37};
38pub use crate::trace_context::{TraceContext, TraceContextProvider};
39use crate::transforms::SystemMessageTransform;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44#[allow(dead_code)]
45#[non_exhaustive]
46pub(crate) enum ConnectionState {
47 Disconnected,
49 Connecting,
51 Connected,
53 Error,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
62#[non_exhaustive]
63pub enum SessionLifecycleEventType {
64 #[serde(rename = "session.created")]
66 Created,
67 #[serde(rename = "session.deleted")]
69 Deleted,
70 #[serde(rename = "session.updated")]
72 Updated,
73 #[serde(rename = "session.foreground")]
75 Foreground,
76 #[serde(rename = "session.background")]
78 Background,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct SessionLifecycleEventMetadata {
84 #[serde(rename = "startTime")]
86 pub start_time: String,
87 #[serde(rename = "modifiedTime")]
89 pub modified_time: String,
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub summary: Option<String>,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98pub struct SessionLifecycleEvent {
99 #[serde(rename = "type")]
101 pub event_type: SessionLifecycleEventType,
102 #[serde(rename = "sessionId")]
104 pub session_id: SessionId,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub metadata: Option<SessionLifecycleEventMetadata>,
108}
109
110#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
116#[serde(transparent)]
117pub struct SessionId(String);
118
119impl SessionId {
120 pub fn new(id: impl Into<String>) -> Self {
122 Self(id.into())
123 }
124
125 pub fn as_str(&self) -> &str {
127 &self.0
128 }
129
130 pub fn into_inner(self) -> String {
132 self.0
133 }
134}
135
136impl std::ops::Deref for SessionId {
137 type Target = str;
138
139 fn deref(&self) -> &str {
140 &self.0
141 }
142}
143
144impl std::fmt::Display for SessionId {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 f.write_str(&self.0)
147 }
148}
149
150impl From<String> for SessionId {
151 fn from(s: String) -> Self {
152 Self(s)
153 }
154}
155
156impl From<&str> for SessionId {
157 fn from(s: &str) -> Self {
158 Self(s.to_owned())
159 }
160}
161
162impl AsRef<str> for SessionId {
163 fn as_ref(&self) -> &str {
164 &self.0
165 }
166}
167
168impl std::borrow::Borrow<str> for SessionId {
169 fn borrow(&self) -> &str {
170 &self.0
171 }
172}
173
174impl From<SessionId> for String {
175 fn from(id: SessionId) -> String {
176 id.0
177 }
178}
179
180impl PartialEq<str> for SessionId {
181 fn eq(&self, other: &str) -> bool {
182 self.0 == other
183 }
184}
185
186impl PartialEq<String> for SessionId {
187 fn eq(&self, other: &String) -> bool {
188 &self.0 == other
189 }
190}
191
192impl PartialEq<SessionId> for String {
193 fn eq(&self, other: &SessionId) -> bool {
194 self == &other.0
195 }
196}
197
198impl PartialEq<&str> for SessionId {
199 fn eq(&self, other: &&str) -> bool {
200 self.0 == *other
201 }
202}
203
204impl PartialEq<&SessionId> for SessionId {
205 fn eq(&self, other: &&SessionId) -> bool {
206 self.0 == other.0
207 }
208}
209
210impl PartialEq<SessionId> for &SessionId {
211 fn eq(&self, other: &SessionId) -> bool {
212 self.0 == other.0
213 }
214}
215
216#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
222#[serde(transparent)]
223pub struct RequestId(String);
224
225impl RequestId {
226 pub fn new(id: impl Into<String>) -> Self {
228 Self(id.into())
229 }
230
231 pub fn into_inner(self) -> String {
233 self.0
234 }
235}
236
237impl std::ops::Deref for RequestId {
238 type Target = str;
239
240 fn deref(&self) -> &str {
241 &self.0
242 }
243}
244
245impl std::fmt::Display for RequestId {
246 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 f.write_str(&self.0)
248 }
249}
250
251impl From<String> for RequestId {
252 fn from(s: String) -> Self {
253 Self(s)
254 }
255}
256
257impl From<&str> for RequestId {
258 fn from(s: &str) -> Self {
259 Self(s.to_owned())
260 }
261}
262
263impl AsRef<str> for RequestId {
264 fn as_ref(&self) -> &str {
265 &self.0
266 }
267}
268
269impl std::borrow::Borrow<str> for RequestId {
270 fn borrow(&self) -> &str {
271 &self.0
272 }
273}
274
275impl From<RequestId> for String {
276 fn from(id: RequestId) -> String {
277 id.0
278 }
279}
280
281impl PartialEq<str> for RequestId {
282 fn eq(&self, other: &str) -> bool {
283 self.0 == other
284 }
285}
286
287impl PartialEq<String> for RequestId {
288 fn eq(&self, other: &String) -> bool {
289 &self.0 == other
290 }
291}
292
293impl PartialEq<RequestId> for String {
294 fn eq(&self, other: &RequestId) -> bool {
295 self == &other.0
296 }
297}
298
299impl PartialEq<&str> for RequestId {
300 fn eq(&self, other: &&str) -> bool {
301 self.0 == *other
302 }
303}
304
305#[derive(Clone, Default, Serialize, Deserialize)]
320#[serde(rename_all = "camelCase")]
321#[non_exhaustive]
322pub struct Tool {
323 pub name: String,
325 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub namespaced_name: Option<String>,
329 #[serde(default)]
331 pub description: String,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub instructions: Option<String>,
335 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
337 pub parameters: IndexMap<String, Value>,
338 #[serde(default, skip_serializing_if = "is_false")]
342 pub overrides_built_in_tool: bool,
343 #[serde(default, skip_serializing_if = "is_false")]
347 pub skip_permission: bool,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
354 pub defer: Option<DeferMode>,
355 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
360 pub metadata: IndexMap<String, Value>,
361 #[serde(skip)]
373 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
374}
375
376#[inline]
377fn is_false(b: &bool) -> bool {
378 !*b
379}
380
381#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
384#[serde(rename_all = "lowercase")]
385pub enum DeferMode {
386 Auto,
388 Never,
390}
391
392impl Tool {
393 pub fn new(name: impl Into<String>) -> Self {
413 Self {
414 name: name.into(),
415 ..Default::default()
416 }
417 }
418
419 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
422 self.namespaced_name = Some(namespaced_name.into());
423 self
424 }
425
426 pub fn with_description(mut self, description: impl Into<String>) -> Self {
428 self.description = description.into();
429 self
430 }
431
432 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
434 self.instructions = Some(instructions.into());
435 self
436 }
437
438 pub fn with_parameters(mut self, parameters: Value) -> Self {
452 self.parameters = crate::tool::tool_parameters(parameters);
453 self
454 }
455
456 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
460 self.overrides_built_in_tool = overrides;
461 self
462 }
463
464 pub fn with_skip_permission(mut self, skip: bool) -> Self {
468 self.skip_permission = skip;
469 self
470 }
471
472 pub fn with_defer(mut self, defer: DeferMode) -> Self {
476 self.defer = Some(defer);
477 self
478 }
479
480 pub fn with_metadata(mut self, metadata: IndexMap<String, Value>) -> Self {
483 self.metadata = metadata;
484 self
485 }
486
487 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
491 self.handler = Some(handler);
492 self
493 }
494
495 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
500 self.handler.as_ref()
501 }
502}
503
504impl std::fmt::Debug for Tool {
505 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
506 f.debug_struct("Tool")
507 .field("name", &self.name)
508 .field("namespaced_name", &self.namespaced_name)
509 .field("description", &self.description)
510 .field("instructions", &self.instructions)
511 .field("parameters", &self.parameters)
512 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
513 .field("skip_permission", &self.skip_permission)
514 .field("defer", &self.defer)
515 .field("metadata", &self.metadata)
516 .field(
517 "handler",
518 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
519 )
520 .finish()
521 }
522}
523
524#[non_exhaustive]
527#[derive(Debug, Clone)]
528pub struct CommandContext {
529 pub session_id: SessionId,
531 pub command: String,
533 pub command_name: String,
535 pub args: String,
537}
538
539#[async_trait::async_trait]
545pub trait CommandHandler: Send + Sync {
546 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
548}
549
550#[non_exhaustive]
556#[derive(Clone)]
557pub struct CommandDefinition {
558 pub name: String,
560 pub description: Option<String>,
562 pub handler: Arc<dyn CommandHandler>,
564}
565
566impl CommandDefinition {
567 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
570 Self {
571 name: name.into(),
572 description: None,
573 handler,
574 }
575 }
576
577 pub fn with_description(mut self, description: impl Into<String>) -> Self {
579 self.description = Some(description.into());
580 self
581 }
582}
583
584impl std::fmt::Debug for CommandDefinition {
585 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
586 f.debug_struct("CommandDefinition")
587 .field("name", &self.name)
588 .field("description", &self.description)
589 .field("handler", &"<set>")
590 .finish()
591 }
592}
593
594impl Serialize for CommandDefinition {
595 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
596 use serde::ser::SerializeStruct;
597 let len = if self.description.is_some() { 2 } else { 1 };
598 let mut state = serializer.serialize_struct("CommandDefinition", len)?;
599 state.serialize_field("name", &self.name)?;
600 if let Some(description) = &self.description {
601 state.serialize_field("description", description)?;
602 }
603 state.end()
604 }
605}
606
607#[derive(Debug, Clone, Default, Serialize, Deserialize)]
614#[serde(rename_all = "camelCase")]
615#[non_exhaustive]
616pub struct CustomAgentConfig {
617 pub name: String,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
621 pub display_name: Option<String>,
622 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub description: Option<String>,
625 #[serde(default, skip_serializing_if = "Option::is_none")]
627 pub tools: Option<Vec<String>>,
628 pub prompt: String,
630 #[serde(default, skip_serializing_if = "Option::is_none")]
632 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
633 #[serde(default, skip_serializing_if = "Option::is_none")]
635 pub infer: Option<bool>,
636 #[serde(default, skip_serializing_if = "Option::is_none")]
638 pub skills: Option<Vec<String>>,
639 #[serde(default, skip_serializing_if = "Option::is_none")]
644 pub model: Option<String>,
645}
646
647impl CustomAgentConfig {
648 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
655 Self {
656 name: name.into(),
657 prompt: prompt.into(),
658 ..Self::default()
659 }
660 }
661
662 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
664 self.display_name = Some(display_name.into());
665 self
666 }
667
668 pub fn with_description(mut self, description: impl Into<String>) -> Self {
670 self.description = Some(description.into());
671 self
672 }
673
674 pub fn with_tools<I, S>(mut self, tools: I) -> Self
677 where
678 I: IntoIterator<Item = S>,
679 S: Into<String>,
680 {
681 self.tools = Some(tools.into_iter().map(Into::into).collect());
682 self
683 }
684
685 pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
687 self.mcp_servers = Some(mcp_servers);
688 self
689 }
690
691 pub fn with_infer(mut self, infer: bool) -> Self {
693 self.infer = Some(infer);
694 self
695 }
696
697 pub fn with_skills<I, S>(mut self, skills: I) -> Self
699 where
700 I: IntoIterator<Item = S>,
701 S: Into<String>,
702 {
703 self.skills = Some(skills.into_iter().map(Into::into).collect());
704 self
705 }
706
707 pub fn with_model(mut self, model: impl Into<String>) -> Self {
709 self.model = Some(model.into());
710 self
711 }
712}
713
714#[derive(Debug, Clone, Default, Serialize, Deserialize)]
721#[serde(rename_all = "camelCase")]
722pub struct DefaultAgentConfig {
723 #[serde(default, skip_serializing_if = "Option::is_none")]
725 pub excluded_tools: Option<Vec<String>>,
726}
727
728#[derive(Debug, Clone, Default, Serialize, Deserialize)]
734#[serde(rename_all = "camelCase")]
735#[non_exhaustive]
736pub struct LargeToolOutputConfig {
737 #[serde(default, skip_serializing_if = "Option::is_none")]
739 pub enabled: Option<bool>,
740 #[serde(default, skip_serializing_if = "Option::is_none")]
743 pub max_size_bytes: Option<u64>,
744 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
747 pub output_directory: Option<PathBuf>,
748}
749
750impl LargeToolOutputConfig {
751 pub fn new() -> Self {
754 Self::default()
755 }
756
757 pub fn with_enabled(mut self, enabled: bool) -> Self {
759 self.enabled = Some(enabled);
760 self
761 }
762
763 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
765 self.max_size_bytes = Some(max_size_bytes);
766 self
767 }
768
769 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
771 self.output_directory = Some(output_directory.into());
772 self
773 }
774}
775
776#[derive(Debug, Clone, Default, Serialize, Deserialize)]
782#[serde(rename_all = "camelCase")]
783#[non_exhaustive]
784pub struct ToolSearchConfig {
785 #[serde(default, skip_serializing_if = "Option::is_none")]
787 pub enabled: Option<bool>,
788 #[serde(default, skip_serializing_if = "Option::is_none")]
791 pub defer_threshold: Option<u32>,
792}
793
794impl ToolSearchConfig {
795 pub fn new() -> Self {
798 Self::default()
799 }
800
801 pub fn with_enabled(mut self, enabled: bool) -> Self {
803 self.enabled = Some(enabled);
804 self
805 }
806
807 pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
810 self.defer_threshold = Some(defer_threshold);
811 self
812 }
813}
814
815#[derive(Debug, Clone, Default, Serialize, Deserialize)]
822#[serde(rename_all = "camelCase")]
823#[non_exhaustive]
824pub struct InfiniteSessionConfig {
825 #[serde(default, skip_serializing_if = "Option::is_none")]
827 pub enabled: Option<bool>,
828 #[serde(default, skip_serializing_if = "Option::is_none")]
831 pub background_compaction_threshold: Option<f64>,
832 #[serde(default, skip_serializing_if = "Option::is_none")]
835 pub buffer_exhaustion_threshold: Option<f64>,
836}
837
838impl InfiniteSessionConfig {
839 pub fn new() -> Self {
842 Self::default()
843 }
844
845 pub fn with_enabled(mut self, enabled: bool) -> Self {
848 self.enabled = Some(enabled);
849 self
850 }
851
852 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
855 self.background_compaction_threshold = Some(threshold);
856 self
857 }
858
859 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
862 self.buffer_exhaustion_threshold = Some(threshold);
863 self
864 }
865}
866
867#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
878#[serde(rename_all = "camelCase")]
879#[non_exhaustive]
880pub struct MemoryConfiguration {
881 pub enabled: bool,
883}
884
885impl MemoryConfiguration {
886 pub fn enabled() -> Self {
888 Self { enabled: true }
889 }
890
891 pub fn disabled() -> Self {
893 Self { enabled: false }
894 }
895
896 pub fn with_enabled(mut self, enabled: bool) -> Self {
898 self.enabled = enabled;
899 self
900 }
901}
902
903#[derive(Debug, Clone, Serialize, Deserialize)]
905#[serde(rename_all = "camelCase")]
906#[non_exhaustive]
907pub struct CloudSessionRepository {
908 pub owner: String,
910 pub name: String,
912 #[serde(skip_serializing_if = "Option::is_none")]
914 pub branch: Option<String>,
915}
916
917impl CloudSessionRepository {
918 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
920 Self {
921 owner: owner.into(),
922 name: name.into(),
923 branch: None,
924 }
925 }
926
927 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
929 self.branch = Some(branch.into());
930 self
931 }
932}
933
934#[derive(Debug, Clone, Default, Serialize, Deserialize)]
936#[serde(rename_all = "camelCase")]
937#[non_exhaustive]
938pub struct CloudSessionOptions {
939 #[serde(skip_serializing_if = "Option::is_none")]
941 pub repository: Option<CloudSessionRepository>,
942}
943
944impl CloudSessionOptions {
945 pub fn with_repository(repository: CloudSessionRepository) -> Self {
947 Self {
948 repository: Some(repository),
949 }
950 }
951}
952
953#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
955#[serde(rename_all = "camelCase")]
956pub struct ExtensionInfo {
957 pub source: String,
959 pub name: String,
961}
962
963impl ExtensionInfo {
964 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
966 Self {
967 source: source.into(),
968 name: name.into(),
969 }
970 }
971}
972
973#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
984#[serde(rename_all = "camelCase")]
985pub struct CanvasProviderIdentity {
986 pub id: String,
988 #[serde(skip_serializing_if = "Option::is_none")]
990 pub name: Option<String>,
991}
992
993impl CanvasProviderIdentity {
994 pub fn new(id: impl Into<String>) -> Self {
996 Self {
997 id: id.into(),
998 name: None,
999 }
1000 }
1001
1002 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1004 self.name = Some(name.into());
1005 self
1006 }
1007}
1008
1009#[derive(Debug, Clone, Serialize, Deserialize)]
1043#[serde(tag = "type", rename_all = "lowercase")]
1044#[non_exhaustive]
1045pub enum McpServerConfig {
1046 #[serde(alias = "local")]
1050 Stdio(McpStdioServerConfig),
1051 Http(McpHttpServerConfig),
1053 Sse(McpHttpServerConfig),
1055}
1056
1057#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1061#[serde(rename_all = "camelCase")]
1062pub struct McpStdioServerConfig {
1063 #[serde(default, skip_serializing_if = "Option::is_none")]
1069 pub tools: Option<Vec<String>>,
1070 #[serde(default, skip_serializing_if = "Option::is_none")]
1072 pub timeout: Option<i64>,
1073 pub command: String,
1075 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1077 pub args: Vec<String>,
1078 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1081 pub env: HashMap<String, String>,
1082 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1084 pub working_directory: Option<String>,
1085}
1086
1087#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1091#[serde(rename_all = "camelCase")]
1092pub struct McpHttpServerConfig {
1093 #[serde(default, skip_serializing_if = "Option::is_none")]
1099 pub tools: Option<Vec<String>>,
1100 #[serde(default, skip_serializing_if = "Option::is_none")]
1102 pub timeout: Option<i64>,
1103 pub url: String,
1105 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1107 pub headers: HashMap<String, String>,
1108}
1109
1110#[derive(Clone, Default, Serialize, Deserialize)]
1116#[serde(rename_all = "camelCase")]
1117#[non_exhaustive]
1118pub struct ProviderConfig {
1119 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1122 pub provider_type: Option<String>,
1123 #[serde(default, skip_serializing_if = "Option::is_none")]
1126 pub wire_api: Option<String>,
1127 #[serde(default, skip_serializing_if = "Option::is_none")]
1132 pub transport: Option<String>,
1133 pub base_url: String,
1135 #[serde(default, skip_serializing_if = "Option::is_none")]
1137 pub api_key: Option<String>,
1138 #[serde(default, skip_serializing_if = "Option::is_none")]
1142 pub bearer_token: Option<String>,
1143 #[serde(skip)]
1146 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1147 #[serde(default, skip_serializing_if = "Option::is_none")]
1148 pub(crate) has_bearer_token_provider: Option<bool>,
1149 #[serde(default, skip_serializing_if = "Option::is_none")]
1151 pub azure: Option<AzureProviderOptions>,
1152 #[serde(default, skip_serializing_if = "Option::is_none")]
1154 pub headers: Option<HashMap<String, String>>,
1155 #[serde(default, skip_serializing_if = "Option::is_none")]
1159 pub model_id: Option<String>,
1160 #[serde(default, skip_serializing_if = "Option::is_none")]
1167 pub wire_model: Option<String>,
1168 #[serde(default, skip_serializing_if = "Option::is_none")]
1173 pub max_prompt_tokens: Option<i64>,
1174 #[serde(default, skip_serializing_if = "Option::is_none")]
1177 pub max_output_tokens: Option<i64>,
1178}
1179
1180impl std::fmt::Debug for ProviderConfig {
1181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1182 f.debug_struct("ProviderConfig")
1183 .field("provider_type", &self.provider_type)
1184 .field("wire_api", &self.wire_api)
1185 .field("transport", &self.transport)
1186 .field("base_url", &self.base_url)
1187 .field("api_key", &self.api_key)
1188 .field("bearer_token", &self.bearer_token)
1189 .field(
1190 "bearer_token_provider",
1191 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1192 )
1193 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1194 .field("azure", &self.azure)
1195 .field("headers", &self.headers)
1196 .field("model_id", &self.model_id)
1197 .field("wire_model", &self.wire_model)
1198 .field("max_prompt_tokens", &self.max_prompt_tokens)
1199 .field("max_output_tokens", &self.max_output_tokens)
1200 .finish()
1201 }
1202}
1203
1204impl ProviderConfig {
1205 pub fn new(base_url: impl Into<String>) -> Self {
1208 Self {
1209 base_url: base_url.into(),
1210 ..Self::default()
1211 }
1212 }
1213
1214 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1216 self.provider_type = Some(provider_type.into());
1217 self
1218 }
1219
1220 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1222 self.wire_api = Some(wire_api.into());
1223 self
1224 }
1225
1226 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1229 self.transport = Some(transport.into());
1230 self
1231 }
1232
1233 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1235 self.api_key = Some(api_key.into());
1236 self
1237 }
1238
1239 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1242 self.bearer_token = Some(bearer_token.into());
1243 self
1244 }
1245
1246 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1252 self.bearer_token_provider = Some(provider);
1253 self
1254 }
1255
1256 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1258 self.azure = Some(azure);
1259 self
1260 }
1261
1262 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1264 self.headers = Some(headers);
1265 self
1266 }
1267
1268 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1271 self.model_id = Some(model_id.into());
1272 self
1273 }
1274
1275 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1280 self.wire_model = Some(wire_model.into());
1281 self
1282 }
1283
1284 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1288 self.max_prompt_tokens = Some(max);
1289 self
1290 }
1291
1292 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1295 self.max_output_tokens = Some(max);
1296 self
1297 }
1298}
1299
1300#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1313#[serde(rename_all = "camelCase")]
1314#[non_exhaustive]
1315pub struct CapiSessionOptions {
1316 #[serde(default, skip_serializing_if = "Option::is_none")]
1322 pub enable_web_socket_responses: Option<bool>,
1323}
1324
1325impl CapiSessionOptions {
1326 pub fn new() -> Self {
1328 Self::default()
1329 }
1330
1331 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1333 self.enable_web_socket_responses = Some(enable);
1334 self
1335 }
1336}
1337
1338#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1340#[serde(rename_all = "camelCase")]
1341pub struct AzureProviderOptions {
1342 #[serde(default, skip_serializing_if = "Option::is_none")]
1344 pub api_version: Option<String>,
1345}
1346
1347#[derive(Clone, Default, Serialize, Deserialize)]
1358#[serde(rename_all = "camelCase")]
1359#[non_exhaustive]
1360pub struct NamedProviderConfig {
1361 pub name: String,
1364 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1367 pub provider_type: Option<String>,
1368 #[serde(default, skip_serializing_if = "Option::is_none")]
1371 pub wire_api: Option<String>,
1372 pub base_url: String,
1374 #[serde(default, skip_serializing_if = "Option::is_none")]
1376 pub api_key: Option<String>,
1377 #[serde(default, skip_serializing_if = "Option::is_none")]
1380 pub bearer_token: Option<String>,
1381 #[serde(skip)]
1384 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1385 #[serde(default, skip_serializing_if = "Option::is_none")]
1386 pub(crate) has_bearer_token_provider: Option<bool>,
1387 #[serde(default, skip_serializing_if = "Option::is_none")]
1389 pub azure: Option<AzureProviderOptions>,
1390 #[serde(default, skip_serializing_if = "Option::is_none")]
1392 pub headers: Option<HashMap<String, String>>,
1393}
1394
1395impl std::fmt::Debug for NamedProviderConfig {
1396 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1397 f.debug_struct("NamedProviderConfig")
1398 .field("name", &self.name)
1399 .field("provider_type", &self.provider_type)
1400 .field("wire_api", &self.wire_api)
1401 .field("base_url", &self.base_url)
1402 .field("api_key", &self.api_key)
1403 .field("bearer_token", &self.bearer_token)
1404 .field(
1405 "bearer_token_provider",
1406 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1407 )
1408 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1409 .field("azure", &self.azure)
1410 .field("headers", &self.headers)
1411 .finish()
1412 }
1413}
1414
1415impl NamedProviderConfig {
1416 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1419 Self {
1420 name: name.into(),
1421 base_url: base_url.into(),
1422 ..Self::default()
1423 }
1424 }
1425
1426 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1428 self.provider_type = Some(provider_type.into());
1429 self
1430 }
1431
1432 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1434 self.wire_api = Some(wire_api.into());
1435 self
1436 }
1437
1438 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1440 self.api_key = Some(api_key.into());
1441 self
1442 }
1443
1444 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1447 self.bearer_token = Some(bearer_token.into());
1448 self
1449 }
1450
1451 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1457 self.bearer_token_provider = Some(provider);
1458 self
1459 }
1460
1461 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1463 self.azure = Some(azure);
1464 self
1465 }
1466
1467 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1469 self.headers = Some(headers);
1470 self
1471 }
1472}
1473
1474fn prepare_bearer_token_providers(
1475 provider: &mut Option<ProviderConfig>,
1476 providers: &mut Option<Vec<NamedProviderConfig>>,
1477) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1478 let mut bearer_token_providers = HashMap::new();
1479
1480 if let Some(provider) = provider.as_mut()
1481 && let Some(token_provider) = provider.bearer_token_provider.take()
1482 {
1483 provider.has_bearer_token_provider = Some(true);
1484 bearer_token_providers.insert("default".to_string(), token_provider);
1485 }
1486
1487 if let Some(providers) = providers.as_mut() {
1488 for provider in providers {
1489 if let Some(token_provider) = provider.bearer_token_provider.take() {
1490 provider.has_bearer_token_provider = Some(true);
1491 bearer_token_providers.insert(provider.name.clone(), token_provider);
1492 }
1493 }
1494 }
1495
1496 bearer_token_providers
1497}
1498
1499#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1507#[serde(rename_all = "camelCase")]
1508#[non_exhaustive]
1509pub struct ProviderModelConfig {
1510 pub id: String,
1513 pub provider: String,
1515 #[serde(default, skip_serializing_if = "Option::is_none")]
1518 pub wire_model: Option<String>,
1519 #[serde(default, skip_serializing_if = "Option::is_none")]
1522 pub model_id: Option<String>,
1523 #[serde(default, skip_serializing_if = "Option::is_none")]
1525 pub name: Option<String>,
1526 #[serde(default, skip_serializing_if = "Option::is_none")]
1528 pub max_prompt_tokens: Option<i64>,
1529 #[serde(default, skip_serializing_if = "Option::is_none")]
1531 pub max_context_window_tokens: Option<i64>,
1532 #[serde(default, skip_serializing_if = "Option::is_none")]
1534 pub max_output_tokens: Option<i64>,
1535 #[serde(default, skip_serializing_if = "Option::is_none")]
1538 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1539}
1540
1541impl ProviderModelConfig {
1542 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1545 Self {
1546 id: id.into(),
1547 provider: provider.into(),
1548 ..Self::default()
1549 }
1550 }
1551
1552 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1554 self.wire_model = Some(wire_model.into());
1555 self
1556 }
1557
1558 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1561 self.model_id = Some(model_id.into());
1562 self
1563 }
1564
1565 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1567 self.name = Some(name.into());
1568 self
1569 }
1570
1571 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1573 self.max_prompt_tokens = Some(max);
1574 self
1575 }
1576
1577 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1579 self.max_context_window_tokens = Some(max);
1580 self
1581 }
1582
1583 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1585 self.max_output_tokens = Some(max);
1586 self
1587 }
1588
1589 pub fn with_capabilities(
1591 mut self,
1592 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1593 ) -> Self {
1594 self.capabilities = Some(capabilities);
1595 self
1596 }
1597}
1598
1599#[derive(Clone)]
1651#[non_exhaustive]
1652pub struct SessionConfig {
1653 pub session_id: Option<SessionId>,
1655 pub model: Option<String>,
1657 pub client_name: Option<String>,
1659 pub reasoning_effort: Option<String>,
1661 pub reasoning_summary: Option<ReasoningSummary>,
1665 pub context_tier: Option<String>,
1668 pub streaming: Option<bool>,
1670 pub system_message: Option<SystemMessageConfig>,
1672 pub tools: Option<Vec<Tool>>,
1674 pub canvases: Option<Vec<CanvasDeclaration>>,
1676 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1681 pub request_canvas_renderer: Option<bool>,
1683 pub request_extensions: Option<bool>,
1685 pub extension_sdk_path: Option<String>,
1689 pub extension_info: Option<ExtensionInfo>,
1691 pub canvas_provider: Option<CanvasProviderIdentity>,
1694 pub available_tools: Option<Vec<String>>,
1696 pub excluded_tools: Option<Vec<String>>,
1698 pub excluded_builtin_agents: Option<Vec<String>>,
1704 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
1706 pub mcp_oauth_token_storage: Option<String>,
1715 pub enable_config_discovery: Option<bool>,
1717 pub skip_embedding_retrieval: Option<bool>,
1719 pub embedding_cache_storage: Option<String>,
1722 pub organization_custom_instructions: Option<String>,
1724 pub enable_on_demand_instruction_discovery: Option<bool>,
1726 pub enable_file_hooks: Option<bool>,
1728 pub enable_host_git_operations: Option<bool>,
1730 pub enable_session_store: Option<bool>,
1732 pub enable_skills: Option<bool>,
1734 pub enable_mcp_apps: Option<bool>,
1761 pub skill_directories: Option<Vec<PathBuf>>,
1763 pub instruction_directories: Option<Vec<PathBuf>>,
1766 pub plugin_directories: Option<Vec<PathBuf>>,
1768 pub large_output: Option<LargeToolOutputConfig>,
1770 pub tool_search: Option<ToolSearchConfig>,
1774 pub disabled_skills: Option<Vec<String>>,
1777 pub hooks: Option<bool>,
1781 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1783 pub default_agent: Option<DefaultAgentConfig>,
1787 pub agent: Option<String>,
1790 pub infinite_sessions: Option<InfiniteSessionConfig>,
1793 pub provider: Option<ProviderConfig>,
1797 pub capi: Option<CapiSessionOptions>,
1803 pub providers: Option<Vec<NamedProviderConfig>>,
1810 pub models: Option<Vec<ProviderModelConfig>>,
1816 pub enable_session_telemetry: Option<bool>,
1824 pub enable_citations: Option<bool>,
1826 pub session_limits: Option<SessionLimitsConfig>,
1828 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1831 pub memory: Option<MemoryConfiguration>,
1833 pub config_directory: Option<PathBuf>,
1836 pub working_directory: Option<PathBuf>,
1839 pub github_token: Option<String>,
1845 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
1851 pub cloud: Option<CloudSessionOptions>,
1854 pub include_sub_agent_streaming_events: Option<bool>,
1858 pub commands: Option<Vec<CommandDefinition>>,
1862 #[doc(hidden)]
1869 pub exp_assignments: Option<Value>,
1870 pub enable_managed_settings: Option<bool>,
1877 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1882 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
1886 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
1889 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
1892 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
1896 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
1899 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
1902 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1906 pub(crate) permission_policy: Option<crate::permission::Policy>,
1910 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
1915 pub skip_custom_instructions: Option<bool>,
1919 pub custom_agents_local_only: Option<bool>,
1923 pub coauthor_enabled: Option<bool>,
1927 pub manage_schedule_enabled: Option<bool>,
1931}
1932
1933impl std::fmt::Debug for SessionConfig {
1934 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1935 f.debug_struct("SessionConfig")
1936 .field("session_id", &self.session_id)
1937 .field("model", &self.model)
1938 .field("client_name", &self.client_name)
1939 .field("reasoning_effort", &self.reasoning_effort)
1940 .field("reasoning_summary", &self.reasoning_summary)
1941 .field("context_tier", &self.context_tier)
1942 .field("streaming", &self.streaming)
1943 .field("system_message", &self.system_message)
1944 .field("tools", &self.tools)
1945 .field("canvases", &self.canvases)
1946 .field(
1947 "canvas_handler",
1948 &self.canvas_handler.as_ref().map(|_| "<set>"),
1949 )
1950 .field("request_canvas_renderer", &self.request_canvas_renderer)
1951 .field("request_extensions", &self.request_extensions)
1952 .field("extension_sdk_path", &self.extension_sdk_path)
1953 .field("extension_info", &self.extension_info)
1954 .field("canvas_provider", &self.canvas_provider)
1955 .field("available_tools", &self.available_tools)
1956 .field("excluded_tools", &self.excluded_tools)
1957 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
1958 .field("mcp_servers", &self.mcp_servers)
1959 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
1960 .field("embedding_cache_storage", &self.embedding_cache_storage)
1961 .field("enable_config_discovery", &self.enable_config_discovery)
1962 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
1963 .field(
1964 "organization_custom_instructions",
1965 &self
1966 .organization_custom_instructions
1967 .as_ref()
1968 .map(|_| "<redacted>"),
1969 )
1970 .field(
1971 "enable_on_demand_instruction_discovery",
1972 &self.enable_on_demand_instruction_discovery,
1973 )
1974 .field("enable_file_hooks", &self.enable_file_hooks)
1975 .field(
1976 "enable_host_git_operations",
1977 &self.enable_host_git_operations,
1978 )
1979 .field("enable_session_store", &self.enable_session_store)
1980 .field("enable_skills", &self.enable_skills)
1981 .field("enable_mcp_apps", &self.enable_mcp_apps)
1982 .field("skill_directories", &self.skill_directories)
1983 .field("instruction_directories", &self.instruction_directories)
1984 .field("plugin_directories", &self.plugin_directories)
1985 .field("large_output", &self.large_output)
1986 .field("tool_search", &self.tool_search)
1987 .field("disabled_skills", &self.disabled_skills)
1988 .field("hooks", &self.hooks)
1989 .field("custom_agents", &self.custom_agents)
1990 .field("default_agent", &self.default_agent)
1991 .field("agent", &self.agent)
1992 .field("infinite_sessions", &self.infinite_sessions)
1993 .field("provider", &self.provider)
1994 .field("capi", &self.capi)
1995 .field("enable_session_telemetry", &self.enable_session_telemetry)
1996 .field("enable_citations", &self.enable_citations)
1997 .field("session_limits", &self.session_limits)
1998 .field("model_capabilities", &self.model_capabilities)
1999 .field("memory", &self.memory)
2000 .field("config_directory", &self.config_directory)
2001 .field("working_directory", &self.working_directory)
2002 .field(
2003 "github_token",
2004 &self.github_token.as_ref().map(|_| "<redacted>"),
2005 )
2006 .field("remote_session", &self.remote_session)
2007 .field("cloud", &self.cloud)
2008 .field(
2009 "include_sub_agent_streaming_events",
2010 &self.include_sub_agent_streaming_events,
2011 )
2012 .field("commands", &self.commands)
2013 .field("exp_assignments", &self.exp_assignments)
2014 .field("enable_managed_settings", &self.enable_managed_settings)
2015 .field(
2016 "session_fs_provider",
2017 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2018 )
2019 .field(
2020 "permission_handler",
2021 &self.permission_handler.as_ref().map(|_| "<set>"),
2022 )
2023 .field(
2024 "elicitation_handler",
2025 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2026 )
2027 .field(
2028 "mcp_auth_handler",
2029 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2030 )
2031 .field(
2032 "user_input_handler",
2033 &self.user_input_handler.as_ref().map(|_| "<set>"),
2034 )
2035 .field(
2036 "exit_plan_mode_handler",
2037 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2038 )
2039 .field(
2040 "auto_mode_switch_handler",
2041 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2042 )
2043 .field(
2044 "hooks_handler",
2045 &self.hooks_handler.as_ref().map(|_| "<set>"),
2046 )
2047 .field(
2048 "system_message_transform",
2049 &self.system_message_transform.as_ref().map(|_| "<set>"),
2050 )
2051 .finish()
2052 }
2053}
2054
2055impl Default for SessionConfig {
2056 fn default() -> Self {
2062 Self {
2063 session_id: None,
2064 model: None,
2065 client_name: None,
2066 reasoning_effort: None,
2067 reasoning_summary: None,
2068 context_tier: None,
2069 streaming: None,
2070 system_message: None,
2071 tools: None,
2072 canvases: None,
2073 canvas_handler: None,
2074 request_canvas_renderer: None,
2075 request_extensions: None,
2076 extension_sdk_path: None,
2077 extension_info: None,
2078 canvas_provider: None,
2079 available_tools: None,
2080 excluded_tools: None,
2081 excluded_builtin_agents: None,
2082 mcp_servers: None,
2083 mcp_oauth_token_storage: None,
2084 enable_config_discovery: None,
2085 skip_embedding_retrieval: None,
2086 organization_custom_instructions: None,
2087 enable_on_demand_instruction_discovery: None,
2088 enable_file_hooks: None,
2089 enable_host_git_operations: None,
2090 enable_session_store: None,
2091 enable_skills: None,
2092 embedding_cache_storage: None,
2093 enable_mcp_apps: None,
2094 skill_directories: None,
2095 instruction_directories: None,
2096 plugin_directories: None,
2097 large_output: None,
2098 tool_search: None,
2099 disabled_skills: None,
2100 hooks: None,
2101 custom_agents: None,
2102 default_agent: None,
2103 agent: None,
2104 infinite_sessions: None,
2105 provider: None,
2106 capi: None,
2107 providers: None,
2108 models: None,
2109 enable_session_telemetry: None,
2110 enable_citations: None,
2111 session_limits: None,
2112 model_capabilities: None,
2113 memory: None,
2114 config_directory: None,
2115 working_directory: None,
2116 github_token: None,
2117 remote_session: None,
2118 cloud: None,
2119 include_sub_agent_streaming_events: None,
2120 commands: None,
2121 exp_assignments: None,
2122 enable_managed_settings: None,
2123 session_fs_provider: None,
2124 permission_handler: None,
2125 elicitation_handler: None,
2126 mcp_auth_handler: None,
2127 user_input_handler: None,
2128 exit_plan_mode_handler: None,
2129 auto_mode_switch_handler: None,
2130 hooks_handler: None,
2131 permission_policy: None,
2132 system_message_transform: None,
2133 skip_custom_instructions: None,
2134 custom_agents_local_only: None,
2135 coauthor_enabled: None,
2136 manage_schedule_enabled: None,
2137 }
2138 }
2139}
2140
2141pub(crate) struct SessionConfigRuntime {
2147 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2148 pub permission_policy: Option<crate::permission::Policy>,
2149 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2150 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2151 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2152 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2153 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2154 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2155 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2156 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2157 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2158 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2159 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2160 pub commands: Option<Vec<CommandDefinition>>,
2161}
2162
2163impl SessionConfig {
2164 pub(crate) fn into_wire(
2176 mut self,
2177 session_id: Option<SessionId>,
2178 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2179 let permission_active =
2180 self.permission_handler.is_some() || self.permission_policy.is_some();
2181 let request_user_input = self.user_input_handler.is_some();
2182 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2183 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2184 let request_elicitation = self.elicitation_handler.is_some();
2185 let hooks_flag = self.hooks_handler.is_some();
2186
2187 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2188 if let Some(tools) = self.tools.as_mut() {
2189 for tool in tools.iter_mut() {
2190 if let Some(handler) = tool.handler.take()
2191 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2192 {
2193 return Err(crate::Error::with_message(
2194 crate::ErrorKind::InvalidConfig,
2195 format!("duplicate tool handler registered for name {:?}", tool.name),
2196 ));
2197 }
2198 }
2199 }
2200
2201 let wire_commands = self.commands.as_ref().map(|cmds| {
2202 cmds.iter()
2203 .map(|c| crate::wire::CommandWireDefinition {
2204 name: c.name.clone(),
2205 description: c.description.clone(),
2206 })
2207 .collect()
2208 });
2209 let wire_canvases = self.canvases.clone();
2210 let canvas_handler = self.canvas_handler.clone();
2211 let bearer_token_providers =
2212 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2213
2214 let wire = crate::wire::SessionCreateWire {
2215 session_id,
2216 model: self.model,
2217 client_name: self.client_name,
2218 reasoning_effort: self.reasoning_effort,
2219 reasoning_summary: self.reasoning_summary,
2220 context_tier: self.context_tier,
2221 streaming: self.streaming,
2222 system_message: self.system_message,
2223 tools: self.tools,
2224 canvases: wire_canvases,
2225 request_canvas_renderer: self.request_canvas_renderer,
2226 request_extensions: self.request_extensions,
2227 extension_sdk_path: self.extension_sdk_path,
2228 extension_info: self.extension_info,
2229 canvas_provider: self.canvas_provider,
2230 available_tools: self.available_tools,
2231 excluded_tools: self.excluded_tools,
2232 excluded_builtin_agents: self.excluded_builtin_agents,
2233 tool_filter_precedence: "excluded",
2234 mcp_servers: self.mcp_servers,
2235 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2236 embedding_cache_storage: self.embedding_cache_storage,
2237 env_value_mode: "direct",
2238 enable_config_discovery: self.enable_config_discovery,
2239 skip_embedding_retrieval: self.skip_embedding_retrieval,
2240 organization_custom_instructions: self.organization_custom_instructions,
2241 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2242 enable_file_hooks: self.enable_file_hooks,
2243 enable_host_git_operations: self.enable_host_git_operations,
2244 enable_session_store: self.enable_session_store,
2245 enable_skills: self.enable_skills,
2246 request_user_input,
2247 request_permission: permission_active,
2248 request_exit_plan_mode,
2249 request_auto_mode_switch,
2250 request_elicitation,
2251 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2252 hooks: hooks_flag,
2253 skill_directories: self.skill_directories,
2254 instruction_directories: self.instruction_directories,
2255 plugin_directories: self.plugin_directories,
2256 large_output: self.large_output,
2257 tool_search: self.tool_search,
2258 disabled_skills: self.disabled_skills,
2259 custom_agents: self.custom_agents,
2260 default_agent: self.default_agent,
2261 agent: self.agent,
2262 infinite_sessions: self.infinite_sessions,
2263 provider: self.provider,
2264 capi: self.capi,
2265 providers: self.providers,
2266 models: self.models,
2267 enable_session_telemetry: self.enable_session_telemetry,
2268 enable_citations: self.enable_citations,
2269 session_limits: self.session_limits,
2270 model_capabilities: self.model_capabilities,
2271 memory: self.memory,
2272 config_dir: self.config_directory,
2273 working_directory: self.working_directory,
2274 github_token: self.github_token,
2275 remote_session: self.remote_session,
2276 cloud: self.cloud,
2277 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2278 enable_github_telemetry_forwarding: None,
2279 commands: wire_commands,
2280 exp_assignments: self.exp_assignments,
2281 enable_managed_settings: self.enable_managed_settings,
2282 };
2283
2284 let runtime = SessionConfigRuntime {
2285 permission_handler: self.permission_handler,
2286 permission_policy: self.permission_policy,
2287 elicitation_handler: self.elicitation_handler,
2288 mcp_auth_handler: self.mcp_auth_handler,
2289 user_input_handler: self.user_input_handler,
2290 exit_plan_mode_handler: self.exit_plan_mode_handler,
2291 auto_mode_switch_handler: self.auto_mode_switch_handler,
2292 hooks_handler: self.hooks_handler,
2293 system_message_transform: self.system_message_transform,
2294 tool_handlers,
2295 canvas_handler,
2296 session_fs_provider: self.session_fs_provider,
2297 bearer_token_providers,
2298 commands: self.commands,
2299 };
2300
2301 Ok((wire, runtime))
2302 }
2303
2304 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2308 self.permission_handler = Some(handler);
2309 self
2310 }
2311
2312 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2315 self.elicitation_handler = Some(handler);
2316 self
2317 }
2318
2319 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2321 self.mcp_auth_handler = Some(handler);
2322 self
2323 }
2324
2325 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2328 self.user_input_handler = Some(handler);
2329 self
2330 }
2331
2332 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2334 self.exit_plan_mode_handler = Some(handler);
2335 self
2336 }
2337
2338 pub fn with_auto_mode_switch_handler(
2340 mut self,
2341 handler: Arc<dyn AutoModeSwitchHandler>,
2342 ) -> Self {
2343 self.auto_mode_switch_handler = Some(handler);
2344 self
2345 }
2346
2347 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2352 self.commands = Some(commands);
2353 self
2354 }
2355
2356 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2360 self.session_fs_provider = Some(provider);
2361 self
2362 }
2363
2364 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2367 self.hooks_handler = Some(hooks);
2368 self
2369 }
2370
2371 pub fn with_system_message_transform(
2375 mut self,
2376 transform: Arc<dyn SystemMessageTransform>,
2377 ) -> Self {
2378 self.system_message_transform = Some(transform);
2379 self
2380 }
2381
2382 pub fn approve_all_permissions(mut self) -> Self {
2388 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2389 self
2390 }
2391
2392 pub fn deny_all_permissions(mut self) -> Self {
2395 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2396 self
2397 }
2398
2399 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2404 where
2405 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2406 {
2407 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2408 self
2409 }
2410
2411 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2413 self.session_id = Some(id.into());
2414 self
2415 }
2416
2417 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2419 self.model = Some(model.into());
2420 self
2421 }
2422
2423 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2425 self.client_name = Some(name.into());
2426 self
2427 }
2428
2429 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2431 self.reasoning_effort = Some(effort.into());
2432 self
2433 }
2434
2435 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2437 self.reasoning_summary = Some(summary);
2438 self
2439 }
2440
2441 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2443 self.context_tier = Some(tier.into());
2444 self
2445 }
2446
2447 pub fn with_streaming(mut self, streaming: bool) -> Self {
2449 self.streaming = Some(streaming);
2450 self
2451 }
2452
2453 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2455 self.system_message = Some(system_message);
2456 self
2457 }
2458
2459 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2461 self.tools = Some(tools.into_iter().collect());
2462 self
2463 }
2464
2465 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2470 self.canvases = Some(canvases.into_iter().collect());
2471 self
2472 }
2473
2474 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2476 self.canvas_handler = Some(handler);
2477 self
2478 }
2479
2480 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2482 self.request_canvas_renderer = Some(request);
2483 self
2484 }
2485
2486 pub fn with_request_extensions(mut self, request: bool) -> Self {
2488 self.request_extensions = Some(request);
2489 self
2490 }
2491
2492 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2496 self.extension_sdk_path = Some(path.into());
2497 self
2498 }
2499
2500 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2502 self.extension_info = Some(extension_info);
2503 self
2504 }
2505
2506 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2509 self.canvas_provider = Some(canvas_provider);
2510 self
2511 }
2512
2513 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2515 where
2516 I: IntoIterator<Item = S>,
2517 S: Into<String>,
2518 {
2519 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2520 self
2521 }
2522
2523 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2525 where
2526 I: IntoIterator<Item = S>,
2527 S: Into<String>,
2528 {
2529 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2530 self
2531 }
2532
2533 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2535 where
2536 I: IntoIterator<Item = S>,
2537 S: Into<String>,
2538 {
2539 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2540 self
2541 }
2542
2543 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2545 self.mcp_servers = Some(servers);
2546 self
2547 }
2548
2549 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2557 self.mcp_oauth_token_storage = Some(mode.into());
2558 self
2559 }
2560
2561 pub fn with_embedding_cache_storage(
2563 mut self,
2564 embedding_cache_storage: impl Into<String>,
2565 ) -> Self {
2566 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2567 self
2568 }
2569
2570 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2572 self.enable_config_discovery = Some(enable);
2573 self
2574 }
2575
2576 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2578 self.skip_embedding_retrieval = Some(value);
2579 self
2580 }
2581
2582 pub fn with_organization_custom_instructions(
2584 mut self,
2585 instructions: impl Into<String>,
2586 ) -> Self {
2587 self.organization_custom_instructions = Some(instructions.into());
2588 self
2589 }
2590
2591 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2593 self.enable_on_demand_instruction_discovery = Some(value);
2594 self
2595 }
2596
2597 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2599 self.enable_file_hooks = Some(value);
2600 self
2601 }
2602
2603 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2605 self.enable_host_git_operations = Some(value);
2606 self
2607 }
2608
2609 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2611 self.enable_session_store = Some(value);
2612 self
2613 }
2614
2615 pub fn with_enable_skills(mut self, value: bool) -> Self {
2617 self.enable_skills = Some(value);
2618 self
2619 }
2620
2621 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2627 self.enable_mcp_apps = Some(enable);
2628 self
2629 }
2630
2631 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2633 where
2634 I: IntoIterator<Item = P>,
2635 P: Into<PathBuf>,
2636 {
2637 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2638 self
2639 }
2640
2641 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2645 where
2646 I: IntoIterator<Item = P>,
2647 P: Into<PathBuf>,
2648 {
2649 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2650 self
2651 }
2652
2653 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2655 where
2656 I: IntoIterator<Item = P>,
2657 P: Into<PathBuf>,
2658 {
2659 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2660 self
2661 }
2662
2663 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2665 self.large_output = Some(config);
2666 self
2667 }
2668
2669 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
2672 self.tool_search = Some(config);
2673 self
2674 }
2675
2676 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2678 where
2679 I: IntoIterator<Item = S>,
2680 S: Into<String>,
2681 {
2682 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2683 self
2684 }
2685
2686 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2688 mut self,
2689 agents: I,
2690 ) -> Self {
2691 self.custom_agents = Some(agents.into_iter().collect());
2692 self
2693 }
2694
2695 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2697 self.default_agent = Some(agent);
2698 self
2699 }
2700
2701 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2704 self.agent = Some(name.into());
2705 self
2706 }
2707
2708 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2711 self.infinite_sessions = Some(config);
2712 self
2713 }
2714
2715 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2717 self.provider = Some(provider);
2718 self
2719 }
2720
2721 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
2723 self.capi = Some(capi);
2724 self
2725 }
2726
2727 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
2733 self.providers = Some(providers);
2734 self
2735 }
2736
2737 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
2743 self.models = Some(models);
2744 self
2745 }
2746
2747 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2751 self.enable_session_telemetry = Some(enable);
2752 self
2753 }
2754
2755 pub fn with_enable_citations(mut self, enable: bool) -> Self {
2757 self.enable_citations = Some(enable);
2758 self
2759 }
2760
2761 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
2763 self.session_limits = Some(limits);
2764 self
2765 }
2766
2767 pub fn with_model_capabilities(
2769 mut self,
2770 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2771 ) -> Self {
2772 self.model_capabilities = Some(capabilities);
2773 self
2774 }
2775
2776 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
2778 self.memory = Some(memory);
2779 self
2780 }
2781
2782 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2784 self.config_directory = Some(dir.into());
2785 self
2786 }
2787
2788 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2791 self.working_directory = Some(dir.into());
2792 self
2793 }
2794
2795 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2800 self.github_token = Some(token.into());
2801 self
2802 }
2803
2804 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2807 self.include_sub_agent_streaming_events = Some(include);
2808 self
2809 }
2810
2811 pub fn with_remote_session(
2813 mut self,
2814 mode: crate::generated::api_types::RemoteSessionMode,
2815 ) -> Self {
2816 self.remote_session = Some(mode);
2817 self
2818 }
2819
2820 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
2822 self.cloud = Some(cloud);
2823 self
2824 }
2825
2826 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
2828 self.skip_custom_instructions = Some(value);
2829 self
2830 }
2831
2832 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
2834 self.custom_agents_local_only = Some(value);
2835 self
2836 }
2837
2838 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
2840 self.coauthor_enabled = Some(value);
2841 self
2842 }
2843
2844 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
2846 self.manage_schedule_enabled = Some(value);
2847 self
2848 }
2849
2850 #[doc(hidden)]
2858 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
2859 self.exp_assignments = Some(assignments);
2860 self
2861 }
2862
2863 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
2869 self.enable_managed_settings = Some(enabled);
2870 self
2871 }
2872}
2873#[derive(Clone)]
2880#[non_exhaustive]
2881pub struct ResumeSessionConfig {
2882 pub session_id: SessionId,
2884 pub model: Option<String>,
2887 pub client_name: Option<String>,
2889 pub reasoning_effort: Option<String>,
2891 pub reasoning_summary: Option<ReasoningSummary>,
2895 pub context_tier: Option<String>,
2898 pub streaming: Option<bool>,
2900 pub system_message: Option<SystemMessageConfig>,
2903 pub tools: Option<Vec<Tool>>,
2905 pub canvases: Option<Vec<CanvasDeclaration>>,
2907 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2910 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
2912 pub request_canvas_renderer: Option<bool>,
2914 pub request_extensions: Option<bool>,
2916 pub extension_sdk_path: Option<String>,
2920 pub extension_info: Option<ExtensionInfo>,
2922 pub canvas_provider: Option<CanvasProviderIdentity>,
2925 pub available_tools: Option<Vec<String>>,
2927 pub excluded_tools: Option<Vec<String>>,
2929 pub excluded_builtin_agents: Option<Vec<String>>,
2935 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
2937 pub mcp_oauth_token_storage: Option<String>,
2940 pub enable_config_discovery: Option<bool>,
2942 pub skip_embedding_retrieval: Option<bool>,
2944 pub embedding_cache_storage: Option<String>,
2946 pub organization_custom_instructions: Option<String>,
2948 pub enable_on_demand_instruction_discovery: Option<bool>,
2950 pub enable_file_hooks: Option<bool>,
2952 pub enable_host_git_operations: Option<bool>,
2954 pub enable_session_store: Option<bool>,
2956 pub enable_skills: Option<bool>,
2958 pub enable_mcp_apps: Option<bool>,
2964 pub skill_directories: Option<Vec<PathBuf>>,
2966 pub instruction_directories: Option<Vec<PathBuf>>,
2969 pub plugin_directories: Option<Vec<PathBuf>>,
2971 pub large_output: Option<LargeToolOutputConfig>,
2973 pub tool_search: Option<ToolSearchConfig>,
2976 pub disabled_skills: Option<Vec<String>>,
2978 pub hooks: Option<bool>,
2980 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2982 pub default_agent: Option<DefaultAgentConfig>,
2984 pub agent: Option<String>,
2986 pub infinite_sessions: Option<InfiniteSessionConfig>,
2988 pub provider: Option<ProviderConfig>,
2990 pub capi: Option<CapiSessionOptions>,
2996 pub providers: Option<Vec<NamedProviderConfig>>,
3002 pub models: Option<Vec<ProviderModelConfig>>,
3008 pub enable_session_telemetry: Option<bool>,
3016 pub enable_citations: Option<bool>,
3018 pub session_limits: Option<SessionLimitsConfig>,
3020 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3022 pub memory: Option<MemoryConfiguration>,
3024 pub config_directory: Option<PathBuf>,
3026 pub working_directory: Option<PathBuf>,
3028 pub github_token: Option<String>,
3031 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3034 pub include_sub_agent_streaming_events: Option<bool>,
3036 pub commands: Option<Vec<CommandDefinition>>,
3040 #[doc(hidden)]
3045 pub exp_assignments: Option<Value>,
3046 pub enable_managed_settings: Option<bool>,
3052 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3057 pub suppress_resume_event: Option<bool>,
3060 pub continue_pending_work: Option<bool>,
3068 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3071 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3074 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3076 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3079 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3082 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3085 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3087 pub(crate) permission_policy: Option<crate::permission::Policy>,
3089 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3091 pub skip_custom_instructions: Option<bool>,
3093 pub custom_agents_local_only: Option<bool>,
3095 pub coauthor_enabled: Option<bool>,
3097 pub manage_schedule_enabled: Option<bool>,
3099}
3100
3101impl std::fmt::Debug for ResumeSessionConfig {
3102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3103 f.debug_struct("ResumeSessionConfig")
3104 .field("session_id", &self.session_id)
3105 .field("model", &self.model)
3106 .field("client_name", &self.client_name)
3107 .field("reasoning_effort", &self.reasoning_effort)
3108 .field("reasoning_summary", &self.reasoning_summary)
3109 .field("context_tier", &self.context_tier)
3110 .field("streaming", &self.streaming)
3111 .field("system_message", &self.system_message)
3112 .field("tools", &self.tools)
3113 .field("canvases", &self.canvases)
3114 .field(
3115 "canvas_handler",
3116 &self.canvas_handler.as_ref().map(|_| "<set>"),
3117 )
3118 .field("open_canvases", &self.open_canvases)
3119 .field("request_canvas_renderer", &self.request_canvas_renderer)
3120 .field("request_extensions", &self.request_extensions)
3121 .field("extension_sdk_path", &self.extension_sdk_path)
3122 .field("extension_info", &self.extension_info)
3123 .field("canvas_provider", &self.canvas_provider)
3124 .field("available_tools", &self.available_tools)
3125 .field("excluded_tools", &self.excluded_tools)
3126 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3127 .field("mcp_servers", &self.mcp_servers)
3128 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3129 .field("embedding_cache_storage", &self.embedding_cache_storage)
3130 .field("enable_config_discovery", &self.enable_config_discovery)
3131 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3132 .field(
3133 "organization_custom_instructions",
3134 &self
3135 .organization_custom_instructions
3136 .as_ref()
3137 .map(|_| "<redacted>"),
3138 )
3139 .field(
3140 "enable_on_demand_instruction_discovery",
3141 &self.enable_on_demand_instruction_discovery,
3142 )
3143 .field("enable_file_hooks", &self.enable_file_hooks)
3144 .field(
3145 "enable_host_git_operations",
3146 &self.enable_host_git_operations,
3147 )
3148 .field("enable_session_store", &self.enable_session_store)
3149 .field("enable_skills", &self.enable_skills)
3150 .field("enable_mcp_apps", &self.enable_mcp_apps)
3151 .field("skill_directories", &self.skill_directories)
3152 .field("instruction_directories", &self.instruction_directories)
3153 .field("plugin_directories", &self.plugin_directories)
3154 .field("large_output", &self.large_output)
3155 .field("tool_search", &self.tool_search)
3156 .field("disabled_skills", &self.disabled_skills)
3157 .field("hooks", &self.hooks)
3158 .field("custom_agents", &self.custom_agents)
3159 .field("default_agent", &self.default_agent)
3160 .field("agent", &self.agent)
3161 .field("infinite_sessions", &self.infinite_sessions)
3162 .field("provider", &self.provider)
3163 .field("capi", &self.capi)
3164 .field("enable_session_telemetry", &self.enable_session_telemetry)
3165 .field("enable_citations", &self.enable_citations)
3166 .field("session_limits", &self.session_limits)
3167 .field("model_capabilities", &self.model_capabilities)
3168 .field("memory", &self.memory)
3169 .field("config_directory", &self.config_directory)
3170 .field("working_directory", &self.working_directory)
3171 .field(
3172 "github_token",
3173 &self.github_token.as_ref().map(|_| "<redacted>"),
3174 )
3175 .field("remote_session", &self.remote_session)
3176 .field(
3177 "include_sub_agent_streaming_events",
3178 &self.include_sub_agent_streaming_events,
3179 )
3180 .field("commands", &self.commands)
3181 .field("exp_assignments", &self.exp_assignments)
3182 .field("enable_managed_settings", &self.enable_managed_settings)
3183 .field(
3184 "session_fs_provider",
3185 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3186 )
3187 .field(
3188 "permission_handler",
3189 &self.permission_handler.as_ref().map(|_| "<set>"),
3190 )
3191 .field(
3192 "elicitation_handler",
3193 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3194 )
3195 .field(
3196 "user_input_handler",
3197 &self.user_input_handler.as_ref().map(|_| "<set>"),
3198 )
3199 .field(
3200 "exit_plan_mode_handler",
3201 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3202 )
3203 .field(
3204 "auto_mode_switch_handler",
3205 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3206 )
3207 .field(
3208 "hooks_handler",
3209 &self.hooks_handler.as_ref().map(|_| "<set>"),
3210 )
3211 .field(
3212 "system_message_transform",
3213 &self.system_message_transform.as_ref().map(|_| "<set>"),
3214 )
3215 .field("suppress_resume_event", &self.suppress_resume_event)
3216 .field("continue_pending_work", &self.continue_pending_work)
3217 .finish()
3218 }
3219}
3220
3221impl ResumeSessionConfig {
3222 pub(crate) fn into_wire(
3230 mut self,
3231 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3232 let permission_active =
3233 self.permission_handler.is_some() || self.permission_policy.is_some();
3234 let request_user_input = self.user_input_handler.is_some();
3235 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3236 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3237 let request_elicitation = self.elicitation_handler.is_some();
3238 let hooks_flag = self.hooks_handler.is_some();
3239
3240 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3241 if let Some(tools) = self.tools.as_mut() {
3242 for tool in tools.iter_mut() {
3243 if let Some(handler) = tool.handler.take()
3244 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3245 {
3246 return Err(crate::Error::with_message(
3247 crate::ErrorKind::InvalidConfig,
3248 format!("duplicate tool handler registered for name {:?}", tool.name),
3249 ));
3250 }
3251 }
3252 }
3253
3254 let wire_commands = self.commands.as_ref().map(|cmds| {
3255 cmds.iter()
3256 .map(|c| crate::wire::CommandWireDefinition {
3257 name: c.name.clone(),
3258 description: c.description.clone(),
3259 })
3260 .collect()
3261 });
3262 let wire_canvases = self.canvases.clone();
3263 let canvas_handler = self.canvas_handler.clone();
3264 let bearer_token_providers =
3265 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3266
3267 let wire = crate::wire::SessionResumeWire {
3268 session_id: self.session_id,
3269 model: self.model,
3270 client_name: self.client_name,
3271 reasoning_effort: self.reasoning_effort,
3272 reasoning_summary: self.reasoning_summary,
3273 context_tier: self.context_tier,
3274 streaming: self.streaming,
3275 system_message: self.system_message,
3276 tools: self.tools,
3277 canvases: wire_canvases,
3278 open_canvases: self.open_canvases,
3279 request_canvas_renderer: self.request_canvas_renderer,
3280 request_extensions: self.request_extensions,
3281 extension_sdk_path: self.extension_sdk_path,
3282 extension_info: self.extension_info,
3283 canvas_provider: self.canvas_provider,
3284 available_tools: self.available_tools,
3285 excluded_tools: self.excluded_tools,
3286 excluded_builtin_agents: self.excluded_builtin_agents,
3287 tool_filter_precedence: "excluded",
3288 mcp_servers: self.mcp_servers,
3289 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3290 embedding_cache_storage: self.embedding_cache_storage,
3291 env_value_mode: "direct",
3292 enable_config_discovery: self.enable_config_discovery,
3293 skip_embedding_retrieval: self.skip_embedding_retrieval,
3294 organization_custom_instructions: self.organization_custom_instructions,
3295 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3296 enable_file_hooks: self.enable_file_hooks,
3297 enable_host_git_operations: self.enable_host_git_operations,
3298 enable_session_store: self.enable_session_store,
3299 enable_skills: self.enable_skills,
3300 request_user_input,
3301 request_permission: permission_active,
3302 request_exit_plan_mode,
3303 request_auto_mode_switch,
3304 request_elicitation,
3305 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3306 hooks: hooks_flag,
3307 skill_directories: self.skill_directories,
3308 instruction_directories: self.instruction_directories,
3309 plugin_directories: self.plugin_directories,
3310 large_output: self.large_output,
3311 tool_search: self.tool_search,
3312 disabled_skills: self.disabled_skills,
3313 custom_agents: self.custom_agents,
3314 default_agent: self.default_agent,
3315 agent: self.agent,
3316 infinite_sessions: self.infinite_sessions,
3317 provider: self.provider,
3318 capi: self.capi,
3319 providers: self.providers,
3320 models: self.models,
3321 enable_session_telemetry: self.enable_session_telemetry,
3322 enable_citations: self.enable_citations,
3323 session_limits: self.session_limits,
3324 model_capabilities: self.model_capabilities,
3325 memory: self.memory,
3326 config_dir: self.config_directory,
3327 working_directory: self.working_directory,
3328 github_token: self.github_token,
3329 remote_session: self.remote_session,
3330 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3331 enable_github_telemetry_forwarding: None,
3332 commands: wire_commands,
3333 exp_assignments: self.exp_assignments,
3334 enable_managed_settings: self.enable_managed_settings,
3335 suppress_resume_event: self.suppress_resume_event,
3336 continue_pending_work: self.continue_pending_work,
3337 };
3338
3339 let runtime = SessionConfigRuntime {
3340 permission_handler: self.permission_handler,
3341 permission_policy: self.permission_policy,
3342 elicitation_handler: self.elicitation_handler,
3343 mcp_auth_handler: self.mcp_auth_handler,
3344 user_input_handler: self.user_input_handler,
3345 exit_plan_mode_handler: self.exit_plan_mode_handler,
3346 auto_mode_switch_handler: self.auto_mode_switch_handler,
3347 hooks_handler: self.hooks_handler,
3348 system_message_transform: self.system_message_transform,
3349 tool_handlers,
3350 canvas_handler,
3351 session_fs_provider: self.session_fs_provider,
3352 bearer_token_providers,
3353 commands: self.commands,
3354 };
3355
3356 Ok((wire, runtime))
3357 }
3358
3359 pub fn new(session_id: SessionId) -> Self {
3364 Self {
3365 session_id,
3366 model: None,
3367 client_name: None,
3368 reasoning_effort: None,
3369 reasoning_summary: None,
3370 context_tier: None,
3371 streaming: None,
3372 system_message: None,
3373 tools: None,
3374 canvases: None,
3375 canvas_handler: None,
3376 open_canvases: None,
3377 request_canvas_renderer: None,
3378 request_extensions: None,
3379 extension_sdk_path: None,
3380 extension_info: None,
3381 canvas_provider: None,
3382 available_tools: None,
3383 excluded_tools: None,
3384 excluded_builtin_agents: None,
3385 mcp_servers: None,
3386 mcp_oauth_token_storage: None,
3387 enable_config_discovery: None,
3388 skip_embedding_retrieval: None,
3389 organization_custom_instructions: None,
3390 enable_on_demand_instruction_discovery: None,
3391 enable_file_hooks: None,
3392 enable_host_git_operations: None,
3393 enable_session_store: None,
3394 enable_skills: None,
3395 embedding_cache_storage: None,
3396 enable_mcp_apps: None,
3397 skill_directories: None,
3398 instruction_directories: None,
3399 plugin_directories: None,
3400 large_output: None,
3401 tool_search: None,
3402 disabled_skills: None,
3403 hooks: None,
3404 custom_agents: None,
3405 default_agent: None,
3406 agent: None,
3407 infinite_sessions: None,
3408 provider: None,
3409 capi: None,
3410 providers: None,
3411 models: None,
3412 enable_session_telemetry: None,
3413 enable_citations: None,
3414 session_limits: None,
3415 model_capabilities: None,
3416 memory: None,
3417 config_directory: None,
3418 working_directory: None,
3419 github_token: None,
3420 remote_session: None,
3421 include_sub_agent_streaming_events: None,
3422 commands: None,
3423 exp_assignments: None,
3424 enable_managed_settings: None,
3425 session_fs_provider: None,
3426 suppress_resume_event: None,
3427 continue_pending_work: None,
3428 permission_handler: None,
3429 elicitation_handler: None,
3430 mcp_auth_handler: None,
3431 user_input_handler: None,
3432 exit_plan_mode_handler: None,
3433 auto_mode_switch_handler: None,
3434 hooks_handler: None,
3435 permission_policy: None,
3436 system_message_transform: None,
3437 skip_custom_instructions: None,
3438 custom_agents_local_only: None,
3439 coauthor_enabled: None,
3440 manage_schedule_enabled: None,
3441 }
3442 }
3443
3444 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3446 self.permission_handler = Some(handler);
3447 self
3448 }
3449
3450 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3452 self.elicitation_handler = Some(handler);
3453 self
3454 }
3455
3456 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3458 self.mcp_auth_handler = Some(handler);
3459 self
3460 }
3461
3462 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3464 self.user_input_handler = Some(handler);
3465 self
3466 }
3467
3468 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3470 self.exit_plan_mode_handler = Some(handler);
3471 self
3472 }
3473
3474 pub fn with_auto_mode_switch_handler(
3476 mut self,
3477 handler: Arc<dyn AutoModeSwitchHandler>,
3478 ) -> Self {
3479 self.auto_mode_switch_handler = Some(handler);
3480 self
3481 }
3482
3483 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3486 self.hooks_handler = Some(hooks);
3487 self
3488 }
3489
3490 pub fn with_system_message_transform(
3492 mut self,
3493 transform: Arc<dyn SystemMessageTransform>,
3494 ) -> Self {
3495 self.system_message_transform = Some(transform);
3496 self
3497 }
3498
3499 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3503 self.commands = Some(commands);
3504 self
3505 }
3506
3507 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3510 self.session_fs_provider = Some(provider);
3511 self
3512 }
3513
3514 pub fn approve_all_permissions(mut self) -> Self {
3517 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3518 self
3519 }
3520
3521 pub fn deny_all_permissions(mut self) -> Self {
3524 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3525 self
3526 }
3527
3528 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3531 where
3532 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3533 {
3534 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3535 self
3536 }
3537
3538 pub fn with_model(mut self, model: impl Into<String>) -> Self {
3540 self.model = Some(model.into());
3541 self
3542 }
3543
3544 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3546 self.client_name = Some(name.into());
3547 self
3548 }
3549
3550 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3552 self.reasoning_effort = Some(effort.into());
3553 self
3554 }
3555
3556 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3558 self.reasoning_summary = Some(summary);
3559 self
3560 }
3561
3562 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3565 self.context_tier = Some(tier.into());
3566 self
3567 }
3568
3569 pub fn with_streaming(mut self, streaming: bool) -> Self {
3571 self.streaming = Some(streaming);
3572 self
3573 }
3574
3575 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3578 self.system_message = Some(system_message);
3579 self
3580 }
3581
3582 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3584 self.tools = Some(tools.into_iter().collect());
3585 self
3586 }
3587
3588 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3590 self.canvases = Some(canvases.into_iter().collect());
3591 self
3592 }
3593
3594 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
3596 self.canvas_handler = Some(handler);
3597 self
3598 }
3599
3600 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
3602 mut self,
3603 open_canvases: I,
3604 ) -> Self {
3605 self.open_canvases = Some(open_canvases.into_iter().collect());
3606 self
3607 }
3608
3609 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
3611 self.request_canvas_renderer = Some(request);
3612 self
3613 }
3614
3615 pub fn with_request_extensions(mut self, request: bool) -> Self {
3617 self.request_extensions = Some(request);
3618 self
3619 }
3620
3621 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
3625 self.extension_sdk_path = Some(path.into());
3626 self
3627 }
3628
3629 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
3631 self.extension_info = Some(extension_info);
3632 self
3633 }
3634
3635 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
3638 self.canvas_provider = Some(canvas_provider);
3639 self
3640 }
3641
3642 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
3644 where
3645 I: IntoIterator<Item = S>,
3646 S: Into<String>,
3647 {
3648 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
3649 self
3650 }
3651
3652 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
3654 where
3655 I: IntoIterator<Item = S>,
3656 S: Into<String>,
3657 {
3658 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
3659 self
3660 }
3661
3662 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
3664 where
3665 I: IntoIterator<Item = S>,
3666 S: Into<String>,
3667 {
3668 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
3669 self
3670 }
3671
3672 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
3674 self.mcp_servers = Some(servers);
3675 self
3676 }
3677
3678 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3681 self.mcp_oauth_token_storage = Some(mode.into());
3682 self
3683 }
3684
3685 pub fn with_embedding_cache_storage(
3687 mut self,
3688 embedding_cache_storage: impl Into<String>,
3689 ) -> Self {
3690 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3691 self
3692 }
3693
3694 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3696 self.enable_config_discovery = Some(enable);
3697 self
3698 }
3699
3700 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3702 self.skip_embedding_retrieval = Some(value);
3703 self
3704 }
3705
3706 pub fn with_organization_custom_instructions(
3708 mut self,
3709 instructions: impl Into<String>,
3710 ) -> Self {
3711 self.organization_custom_instructions = Some(instructions.into());
3712 self
3713 }
3714
3715 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3717 self.enable_on_demand_instruction_discovery = Some(value);
3718 self
3719 }
3720
3721 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3723 self.enable_file_hooks = Some(value);
3724 self
3725 }
3726
3727 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3729 self.enable_host_git_operations = Some(value);
3730 self
3731 }
3732
3733 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3735 self.enable_session_store = Some(value);
3736 self
3737 }
3738
3739 pub fn with_enable_skills(mut self, value: bool) -> Self {
3741 self.enable_skills = Some(value);
3742 self
3743 }
3744
3745 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3751 self.enable_mcp_apps = Some(enable);
3752 self
3753 }
3754
3755 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3757 where
3758 I: IntoIterator<Item = P>,
3759 P: Into<PathBuf>,
3760 {
3761 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3762 self
3763 }
3764
3765 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3769 where
3770 I: IntoIterator<Item = P>,
3771 P: Into<PathBuf>,
3772 {
3773 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3774 self
3775 }
3776
3777 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3779 where
3780 I: IntoIterator<Item = P>,
3781 P: Into<PathBuf>,
3782 {
3783 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3784 self
3785 }
3786
3787 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3789 self.large_output = Some(config);
3790 self
3791 }
3792
3793 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
3796 self.tool_search = Some(config);
3797 self
3798 }
3799
3800 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3802 where
3803 I: IntoIterator<Item = S>,
3804 S: Into<String>,
3805 {
3806 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3807 self
3808 }
3809
3810 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3812 mut self,
3813 agents: I,
3814 ) -> Self {
3815 self.custom_agents = Some(agents.into_iter().collect());
3816 self
3817 }
3818
3819 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3821 self.default_agent = Some(agent);
3822 self
3823 }
3824
3825 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3827 self.agent = Some(name.into());
3828 self
3829 }
3830
3831 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3833 self.infinite_sessions = Some(config);
3834 self
3835 }
3836
3837 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3839 self.provider = Some(provider);
3840 self
3841 }
3842
3843 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3845 self.capi = Some(capi);
3846 self
3847 }
3848
3849 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3855 self.providers = Some(providers);
3856 self
3857 }
3858
3859 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3865 self.models = Some(models);
3866 self
3867 }
3868
3869 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3873 self.enable_session_telemetry = Some(enable);
3874 self
3875 }
3876
3877 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3879 self.enable_citations = Some(enable);
3880 self
3881 }
3882
3883 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3885 self.session_limits = Some(limits);
3886 self
3887 }
3888
3889 pub fn with_model_capabilities(
3891 mut self,
3892 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3893 ) -> Self {
3894 self.model_capabilities = Some(capabilities);
3895 self
3896 }
3897
3898 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3900 self.memory = Some(memory);
3901 self
3902 }
3903
3904 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3906 self.config_directory = Some(dir.into());
3907 self
3908 }
3909
3910 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3912 self.working_directory = Some(dir.into());
3913 self
3914 }
3915
3916 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3920 self.github_token = Some(token.into());
3921 self
3922 }
3923
3924 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3926 self.include_sub_agent_streaming_events = Some(include);
3927 self
3928 }
3929
3930 pub fn with_remote_session(
3932 mut self,
3933 mode: crate::generated::api_types::RemoteSessionMode,
3934 ) -> Self {
3935 self.remote_session = Some(mode);
3936 self
3937 }
3938
3939 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
3942 self.suppress_resume_event = Some(suppress);
3943 self
3944 }
3945
3946 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
3952 self.continue_pending_work = Some(continue_pending);
3953 self
3954 }
3955
3956 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3958 self.skip_custom_instructions = Some(value);
3959 self
3960 }
3961
3962 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3964 self.custom_agents_local_only = Some(value);
3965 self
3966 }
3967
3968 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3970 self.coauthor_enabled = Some(value);
3971 self
3972 }
3973
3974 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3976 self.manage_schedule_enabled = Some(value);
3977 self
3978 }
3979
3980 #[doc(hidden)]
3984 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
3985 self.exp_assignments = Some(assignments);
3986 self
3987 }
3988
3989 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3992 self.enable_managed_settings = Some(enabled);
3993 self
3994 }
3995}
3996
3997#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4003#[serde(rename_all = "camelCase")]
4004#[non_exhaustive]
4005pub struct SystemMessageConfig {
4006 #[serde(skip_serializing_if = "Option::is_none")]
4008 pub mode: Option<String>,
4009 #[serde(skip_serializing_if = "Option::is_none")]
4011 pub content: Option<String>,
4012 #[serde(skip_serializing_if = "Option::is_none")]
4014 pub sections: Option<HashMap<String, SectionOverride>>,
4015}
4016
4017impl SystemMessageConfig {
4018 pub fn new() -> Self {
4021 Self::default()
4022 }
4023
4024 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4027 self.mode = Some(mode.into());
4028 self
4029 }
4030
4031 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4034 self.content = Some(content.into());
4035 self
4036 }
4037
4038 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4040 self.sections = Some(sections);
4041 self
4042 }
4043}
4044
4045#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4051#[serde(rename_all = "camelCase")]
4052pub struct SectionOverride {
4053 #[serde(skip_serializing_if = "Option::is_none")]
4056 pub action: Option<String>,
4057 #[serde(skip_serializing_if = "Option::is_none")]
4059 pub content: Option<String>,
4060}
4061
4062#[derive(Debug, Clone, Serialize, Deserialize)]
4064#[serde(rename_all = "camelCase")]
4065pub struct CreateSessionResult {
4066 pub session_id: SessionId,
4068 #[serde(skip_serializing_if = "Option::is_none")]
4070 pub workspace_path: Option<PathBuf>,
4071 #[serde(default, alias = "remote_url")]
4073 pub remote_url: Option<String>,
4074 #[serde(skip_serializing_if = "Option::is_none")]
4076 pub capabilities: Option<SessionCapabilities>,
4077}
4078
4079#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4081#[serde(rename_all = "camelCase")]
4082pub(crate) struct ResumeSessionResult {
4083 #[serde(default)]
4085 pub session_id: Option<SessionId>,
4086 #[serde(default, skip_serializing_if = "Option::is_none")]
4088 pub workspace_path: Option<PathBuf>,
4089 #[serde(default, alias = "remote_url")]
4091 pub remote_url: Option<String>,
4092 #[serde(default, skip_serializing_if = "Option::is_none")]
4094 pub capabilities: Option<SessionCapabilities>,
4095 #[serde(
4097 default,
4098 alias = "openCanvasInstances",
4099 skip_serializing_if = "Option::is_none"
4100 )]
4101 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4102}
4103
4104#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4106#[serde(rename_all = "lowercase")]
4107pub enum LogLevel {
4108 #[default]
4110 Info,
4111 Warning,
4113 Error,
4115}
4116
4117#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4122#[serde(rename_all = "camelCase")]
4123pub struct LogOptions {
4124 #[serde(skip_serializing_if = "Option::is_none")]
4126 pub level: Option<LogLevel>,
4127 #[serde(skip_serializing_if = "Option::is_none")]
4130 pub ephemeral: Option<bool>,
4131}
4132
4133impl LogOptions {
4134 pub fn with_level(mut self, level: LogLevel) -> Self {
4136 self.level = Some(level);
4137 self
4138 }
4139
4140 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4142 self.ephemeral = Some(ephemeral);
4143 self
4144 }
4145}
4146
4147#[derive(Debug, Clone, Default)]
4151pub struct SetModelOptions {
4152 pub reasoning_effort: Option<String>,
4155 pub reasoning_summary: Option<ReasoningSummary>,
4159 pub context_tier: Option<ContextTier>,
4162 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4166}
4167
4168impl SetModelOptions {
4169 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4171 self.reasoning_effort = Some(effort.into());
4172 self
4173 }
4174
4175 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4177 self.reasoning_summary = Some(summary);
4178 self
4179 }
4180
4181 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4183 self.context_tier = Some(tier);
4184 self
4185 }
4186
4187 pub fn with_model_capabilities(
4189 mut self,
4190 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4191 ) -> Self {
4192 self.model_capabilities = Some(caps);
4193 self
4194 }
4195}
4196
4197#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4204#[serde(rename_all = "camelCase")]
4205pub struct PingResponse {
4206 #[serde(default)]
4208 pub message: String,
4209 #[serde(default)]
4211 pub timestamp: String,
4212 #[serde(skip_serializing_if = "Option::is_none")]
4214 pub protocol_version: Option<u32>,
4215}
4216
4217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4219#[serde(rename_all = "camelCase")]
4220pub struct AttachmentLineRange {
4221 pub start: u32,
4223 pub end: u32,
4225}
4226
4227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4229#[serde(rename_all = "camelCase")]
4230pub struct AttachmentSelectionPosition {
4231 pub line: u32,
4233 pub character: u32,
4235}
4236
4237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4239#[serde(rename_all = "camelCase")]
4240pub struct AttachmentSelectionRange {
4241 pub start: AttachmentSelectionPosition,
4243 pub end: AttachmentSelectionPosition,
4245}
4246
4247#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4249#[serde(rename_all = "snake_case")]
4250#[non_exhaustive]
4251pub enum GitHubReferenceType {
4252 Issue,
4254 Pr,
4256 Discussion,
4258}
4259
4260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4266#[serde(rename_all = "camelCase")]
4267pub struct GitHubRepoPointer {
4268 #[serde(skip_serializing_if = "Option::is_none")]
4270 pub id: Option<i64>,
4271 pub name: String,
4273 pub owner: String,
4275}
4276
4277#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4279#[serde(rename_all = "camelCase")]
4280pub struct GitHubFileDiffSide {
4281 pub path: String,
4283 pub r#ref: String,
4285 pub repo: GitHubRepoPointer,
4287}
4288
4289#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4291#[serde(rename_all = "camelCase")]
4292pub struct GitHubTreeComparisonSide {
4293 pub repo: GitHubRepoPointer,
4295 pub revision: String,
4297}
4298
4299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4301#[serde(rename_all = "camelCase")]
4302pub struct GitHubSnippetLineRange {
4303 pub start: i64,
4305 pub end: i64,
4307}
4308
4309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4311#[serde(
4312 tag = "type",
4313 rename_all = "camelCase",
4314 rename_all_fields = "camelCase"
4315)]
4316#[non_exhaustive]
4317pub enum Attachment {
4318 File {
4320 path: PathBuf,
4322 #[serde(skip_serializing_if = "Option::is_none")]
4324 display_name: Option<String>,
4325 #[serde(skip_serializing_if = "Option::is_none")]
4327 line_range: Option<AttachmentLineRange>,
4328 },
4329 Directory {
4331 path: PathBuf,
4333 #[serde(skip_serializing_if = "Option::is_none")]
4335 display_name: Option<String>,
4336 },
4337 Selection {
4339 file_path: PathBuf,
4341 text: String,
4343 #[serde(skip_serializing_if = "Option::is_none")]
4345 display_name: Option<String>,
4346 selection: AttachmentSelectionRange,
4348 },
4349 Blob {
4351 data: String,
4353 mime_type: String,
4355 #[serde(skip_serializing_if = "Option::is_none")]
4357 display_name: Option<String>,
4358 },
4359 #[serde(rename = "github_reference")]
4361 GitHubReference {
4362 number: u64,
4364 title: String,
4366 reference_type: GitHubReferenceType,
4368 state: String,
4370 url: String,
4372 },
4373 #[serde(rename = "github_commit")]
4375 GitHubCommit {
4376 message: String,
4378 oid: String,
4380 repo: GitHubRepoPointer,
4382 url: String,
4384 },
4385 #[serde(rename = "github_release")]
4387 GitHubRelease {
4388 name: String,
4390 repo: GitHubRepoPointer,
4392 tag_name: String,
4394 url: String,
4396 },
4397 #[serde(rename = "github_actions_job")]
4399 GitHubActionsJob {
4400 #[serde(skip_serializing_if = "Option::is_none")]
4403 conclusion: Option<String>,
4404 job_id: i64,
4406 job_name: String,
4408 repo: GitHubRepoPointer,
4410 url: String,
4412 workflow_name: String,
4414 },
4415 #[serde(rename = "github_repository")]
4417 GitHubRepository {
4418 #[serde(skip_serializing_if = "Option::is_none")]
4420 description: Option<String>,
4421 #[serde(skip_serializing_if = "Option::is_none")]
4424 r#ref: Option<String>,
4425 repo: GitHubRepoPointer,
4427 url: String,
4429 },
4430 #[serde(rename = "github_file_diff")]
4432 GitHubFileDiff {
4433 #[serde(skip_serializing_if = "Option::is_none")]
4435 base: Option<GitHubFileDiffSide>,
4436 #[serde(skip_serializing_if = "Option::is_none")]
4438 head: Option<GitHubFileDiffSide>,
4439 url: String,
4441 },
4442 #[serde(rename = "github_tree_comparison")]
4444 GitHubTreeComparison {
4445 base: GitHubTreeComparisonSide,
4447 head: GitHubTreeComparisonSide,
4449 url: String,
4451 },
4452 #[serde(rename = "github_url")]
4454 GitHubUrl {
4455 url: String,
4457 },
4458 #[serde(rename = "github_file")]
4460 GitHubFile {
4461 path: String,
4463 r#ref: String,
4465 repo: GitHubRepoPointer,
4467 url: String,
4469 },
4470 #[serde(rename = "github_snippet")]
4472 GitHubSnippet {
4473 line_range: GitHubSnippetLineRange,
4475 path: String,
4477 r#ref: String,
4479 repo: GitHubRepoPointer,
4481 url: String,
4483 },
4484}
4485
4486impl Attachment {
4487 pub fn display_name(&self) -> Option<&str> {
4489 match self {
4490 Self::File { display_name, .. }
4491 | Self::Directory { display_name, .. }
4492 | Self::Selection { display_name, .. }
4493 | Self::Blob { display_name, .. } => display_name.as_deref(),
4494 Self::GitHubReference { .. }
4495 | Self::GitHubCommit { .. }
4496 | Self::GitHubRelease { .. }
4497 | Self::GitHubActionsJob { .. }
4498 | Self::GitHubRepository { .. }
4499 | Self::GitHubFileDiff { .. }
4500 | Self::GitHubTreeComparison { .. }
4501 | Self::GitHubUrl { .. }
4502 | Self::GitHubFile { .. }
4503 | Self::GitHubSnippet { .. } => None,
4504 }
4505 }
4506
4507 pub fn label(&self) -> Option<String> {
4509 if let Some(display_name) = self
4510 .display_name()
4511 .map(str::trim)
4512 .filter(|name| !name.is_empty())
4513 {
4514 return Some(display_name.to_string());
4515 }
4516
4517 match self {
4518 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4519 format!("#{}", number)
4520 } else {
4521 title.trim().to_string()
4522 }),
4523 _ => self.derived_display_name(),
4524 }
4525 }
4526
4527 pub fn ensure_display_name(&mut self) {
4529 if self
4530 .display_name()
4531 .map(str::trim)
4532 .is_some_and(|name| !name.is_empty())
4533 {
4534 return;
4535 }
4536
4537 let Some(derived_display_name) = self.derived_display_name() else {
4538 return;
4539 };
4540
4541 match self {
4542 Self::File { display_name, .. }
4543 | Self::Directory { display_name, .. }
4544 | Self::Selection { display_name, .. }
4545 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4546 Self::GitHubReference { .. }
4547 | Self::GitHubCommit { .. }
4548 | Self::GitHubRelease { .. }
4549 | Self::GitHubActionsJob { .. }
4550 | Self::GitHubRepository { .. }
4551 | Self::GitHubFileDiff { .. }
4552 | Self::GitHubTreeComparison { .. }
4553 | Self::GitHubUrl { .. }
4554 | Self::GitHubFile { .. }
4555 | Self::GitHubSnippet { .. } => {}
4556 }
4557 }
4558
4559 fn derived_display_name(&self) -> Option<String> {
4560 match self {
4561 Self::File { path, .. } | Self::Directory { path, .. } => {
4562 Some(attachment_name_from_path(path))
4563 }
4564 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4565 Self::Blob { .. } => Some("attachment".to_string()),
4566 Self::GitHubReference { .. }
4567 | Self::GitHubCommit { .. }
4568 | Self::GitHubRelease { .. }
4569 | Self::GitHubActionsJob { .. }
4570 | Self::GitHubRepository { .. }
4571 | Self::GitHubFileDiff { .. }
4572 | Self::GitHubTreeComparison { .. }
4573 | Self::GitHubUrl { .. }
4574 | Self::GitHubFile { .. }
4575 | Self::GitHubSnippet { .. } => None,
4576 }
4577 }
4578}
4579
4580fn attachment_name_from_path(path: &Path) -> String {
4581 path.file_name()
4582 .map(|name| name.to_string_lossy().into_owned())
4583 .filter(|name| !name.is_empty())
4584 .unwrap_or_else(|| {
4585 let full = path.to_string_lossy();
4586 if full.is_empty() {
4587 "attachment".to_string()
4588 } else {
4589 full.into_owned()
4590 }
4591 })
4592}
4593
4594pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
4596 for attachment in attachments {
4597 attachment.ensure_display_name();
4598 }
4599}
4600
4601#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4606#[serde(rename_all = "lowercase")]
4607#[non_exhaustive]
4608pub enum DeliveryMode {
4609 Enqueue,
4611 Immediate,
4613}
4614
4615#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4620#[serde(rename_all = "lowercase")]
4621#[non_exhaustive]
4622pub enum AgentMode {
4623 Interactive,
4625 Plan,
4627 Autopilot,
4629 Shell,
4631}
4632
4633#[derive(Debug, Clone)]
4662#[non_exhaustive]
4663pub struct MessageOptions {
4664 pub prompt: String,
4666 pub mode: Option<DeliveryMode>,
4672 pub agent_mode: Option<AgentMode>,
4676 pub attachments: Option<Vec<Attachment>>,
4678 pub wait_timeout: Option<Duration>,
4681 pub request_headers: Option<HashMap<String, String>>,
4685 pub traceparent: Option<String>,
4692 pub tracestate: Option<String>,
4696 pub display_prompt: Option<String>,
4698}
4699
4700impl MessageOptions {
4701 pub fn new(prompt: impl Into<String>) -> Self {
4703 Self {
4704 prompt: prompt.into(),
4705 mode: None,
4706 agent_mode: None,
4707 attachments: None,
4708 wait_timeout: None,
4709 request_headers: None,
4710 traceparent: None,
4711 tracestate: None,
4712 display_prompt: None,
4713 }
4714 }
4715
4716 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
4722 self.mode = Some(mode);
4723 self
4724 }
4725
4726 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
4730 self.agent_mode = Some(agent_mode);
4731 self
4732 }
4733
4734 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
4736 self.attachments = Some(attachments);
4737 self
4738 }
4739
4740 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
4742 self.wait_timeout = Some(timeout);
4743 self
4744 }
4745
4746 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
4748 self.request_headers = Some(headers);
4749 self
4750 }
4751
4752 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
4757 self.traceparent = ctx.traceparent;
4758 self.tracestate = ctx.tracestate;
4759 self
4760 }
4761
4762 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
4764 self.traceparent = Some(traceparent.into());
4765 self
4766 }
4767
4768 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
4770 self.tracestate = Some(tracestate.into());
4771 self
4772 }
4773
4774 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
4776 self.display_prompt = Some(display_prompt.into());
4777 self
4778 }
4779}
4780
4781impl From<&str> for MessageOptions {
4782 fn from(prompt: &str) -> Self {
4783 Self::new(prompt)
4784 }
4785}
4786
4787impl From<String> for MessageOptions {
4788 fn from(prompt: String) -> Self {
4789 Self::new(prompt)
4790 }
4791}
4792
4793impl From<&String> for MessageOptions {
4794 fn from(prompt: &String) -> Self {
4795 Self::new(prompt.clone())
4796 }
4797}
4798
4799#[derive(Debug, Clone, Serialize, Deserialize)]
4801#[serde(rename_all = "camelCase")]
4802#[non_exhaustive]
4803pub struct GetStatusResponse {
4804 pub version: String,
4806 pub protocol_version: u32,
4808}
4809
4810#[derive(Debug, Clone, Serialize, Deserialize)]
4812#[serde(rename_all = "camelCase")]
4813#[non_exhaustive]
4814pub struct GetAuthStatusResponse {
4815 pub is_authenticated: bool,
4817 #[serde(skip_serializing_if = "Option::is_none")]
4820 pub auth_type: Option<String>,
4821 #[serde(skip_serializing_if = "Option::is_none")]
4823 pub host: Option<String>,
4824 #[serde(skip_serializing_if = "Option::is_none")]
4826 pub login: Option<String>,
4827 #[serde(skip_serializing_if = "Option::is_none")]
4829 pub status_message: Option<String>,
4830}
4831
4832#[derive(Debug, Clone, Serialize, Deserialize)]
4836#[serde(rename_all = "camelCase")]
4837pub struct SessionEventNotification {
4838 pub session_id: SessionId,
4840 pub event: SessionEvent,
4842}
4843
4844#[derive(Debug, Clone, Serialize, Deserialize)]
4851#[serde(rename_all = "camelCase")]
4852pub struct SessionEvent {
4853 pub id: String,
4855 pub timestamp: String,
4857 pub parent_id: Option<String>,
4859 #[serde(skip_serializing_if = "Option::is_none")]
4861 pub ephemeral: Option<bool>,
4862 #[serde(skip_serializing_if = "Option::is_none")]
4865 pub agent_id: Option<String>,
4866 #[serde(skip_serializing_if = "Option::is_none")]
4868 pub debug_cli_received_at_ms: Option<i64>,
4869 #[serde(skip_serializing_if = "Option::is_none")]
4871 pub debug_ws_forwarded_at_ms: Option<i64>,
4872 #[serde(rename = "type")]
4874 pub event_type: String,
4875 pub data: Value,
4877}
4878
4879impl SessionEvent {
4880 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
4885 use serde::de::IntoDeserializer;
4886 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
4887 self.event_type.as_str().into_deserializer();
4888 crate::generated::SessionEventType::deserialize(deserializer)
4889 .unwrap_or(crate::generated::SessionEventType::Unknown)
4890 }
4891
4892 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
4898 serde_json::from_value(self.data.clone()).ok()
4899 }
4900
4901 pub fn is_transient_error(&self) -> bool {
4905 self.event_type == "session.error"
4906 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
4907 }
4908}
4909
4910#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4915#[serde(rename_all = "camelCase")]
4916#[non_exhaustive]
4917pub struct ToolInvocation {
4918 pub session_id: SessionId,
4920 pub tool_call_id: String,
4922 pub tool_name: String,
4924 pub arguments: Value,
4926 #[serde(skip)]
4934 pub available_tools: Option<Vec<CurrentToolMetadata>>,
4935 #[serde(default, skip_serializing_if = "Option::is_none")]
4940 pub traceparent: Option<String>,
4941 #[serde(default, skip_serializing_if = "Option::is_none")]
4944 pub tracestate: Option<String>,
4945}
4946
4947impl ToolInvocation {
4948 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
4969 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
4970 }
4971
4972 pub fn trace_context(&self) -> TraceContext {
4975 TraceContext {
4976 traceparent: self.traceparent.clone(),
4977 tracestate: self.tracestate.clone(),
4978 }
4979 }
4980}
4981
4982#[derive(Debug, Clone, Serialize, Deserialize)]
4984#[serde(rename_all = "camelCase")]
4985pub struct ToolBinaryResult {
4986 pub data: String,
4988 pub mime_type: String,
4990 pub r#type: String,
4992 #[serde(default, skip_serializing_if = "Option::is_none")]
4994 pub description: Option<String>,
4995}
4996
4997#[derive(Debug, Clone, Serialize, Deserialize)]
5004#[serde(rename_all = "camelCase")]
5005#[non_exhaustive]
5006pub struct ToolResultExpanded {
5007 pub text_result_for_llm: String,
5009 pub result_type: String,
5011 #[serde(default, skip_serializing_if = "Option::is_none")]
5013 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5014 #[serde(skip_serializing_if = "Option::is_none")]
5016 pub session_log: Option<String>,
5017 #[serde(skip_serializing_if = "Option::is_none")]
5019 pub error: Option<String>,
5020 #[serde(default, skip_serializing_if = "Option::is_none")]
5022 pub tool_telemetry: Option<HashMap<String, Value>>,
5023 #[serde(default, skip_serializing_if = "Option::is_none")]
5025 pub tool_references: Option<Vec<String>>,
5026}
5027
5028impl ToolResultExpanded {
5029 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5033 Self {
5034 text_result_for_llm: text_result_for_llm.into(),
5035 result_type: result_type.into(),
5036 binary_results_for_llm: None,
5037 session_log: None,
5038 error: None,
5039 tool_telemetry: None,
5040 tool_references: None,
5041 }
5042 }
5043
5044 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5046 self.binary_results_for_llm = Some(results);
5047 self
5048 }
5049
5050 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5052 self.session_log = Some(session_log.into());
5053 self
5054 }
5055
5056 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5058 self.error = Some(error.into());
5059 self
5060 }
5061
5062 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5064 self.tool_telemetry = Some(telemetry);
5065 self
5066 }
5067
5068 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5070 where
5071 I: IntoIterator<Item = S>,
5072 S: Into<String>,
5073 {
5074 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5075 self
5076 }
5077}
5078
5079#[derive(Debug, Clone, Serialize, Deserialize)]
5081#[serde(untagged)]
5082#[non_exhaustive]
5083pub enum ToolResult {
5084 Text(String),
5086 Expanded(ToolResultExpanded),
5088}
5089
5090#[derive(Debug, Clone, Serialize, Deserialize)]
5092#[serde(rename_all = "camelCase")]
5093pub struct ToolResultResponse {
5094 pub result: ToolResult,
5096}
5097
5098#[derive(Debug, Clone, Serialize, Deserialize)]
5100#[serde(rename_all = "camelCase")]
5101pub struct SessionMetadata {
5102 pub session_id: SessionId,
5104 pub start_time: String,
5106 pub modified_time: String,
5108 #[serde(skip_serializing_if = "Option::is_none")]
5110 pub summary: Option<String>,
5111 pub is_remote: bool,
5113}
5114
5115#[derive(Debug, Clone, Serialize, Deserialize)]
5117#[serde(rename_all = "camelCase")]
5118pub struct ListSessionsResponse {
5119 pub sessions: Vec<SessionMetadata>,
5121}
5122
5123#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5127#[serde(rename_all = "camelCase")]
5128pub struct SessionListFilter {
5129 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5131 pub working_directory: Option<String>,
5132 #[serde(default, skip_serializing_if = "Option::is_none")]
5134 pub git_root: Option<String>,
5135 #[serde(default, skip_serializing_if = "Option::is_none")]
5137 pub repository: Option<String>,
5138 #[serde(default, skip_serializing_if = "Option::is_none")]
5140 pub branch: Option<String>,
5141}
5142
5143#[derive(Debug, Clone, Serialize, Deserialize)]
5145#[serde(rename_all = "camelCase")]
5146pub struct GetSessionMetadataResponse {
5147 #[serde(skip_serializing_if = "Option::is_none")]
5149 pub session: Option<SessionMetadata>,
5150}
5151
5152#[derive(Debug, Clone, Serialize, Deserialize)]
5154#[serde(rename_all = "camelCase")]
5155pub struct GetLastSessionIdResponse {
5156 #[serde(skip_serializing_if = "Option::is_none")]
5158 pub session_id: Option<SessionId>,
5159}
5160
5161#[derive(Debug, Clone, Serialize, Deserialize)]
5163#[serde(rename_all = "camelCase")]
5164pub struct GetForegroundSessionResponse {
5165 #[serde(skip_serializing_if = "Option::is_none")]
5167 pub session_id: Option<SessionId>,
5168}
5169
5170#[derive(Debug, Clone, Serialize, Deserialize)]
5172#[serde(rename_all = "camelCase")]
5173pub struct GetMessagesResponse {
5174 pub events: Vec<SessionEvent>,
5176}
5177
5178#[derive(Debug, Clone, Serialize, Deserialize)]
5180#[serde(rename_all = "camelCase")]
5181pub struct ElicitationResult {
5182 pub action: String,
5184 #[serde(skip_serializing_if = "Option::is_none")]
5186 pub content: Option<Value>,
5187}
5188
5189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5195#[serde(rename_all = "camelCase")]
5196#[non_exhaustive]
5197pub enum ElicitationMode {
5198 Form,
5200 Url,
5202 #[serde(other)]
5204 Unknown,
5205}
5206
5207#[derive(Debug, Clone, Serialize, Deserialize)]
5214#[serde(rename_all = "camelCase")]
5215pub struct ElicitationRequest {
5216 pub message: String,
5218 #[serde(skip_serializing_if = "Option::is_none")]
5220 pub requested_schema: Option<Value>,
5221 #[serde(skip_serializing_if = "Option::is_none")]
5223 pub mode: Option<ElicitationMode>,
5224 #[serde(skip_serializing_if = "Option::is_none")]
5226 pub elicitation_source: Option<String>,
5227 #[serde(skip_serializing_if = "Option::is_none")]
5229 pub url: Option<String>,
5230}
5231
5232#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5237#[serde(rename_all = "camelCase")]
5238pub struct SessionCapabilities {
5239 #[serde(skip_serializing_if = "Option::is_none")]
5241 pub ui: Option<UiCapabilities>,
5242}
5243
5244#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5246#[serde(rename_all = "camelCase")]
5247pub struct UiCapabilities {
5248 #[serde(skip_serializing_if = "Option::is_none")]
5250 pub elicitation: Option<bool>,
5251 #[serde(skip_serializing_if = "Option::is_none")]
5262 pub mcp_apps: Option<bool>,
5263 #[serde(skip_serializing_if = "Option::is_none")]
5265 pub canvases: Option<bool>,
5266}
5267
5268#[derive(Debug, Clone, Default)]
5270pub struct UiInputOptions<'a> {
5271 pub title: Option<&'a str>,
5273 pub description: Option<&'a str>,
5275 pub min_length: Option<u64>,
5277 pub max_length: Option<u64>,
5279 pub format: Option<InputFormat>,
5281 pub default: Option<&'a str>,
5283}
5284
5285#[derive(Debug, Clone, Copy)]
5287#[non_exhaustive]
5288pub enum InputFormat {
5289 Email,
5291 Uri,
5293 Date,
5295 DateTime,
5297}
5298
5299impl InputFormat {
5300 pub fn as_str(&self) -> &'static str {
5302 match self {
5303 Self::Email => "email",
5304 Self::Uri => "uri",
5305 Self::Date => "date",
5306 Self::DateTime => "date-time",
5307 }
5308 }
5309}
5310
5311pub use crate::generated::api_types::{
5316 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5317 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5318 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5319 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
5320};
5321
5322#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5328#[serde(rename_all = "kebab-case")]
5329#[non_exhaustive]
5330pub enum PermissionRequestKind {
5331 Shell,
5333 Write,
5335 Read,
5337 Url,
5339 Mcp,
5341 CustomTool,
5343 Memory,
5345 Hook,
5347 #[serde(other)]
5350 Unknown,
5351}
5352
5353#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5359#[serde(rename_all = "camelCase")]
5360pub struct PermissionRequestData {
5361 #[serde(default, skip_serializing_if = "Option::is_none")]
5365 pub kind: Option<PermissionRequestKind>,
5366 #[serde(default, skip_serializing_if = "Option::is_none")]
5369 pub tool_call_id: Option<String>,
5370 #[serde(flatten)]
5373 pub extra: Value,
5374}
5375
5376#[derive(Debug, Clone, Serialize, Deserialize)]
5378#[serde(rename_all = "camelCase")]
5379pub struct ExitPlanModeData {
5380 #[serde(default)]
5382 pub summary: String,
5383 #[serde(default, skip_serializing_if = "Option::is_none")]
5385 pub plan_content: Option<String>,
5386 #[serde(default)]
5388 pub actions: Vec<String>,
5389 #[serde(default = "default_recommended_action")]
5391 pub recommended_action: String,
5392}
5393
5394fn default_recommended_action() -> String {
5395 "autopilot".to_string()
5396}
5397
5398impl Default for ExitPlanModeData {
5399 fn default() -> Self {
5400 Self {
5401 summary: String::new(),
5402 plan_content: None,
5403 actions: Vec::new(),
5404 recommended_action: default_recommended_action(),
5405 }
5406 }
5407}
5408
5409#[cfg(test)]
5410mod tests {
5411 use std::path::PathBuf;
5412
5413 use serde_json::json;
5414
5415 use super::{
5416 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5417 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5418 CustomAgentConfig, DeliveryMode, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig,
5419 LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration,
5420 NamedProviderConfig, ProviderConfig, ProviderModelConfig, ReasoningSummary,
5421 ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, SystemMessageConfig, Tool,
5422 ToolBinaryResult, ToolResult, ToolResultExpanded, ToolResultResponse,
5423 ensure_attachment_display_names,
5424 };
5425 use crate::generated::session_events::TypedSessionEvent;
5426
5427 #[test]
5428 fn tool_builder_composes() {
5429 let tool = Tool::new("greet")
5430 .with_description("Say hello")
5431 .with_namespaced_name("hello/greet")
5432 .with_instructions("Pass the user's name")
5433 .with_parameters(json!({
5434 "type": "object",
5435 "properties": { "name": { "type": "string" } },
5436 "required": ["name"]
5437 }))
5438 .with_overrides_built_in_tool(true)
5439 .with_skip_permission(true);
5440 assert_eq!(tool.name, "greet");
5441 assert_eq!(tool.description, "Say hello");
5442 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5443 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5444 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5445 assert!(tool.overrides_built_in_tool);
5446 assert!(tool.skip_permission);
5447 }
5448
5449 #[test]
5450 fn tool_defer_serialization() {
5451 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5452 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5453 let value = serde_json::to_value(&tool).unwrap();
5454 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5455
5456 let plain = Tool::new("plain");
5457 let value = serde_json::to_value(&plain).unwrap();
5458 assert!(value.get("defer").is_none());
5459 }
5460
5461 #[test]
5462 fn tool_metadata_serialization() {
5463 use indexmap::IndexMap;
5464
5465 let mut metadata = IndexMap::new();
5466 metadata.insert(
5467 "github.com/copilot:safeForTelemetry".to_string(),
5468 json!({ "name": true, "inputsNames": false }),
5469 );
5470 let tool = Tool::new("lookup").with_metadata(metadata);
5471 let value = serde_json::to_value(&tool).unwrap();
5472 assert_eq!(
5473 value
5474 .get("metadata")
5475 .unwrap()
5476 .get("github.com/copilot:safeForTelemetry")
5477 .unwrap(),
5478 &json!({ "name": true, "inputsNames": false })
5479 );
5480
5481 let plain = Tool::new("plain");
5483 let value = serde_json::to_value(&plain).unwrap();
5484 assert!(value.get("metadata").is_none());
5485 }
5486
5487 #[test]
5488 fn custom_agent_config_builder_with_model() {
5489 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5490 .with_model("claude-haiku-4.5")
5491 .with_display_name("My Agent");
5492 assert_eq!(agent.name, "my-agent");
5493 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5494 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5495 }
5496
5497 #[test]
5498 fn custom_agent_config_serializes_model() {
5499 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5500 let wire = serde_json::to_value(&agent).unwrap();
5501 assert_eq!(wire["model"], "claude-haiku-4.5");
5502 assert_eq!(wire["name"], "model-agent");
5503 }
5504
5505 #[test]
5506 fn custom_agent_config_omits_model_when_none() {
5507 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5508 let wire = serde_json::to_value(&agent).unwrap();
5509 assert!(wire.get("model").is_none());
5510 }
5511
5512 #[test]
5513 #[should_panic(expected = "tool parameter schema must be a JSON object")]
5514 fn tool_with_parameters_panics_on_non_object_value() {
5515 let _ = Tool::new("noop").with_parameters(json!(null));
5516 }
5517
5518 #[test]
5519 fn tool_result_expanded_serializes_binary_results_for_llm() {
5520 let response = ToolResultResponse {
5521 result: ToolResult::Expanded(ToolResultExpanded {
5522 text_result_for_llm: "rendered chart".to_string(),
5523 result_type: "success".to_string(),
5524 binary_results_for_llm: Some(vec![ToolBinaryResult {
5525 data: "aW1n".to_string(),
5526 mime_type: "image/png".to_string(),
5527 r#type: "image".to_string(),
5528 description: Some("chart preview".to_string()),
5529 }]),
5530 session_log: None,
5531 error: None,
5532 tool_telemetry: None,
5533 tool_references: None,
5534 }),
5535 };
5536
5537 let wire = serde_json::to_value(&response).unwrap();
5538
5539 assert_eq!(
5540 wire,
5541 json!({
5542 "result": {
5543 "textResultForLlm": "rendered chart",
5544 "resultType": "success",
5545 "binaryResultsForLlm": [
5546 {
5547 "data": "aW1n",
5548 "mimeType": "image/png",
5549 "type": "image",
5550 "description": "chart preview"
5551 }
5552 ]
5553 }
5554 })
5555 );
5556 }
5557
5558 #[test]
5559 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
5560 let response = ToolResultResponse {
5561 result: ToolResult::Expanded(ToolResultExpanded {
5562 text_result_for_llm: "ok".to_string(),
5563 result_type: "success".to_string(),
5564 binary_results_for_llm: None,
5565 session_log: None,
5566 error: None,
5567 tool_telemetry: None,
5568 tool_references: None,
5569 }),
5570 };
5571
5572 let wire = serde_json::to_value(&response).unwrap();
5573
5574 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5575 assert!(wire["result"].get("binaryResultsForLlm").is_none());
5576 }
5577
5578 #[test]
5579 fn tool_result_expanded_serializes_tool_references() {
5580 let response = ToolResultResponse {
5581 result: ToolResult::Expanded(
5582 ToolResultExpanded::new("found 2 tools", "success")
5583 .with_tool_references(["get_weather", "check_status"]),
5584 ),
5585 };
5586
5587 let wire = serde_json::to_value(&response).unwrap();
5588
5589 assert_eq!(
5590 wire,
5591 json!({
5592 "result": {
5593 "textResultForLlm": "found 2 tools",
5594 "resultType": "success",
5595 "toolReferences": ["get_weather", "check_status"]
5596 }
5597 })
5598 );
5599 }
5600
5601 #[test]
5602 fn tool_result_expanded_omits_tool_references_when_none() {
5603 let response = ToolResultResponse {
5604 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
5605 };
5606
5607 let wire = serde_json::to_value(&response).unwrap();
5608
5609 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5610 assert!(wire["result"].get("toolReferences").is_none());
5611 }
5612
5613 #[test]
5614 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
5615 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
5618 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
5619
5620 assert_eq!(
5621 expanded.tool_references.as_deref(),
5622 Some(["alpha".to_string(), "beta".to_string()].as_slice())
5623 );
5624 }
5625
5626 #[test]
5627 fn tool_result_expanded_deserializes_tool_references() {
5628 let wire = json!({
5629 "textResultForLlm": "found tools",
5630 "resultType": "success",
5631 "toolReferences": ["alpha", "beta"]
5632 });
5633
5634 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
5635
5636 assert_eq!(
5637 expanded.tool_references.as_deref(),
5638 Some(["alpha".to_string(), "beta".to_string()].as_slice())
5639 );
5640 }
5641
5642 #[test]
5643 fn session_config_default_wire_flags_off_without_handlers() {
5644 let cfg = SessionConfig::default();
5645 assert_eq!(cfg.mcp_oauth_token_storage, None);
5646 let (wire, _runtime) = cfg
5650 .into_wire(Some(SessionId::from("default-flags")))
5651 .expect("default config has no duplicate handlers");
5652 assert!(!wire.request_user_input);
5653 assert!(!wire.request_permission);
5654 assert!(!wire.request_elicitation);
5655 assert!(!wire.request_exit_plan_mode);
5656 assert!(!wire.request_auto_mode_switch);
5657 assert!(!wire.hooks);
5658 assert!(!wire.request_mcp_apps);
5659 }
5660
5661 #[test]
5662 fn resume_session_config_new_wire_flags_off_without_handlers() {
5663 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
5664 assert_eq!(cfg.mcp_oauth_token_storage, None);
5665 let (wire, _runtime) = cfg
5666 .into_wire()
5667 .expect("default resume config has no duplicate handlers");
5668 assert!(!wire.request_user_input);
5669 assert!(!wire.request_permission);
5670 assert!(!wire.request_elicitation);
5671 assert!(!wire.request_exit_plan_mode);
5672 assert!(!wire.request_auto_mode_switch);
5673 assert!(!wire.hooks);
5674 assert!(!wire.request_mcp_apps);
5675 }
5676
5677 #[test]
5678 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5679 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
5680 assert_eq!(cfg.enable_mcp_apps, Some(true));
5681
5682 let (wire, _runtime) = cfg
5683 .into_wire(Some(SessionId::from("enable-mcp-apps")))
5684 .expect("enable_mcp_apps config has no duplicate handlers");
5685 assert!(wire.request_mcp_apps);
5686
5687 let json = serde_json::to_value(&wire).unwrap();
5688 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5689 }
5690
5691 #[test]
5692 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5693 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
5694 .with_enable_mcp_apps(true);
5695 assert_eq!(cfg.enable_mcp_apps, Some(true));
5696
5697 let (wire, _runtime) = cfg
5698 .into_wire()
5699 .expect("resume enable_mcp_apps config has no duplicate handlers");
5700 assert!(wire.request_mcp_apps);
5701
5702 let json = serde_json::to_value(&wire).unwrap();
5703 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5704 }
5705
5706 #[test]
5707 fn memory_configuration_constructors_and_serde() {
5708 assert!(MemoryConfiguration::enabled().enabled);
5709 assert!(!MemoryConfiguration::disabled().enabled);
5710 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
5711
5712 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
5713 assert_eq!(json, serde_json::json!({ "enabled": true }));
5714 }
5715
5716 #[test]
5717 fn session_config_with_memory_serializes() {
5718 let (wire, _runtime) = SessionConfig::default()
5719 .with_memory(MemoryConfiguration::enabled())
5720 .into_wire(Some(SessionId::from("memory-on")))
5721 .expect("no duplicate handlers");
5722 let json = serde_json::to_value(&wire).unwrap();
5723 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5724
5725 let (wire_off, _) = SessionConfig::default()
5726 .with_memory(MemoryConfiguration::disabled())
5727 .into_wire(Some(SessionId::from("memory-off")))
5728 .expect("no duplicate handlers");
5729 let json_off = serde_json::to_value(&wire_off).unwrap();
5730 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
5731
5732 let (empty_wire, _) = SessionConfig::default()
5734 .into_wire(Some(SessionId::from("memory-unset")))
5735 .expect("no duplicate handlers");
5736 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5737 assert!(empty_json.get("memory").is_none());
5738 }
5739
5740 #[test]
5741 fn resume_session_config_with_memory_serializes() {
5742 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
5743 .with_memory(MemoryConfiguration::enabled())
5744 .into_wire()
5745 .expect("no duplicate handlers");
5746 let json = serde_json::to_value(&wire).unwrap();
5747 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5748
5749 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
5751 .into_wire()
5752 .expect("no duplicate handlers");
5753 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5754 assert!(empty_json.get("memory").is_none());
5755 }
5756
5757 #[test]
5758 fn session_config_with_exp_assignments_serializes() {
5759 let assignments = serde_json::json!({
5760 "Parameters": { "copilot_exp_flag": "treatment" },
5761 "AssignmentContext": "ctx-123",
5762 });
5763 let (wire, _runtime) = SessionConfig::default()
5764 .with_exp_assignments(assignments.clone())
5765 .into_wire(Some(SessionId::from("exp-on")))
5766 .expect("no duplicate handlers");
5767 let json = serde_json::to_value(&wire).unwrap();
5768 assert_eq!(json["expAssignments"], assignments);
5769
5770 let (empty_wire, _) = SessionConfig::default()
5772 .into_wire(Some(SessionId::from("exp-unset")))
5773 .expect("no duplicate handlers");
5774 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5775 assert!(empty_json.get("expAssignments").is_none());
5776 }
5777
5778 #[test]
5779 fn resume_session_config_with_exp_assignments_serializes() {
5780 let assignments = serde_json::json!({
5781 "Parameters": { "copilot_exp_flag": "treatment" },
5782 "AssignmentContext": "ctx-456",
5783 });
5784 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
5785 .with_exp_assignments(assignments.clone())
5786 .into_wire()
5787 .expect("no duplicate handlers");
5788 let json = serde_json::to_value(&wire).unwrap();
5789 assert_eq!(json["expAssignments"], assignments);
5790
5791 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
5793 .into_wire()
5794 .expect("no duplicate handlers");
5795 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5796 assert!(empty_json.get("expAssignments").is_none());
5797 }
5798
5799 #[test]
5800 fn session_config_clone_preserves_exp_assignments() {
5801 let assignments = serde_json::json!({
5802 "Parameters": { "copilot_exp_flag": "treatment" },
5803 "AssignmentContext": "ctx-clone",
5804 });
5805 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
5806 let cloned = config.clone();
5807
5808 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5809
5810 let (wire, _runtime) = cloned
5811 .into_wire(Some(SessionId::from("exp-clone")))
5812 .expect("no duplicate handlers");
5813 let json = serde_json::to_value(&wire).unwrap();
5814 assert_eq!(json["expAssignments"], assignments);
5815 }
5816
5817 #[test]
5818 fn resume_session_config_clone_preserves_exp_assignments() {
5819 let assignments = serde_json::json!({
5820 "Parameters": { "copilot_exp_flag": "treatment" },
5821 "AssignmentContext": "ctx-clone-resume",
5822 });
5823 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
5824 .with_exp_assignments(assignments.clone());
5825 let cloned = config.clone();
5826
5827 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5828
5829 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
5830 let json = serde_json::to_value(&wire).unwrap();
5831 assert_eq!(json["expAssignments"], assignments);
5832 }
5833
5834 #[test]
5835 #[allow(clippy::field_reassign_with_default)]
5836 fn session_config_into_wire_serializes_bucket_b_fields() {
5837 use std::path::PathBuf;
5838
5839 use super::{CloudSessionOptions, CloudSessionRepository};
5840
5841 let mut cfg = SessionConfig::default();
5842 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5843 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5844 cfg.github_token = Some("ghs_secret".to_string());
5845 cfg.include_sub_agent_streaming_events = Some(false);
5846 cfg.enable_session_telemetry = Some(false);
5847 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
5848 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
5849 cfg.enable_on_demand_instruction_discovery = Some(false);
5850 cfg.cloud = Some(CloudSessionOptions::with_repository(
5851 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
5852 ));
5853
5854 let (wire, _runtime) = cfg
5855 .into_wire(Some(SessionId::from("custom-id")))
5856 .expect("no duplicate handlers");
5857 let wire_json = serde_json::to_value(&wire).unwrap();
5858 assert_eq!(wire_json["sessionId"], "custom-id");
5859 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5860 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5861 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5862 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
5863 assert_eq!(wire_json["enableSessionTelemetry"], false);
5864 assert_eq!(wire_json["reasoningSummary"], "concise");
5865 assert_eq!(wire_json["remoteSession"], "export");
5866 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5867 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
5868 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
5869 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
5870
5871 let (empty_wire, _) = SessionConfig::default()
5873 .into_wire(Some(SessionId::from("empty")))
5874 .expect("default has no duplicate handlers");
5875 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5876 assert!(empty_json.get("gitHubToken").is_none());
5877 assert!(empty_json.get("enableSessionTelemetry").is_none());
5878 assert!(empty_json.get("reasoningSummary").is_none());
5879 assert!(empty_json.get("remoteSession").is_none());
5880 assert!(
5881 empty_json
5882 .get("enableOnDemandInstructionDiscovery")
5883 .is_none()
5884 );
5885 assert!(empty_json.get("cloud").is_none());
5886 }
5887
5888 #[test]
5889 fn session_config_into_wire_serializes_named_providers_and_models() {
5890 let cfg = SessionConfig::default()
5891 .with_providers(vec![
5892 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
5893 .with_provider_type("openai")
5894 .with_wire_api("responses")
5895 .with_api_key("sk-test"),
5896 ])
5897 .with_models(vec![
5898 ProviderModelConfig::new("gpt-x", "my-openai")
5899 .with_wire_model("gpt-x-2025")
5900 .with_max_output_tokens(2048),
5901 ]);
5902
5903 let (wire, _) = cfg
5904 .into_wire(Some(SessionId::from("sess-providers")))
5905 .expect("no duplicate handlers");
5906 let wire_json = serde_json::to_value(&wire).unwrap();
5907 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
5908 assert_eq!(
5909 wire_json["providers"][0]["baseUrl"],
5910 "https://api.example.com/v1"
5911 );
5912 assert_eq!(wire_json["providers"][0]["type"], "openai");
5913 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
5914 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
5915 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
5916 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
5917 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
5918 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
5919
5920 let (empty_wire, _) = SessionConfig::default()
5921 .into_wire(Some(SessionId::from("empty")))
5922 .expect("default has no duplicate handlers");
5923 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5924 assert!(empty_json.get("providers").is_none());
5925 assert!(empty_json.get("models").is_none());
5926 }
5927
5928 #[test]
5929 fn resume_config_into_wire_serializes_named_providers_and_models() {
5930 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
5931 .with_providers(vec![
5932 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
5933 .with_provider_type("azure")
5934 .with_azure(AzureProviderOptions {
5935 api_version: Some("2024-10-21".to_string()),
5936 }),
5937 ])
5938 .with_models(vec![
5939 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
5940 ]);
5941
5942 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5943 let wire_json = serde_json::to_value(&wire).unwrap();
5944 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
5945 assert_eq!(wire_json["providers"][0]["type"], "azure");
5946 assert_eq!(
5947 wire_json["providers"][0]["azure"]["apiVersion"],
5948 "2024-10-21"
5949 );
5950 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
5951 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
5952 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
5953
5954 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
5955 .into_wire()
5956 .expect("default has no duplicate handlers");
5957 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5958 assert!(empty_json.get("providers").is_none());
5959 assert!(empty_json.get("models").is_none());
5960 }
5961
5962 #[test]
5963 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
5964 use std::path::PathBuf;
5965
5966 let cfg = SessionConfig {
5967 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
5968 large_output: Some(
5969 LargeToolOutputConfig::new()
5970 .with_enabled(true)
5971 .with_max_size_bytes(1024)
5972 .with_output_directory(PathBuf::from("/tmp/large-output")),
5973 ),
5974 ..Default::default()
5975 };
5976
5977 let (wire, _) = cfg
5978 .into_wire(Some(SessionId::from("sess-1")))
5979 .expect("no duplicate handlers");
5980 let wire_json = serde_json::to_value(&wire).unwrap();
5981 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
5982 assert_eq!(wire_json["largeOutput"]["enabled"], true);
5983 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
5984 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
5985
5986 let (empty_wire, _) = SessionConfig::default()
5987 .into_wire(Some(SessionId::from("empty")))
5988 .expect("default has no duplicate handlers");
5989 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5990 assert!(empty_json.get("pluginDirectories").is_none());
5991 assert!(empty_json.get("largeOutput").is_none());
5992 }
5993
5994 #[test]
5995 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
5996 use std::path::PathBuf;
5997
5998 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5999 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
6000 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
6001 cfg.github_token = Some("ghs_secret".to_string());
6002 cfg.include_sub_agent_streaming_events = Some(true);
6003 cfg.enable_session_telemetry = Some(false);
6004 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
6005 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
6006 cfg.enable_on_demand_instruction_discovery = Some(false);
6007
6008 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6009 let wire_json = serde_json::to_value(&wire).unwrap();
6010 assert_eq!(wire_json["sessionId"], "sess-1");
6011 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
6012 assert_eq!(wire_json["configDir"], "/tmp/cfg");
6013 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
6014 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
6015 assert_eq!(wire_json["enableSessionTelemetry"], false);
6016 assert_eq!(wire_json["reasoningSummary"], "detailed");
6017 assert_eq!(wire_json["remoteSession"], "on");
6018 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
6019
6020 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6022 .into_wire()
6023 .expect("default resume has no duplicate handlers");
6024 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6025 assert!(empty_json.get("reasoningSummary").is_none());
6026 assert!(empty_json.get("remoteSession").is_none());
6027 assert!(
6028 empty_json
6029 .get("enableOnDemandInstructionDiscovery")
6030 .is_none()
6031 );
6032 }
6033
6034 #[test]
6035 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
6036 use std::path::PathBuf;
6037
6038 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
6039 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6040 cfg.large_output = Some(
6041 LargeToolOutputConfig::new()
6042 .with_enabled(false)
6043 .with_max_size_bytes(2048)
6044 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6045 );
6046
6047 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6048 let wire_json = serde_json::to_value(&wire).unwrap();
6049 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6050 assert_eq!(wire_json["largeOutput"]["enabled"], false);
6051 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6052 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6053
6054 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6055 .into_wire()
6056 .expect("default resume has no duplicate handlers");
6057 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6058 assert!(empty_json.get("pluginDirectories").is_none());
6059 assert!(empty_json.get("largeOutput").is_none());
6060 }
6061
6062 #[test]
6063 fn session_config_builder_composes() {
6064 use indexmap::IndexMap;
6065
6066 let cfg = SessionConfig::default()
6067 .with_session_id(SessionId::from("sess-1"))
6068 .with_model("claude-sonnet-4")
6069 .with_client_name("test-app")
6070 .with_reasoning_effort("medium")
6071 .with_reasoning_summary(ReasoningSummary::Concise)
6072 .with_context_tier("long_context")
6073 .with_streaming(true)
6074 .with_tools([Tool::new("greet")])
6075 .with_available_tools(["bash", "view"])
6076 .with_excluded_tools(["dangerous"])
6077 .with_mcp_servers(IndexMap::new())
6078 .with_mcp_oauth_token_storage("persistent")
6079 .with_enable_config_discovery(true)
6080 .with_enable_on_demand_instruction_discovery(true)
6081 .with_skill_directories([PathBuf::from("/tmp/skills")])
6082 .with_disabled_skills(["broken-skill"])
6083 .with_agent("researcher")
6084 .with_config_directory(PathBuf::from("/tmp/config"))
6085 .with_working_directory(PathBuf::from("/tmp/work"))
6086 .with_github_token("ghp_test")
6087 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6088 .with_enable_session_telemetry(false)
6089 .with_include_sub_agent_streaming_events(false)
6090 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6091
6092 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
6093 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
6094 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6095 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
6096 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
6097 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
6098 assert_eq!(cfg.streaming, Some(true));
6099 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6100 assert_eq!(
6101 cfg.available_tools.as_deref(),
6102 Some(&["bash".to_string(), "view".to_string()][..])
6103 );
6104 assert_eq!(
6105 cfg.excluded_tools.as_deref(),
6106 Some(&["dangerous".to_string()][..])
6107 );
6108 assert!(cfg.mcp_servers.is_some());
6109 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6110 assert_eq!(cfg.enable_config_discovery, Some(true));
6111 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
6112 assert_eq!(
6113 cfg.skill_directories.as_deref(),
6114 Some(&[PathBuf::from("/tmp/skills")][..])
6115 );
6116 assert_eq!(
6117 cfg.disabled_skills.as_deref(),
6118 Some(&["broken-skill".to_string()][..])
6119 );
6120 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6121 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6122 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6123 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6124 assert_eq!(
6125 cfg.capi,
6126 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6127 );
6128 assert_eq!(cfg.enable_session_telemetry, Some(false));
6129 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
6130 assert_eq!(
6131 cfg.extension_info,
6132 Some(ExtensionInfo::new("github-app", "counter"))
6133 );
6134 }
6135
6136 #[test]
6137 fn resume_session_config_builder_composes() {
6138 use indexmap::IndexMap;
6139
6140 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
6141 .with_client_name("test-app")
6142 .with_reasoning_summary(ReasoningSummary::None)
6143 .with_context_tier("default")
6144 .with_streaming(true)
6145 .with_tools([Tool::new("greet")])
6146 .with_available_tools(["bash", "view"])
6147 .with_excluded_tools(["dangerous"])
6148 .with_mcp_servers(IndexMap::new())
6149 .with_mcp_oauth_token_storage("persistent")
6150 .with_enable_config_discovery(true)
6151 .with_enable_on_demand_instruction_discovery(false)
6152 .with_skill_directories([PathBuf::from("/tmp/skills")])
6153 .with_disabled_skills(["broken-skill"])
6154 .with_agent("researcher")
6155 .with_config_directory(PathBuf::from("/tmp/config"))
6156 .with_working_directory(PathBuf::from("/tmp/work"))
6157 .with_github_token("ghp_test")
6158 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6159 .with_enable_session_telemetry(false)
6160 .with_include_sub_agent_streaming_events(true)
6161 .with_suppress_resume_event(true)
6162 .with_continue_pending_work(true)
6163 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6164
6165 assert_eq!(cfg.session_id.as_str(), "sess-2");
6166 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6167 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
6168 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
6169 assert_eq!(cfg.streaming, Some(true));
6170 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6171 assert_eq!(
6172 cfg.available_tools.as_deref(),
6173 Some(&["bash".to_string(), "view".to_string()][..])
6174 );
6175 assert_eq!(
6176 cfg.excluded_tools.as_deref(),
6177 Some(&["dangerous".to_string()][..])
6178 );
6179 assert!(cfg.mcp_servers.is_some());
6180 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6181 assert_eq!(cfg.enable_config_discovery, Some(true));
6182 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
6183 assert_eq!(
6184 cfg.skill_directories.as_deref(),
6185 Some(&[PathBuf::from("/tmp/skills")][..])
6186 );
6187 assert_eq!(
6188 cfg.disabled_skills.as_deref(),
6189 Some(&["broken-skill".to_string()][..])
6190 );
6191 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6192 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6193 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6194 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6195 assert_eq!(
6196 cfg.capi,
6197 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6198 );
6199 assert_eq!(cfg.enable_session_telemetry, Some(false));
6200 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
6201 assert_eq!(cfg.suppress_resume_event, Some(true));
6202 assert_eq!(cfg.continue_pending_work, Some(true));
6203 assert_eq!(
6204 cfg.extension_info,
6205 Some(ExtensionInfo::new("github-app", "counter"))
6206 );
6207 }
6208
6209 #[test]
6213 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
6214 let cfg =
6215 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
6216 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6217 let json = serde_json::to_value(&wire).unwrap();
6218 assert_eq!(json["continuePendingWork"], true);
6219
6220 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6222 .into_wire()
6223 .expect("no duplicate handlers");
6224 let json = serde_json::to_value(&wire).unwrap();
6225 assert!(json.get("continuePendingWork").is_none());
6226 }
6227
6228 #[test]
6232 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
6233 let cfg =
6234 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
6235 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6236 let json = serde_json::to_value(&wire).unwrap();
6237 assert_eq!(json["disableResume"], true);
6238 assert!(json.get("suppressResumeEvent").is_none());
6239 }
6240
6241 #[test]
6244 fn session_config_serializes_instruction_directories_to_camel_case() {
6245 let cfg =
6246 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
6247 let (wire, _) = cfg
6248 .into_wire(Some(SessionId::from("instr-on")))
6249 .expect("no duplicate handlers");
6250 let json = serde_json::to_value(&wire).unwrap();
6251 assert_eq!(
6252 json["instructionDirectories"],
6253 serde_json::json!(["/tmp/instr"])
6254 );
6255
6256 let (wire, _) = SessionConfig::default()
6258 .into_wire(Some(SessionId::from("instr-off")))
6259 .expect("no duplicate handlers");
6260 let json = serde_json::to_value(&wire).unwrap();
6261 assert!(json.get("instructionDirectories").is_none());
6262 }
6263
6264 #[test]
6267 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
6268 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
6269 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
6270 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6271 let json = serde_json::to_value(&wire).unwrap();
6272 assert_eq!(
6273 json["instructionDirectories"],
6274 serde_json::json!(["/tmp/instr"])
6275 );
6276
6277 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6278 .into_wire()
6279 .expect("no duplicate handlers");
6280 let json = serde_json::to_value(&wire).unwrap();
6281 assert!(json.get("instructionDirectories").is_none());
6282 }
6283
6284 #[test]
6285 fn custom_agent_config_builder_composes() {
6286 use indexmap::IndexMap;
6287
6288 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
6289 .with_display_name("Research Assistant")
6290 .with_description("Investigates technical questions.")
6291 .with_tools(["bash", "view"])
6292 .with_mcp_servers(IndexMap::new())
6293 .with_infer(true)
6294 .with_skills(["rust-coding-skill"]);
6295
6296 assert_eq!(cfg.name, "researcher");
6297 assert_eq!(cfg.prompt, "You are a research assistant.");
6298 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
6299 assert_eq!(
6300 cfg.description.as_deref(),
6301 Some("Investigates technical questions.")
6302 );
6303 assert_eq!(
6304 cfg.tools.as_deref(),
6305 Some(&["bash".to_string(), "view".to_string()][..])
6306 );
6307 assert!(cfg.mcp_servers.is_some());
6308 assert_eq!(cfg.infer, Some(true));
6309 assert_eq!(
6310 cfg.skills.as_deref(),
6311 Some(&["rust-coding-skill".to_string()][..])
6312 );
6313 }
6314
6315 #[test]
6316 fn mcp_servers_serialize_in_insertion_order() {
6317 use indexmap::IndexMap;
6318
6319 let order = [
6325 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
6326 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
6327 ];
6328 let mut servers = IndexMap::new();
6329 for name in order {
6330 servers.insert(
6331 name.to_string(),
6332 McpServerConfig::Stdio(McpStdioServerConfig {
6333 command: "run".to_string(),
6334 ..Default::default()
6335 }),
6336 );
6337 }
6338
6339 let (wire, _runtime) = SessionConfig::default()
6340 .with_mcp_servers(servers)
6341 .into_wire(None)
6342 .expect("into_wire should succeed");
6343 let json = serde_json::to_string(&wire).expect("serialize wire");
6344
6345 let positions: Vec<usize> = order
6346 .iter()
6347 .map(|name| {
6348 json.find(&format!("\"{name}\""))
6349 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
6350 })
6351 .collect();
6352 let mut ascending = positions.clone();
6353 ascending.sort_unstable();
6354 assert_eq!(
6355 positions, ascending,
6356 "mcp server keys must serialize in insertion order: {json}"
6357 );
6358 }
6359
6360 #[test]
6361 fn infinite_session_config_builder_composes() {
6362 let cfg = InfiniteSessionConfig::new()
6363 .with_enabled(true)
6364 .with_background_compaction_threshold(0.75)
6365 .with_buffer_exhaustion_threshold(0.92);
6366
6367 assert_eq!(cfg.enabled, Some(true));
6368 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
6369 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
6370 }
6371
6372 #[test]
6373 fn provider_config_builder_composes() {
6374 use std::collections::HashMap;
6375
6376 let mut headers = HashMap::new();
6377 headers.insert("X-Custom".to_string(), "value".to_string());
6378
6379 let cfg = ProviderConfig::new("https://api.example.com")
6380 .with_provider_type("openai")
6381 .with_wire_api("completions")
6382 .with_transport("websockets")
6383 .with_api_key("sk-test")
6384 .with_bearer_token("bearer-test")
6385 .with_headers(headers)
6386 .with_model_id("gpt-4")
6387 .with_wire_model("azure-gpt-4-deployment")
6388 .with_max_prompt_tokens(8192)
6389 .with_max_output_tokens(2048);
6390
6391 assert_eq!(cfg.base_url, "https://api.example.com");
6392 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
6393 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
6394 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
6395 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
6396 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
6397 assert_eq!(
6398 cfg.headers
6399 .as_ref()
6400 .and_then(|h| h.get("X-Custom"))
6401 .map(String::as_str),
6402 Some("value"),
6403 );
6404 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
6405 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
6406 assert_eq!(cfg.max_prompt_tokens, Some(8192));
6407 assert_eq!(cfg.max_output_tokens, Some(2048));
6408
6409 let wire = serde_json::to_value(&cfg).unwrap();
6411 assert_eq!(wire["modelId"], "gpt-4");
6412 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
6413 assert_eq!(wire["maxPromptTokens"], 8192);
6414 assert_eq!(wire["maxOutputTokens"], 2048);
6415
6416 let unset = ProviderConfig::new("https://api.example.com");
6417 let wire_unset = serde_json::to_value(&unset).unwrap();
6418 assert!(wire_unset.get("modelId").is_none());
6419 assert!(wire_unset.get("wireModel").is_none());
6420 assert!(wire_unset.get("maxPromptTokens").is_none());
6421 assert!(wire_unset.get("maxOutputTokens").is_none());
6422 }
6423
6424 #[test]
6425 fn capi_session_options_builder_composes_and_serializes() {
6426 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
6427
6428 assert_eq!(cfg.enable_web_socket_responses, Some(false));
6429
6430 let wire = serde_json::to_value(&cfg).unwrap();
6431 assert_eq!(
6432 wire,
6433 serde_json::json!({ "enableWebSocketResponses": false })
6434 );
6435
6436 let unset = CapiSessionOptions::new();
6437 let wire_unset = serde_json::to_value(&unset).unwrap();
6438 assert!(wire_unset.get("enableWebSocketResponses").is_none());
6439 }
6440
6441 #[test]
6442 fn session_config_with_capi_serializes() {
6443 let (wire, _) = SessionConfig::default()
6444 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6445 .into_wire(Some(SessionId::from("capi-create")))
6446 .expect("no duplicate handlers");
6447 let json = serde_json::to_value(&wire).unwrap();
6448 assert_eq!(
6449 json["capi"],
6450 serde_json::json!({ "enableWebSocketResponses": false })
6451 );
6452
6453 let (empty_wire, _) = SessionConfig::default()
6454 .into_wire(Some(SessionId::from("capi-create-unset")))
6455 .expect("no duplicate handlers");
6456 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6457 assert!(empty_json.get("capi").is_none());
6458 }
6459
6460 #[test]
6461 fn resume_session_config_with_capi_serializes() {
6462 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
6463 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6464 .into_wire()
6465 .expect("no duplicate handlers");
6466 let json = serde_json::to_value(&wire).unwrap();
6467 assert_eq!(
6468 json["capi"],
6469 serde_json::json!({ "enableWebSocketResponses": false })
6470 );
6471
6472 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
6473 .into_wire()
6474 .expect("no duplicate handlers");
6475 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6476 assert!(empty_json.get("capi").is_none());
6477 }
6478
6479 #[test]
6480 fn system_message_config_builder_composes() {
6481 use std::collections::HashMap;
6482
6483 let cfg = SystemMessageConfig::new()
6484 .with_mode("replace")
6485 .with_content("Custom system message.")
6486 .with_sections(HashMap::new());
6487
6488 assert_eq!(cfg.mode.as_deref(), Some("replace"));
6489 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
6490 assert!(cfg.sections.is_some());
6491 }
6492
6493 #[test]
6494 fn delivery_mode_serializes_to_kebab_case_strings() {
6495 assert_eq!(
6496 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
6497 "\"enqueue\""
6498 );
6499 assert_eq!(
6500 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
6501 "\"immediate\""
6502 );
6503 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
6504 assert_eq!(parsed, DeliveryMode::Immediate);
6505 }
6506
6507 #[test]
6508 fn agent_mode_serializes_to_kebab_case_strings() {
6509 assert_eq!(
6510 serde_json::to_string(&AgentMode::Interactive).unwrap(),
6511 "\"interactive\""
6512 );
6513 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
6514 assert_eq!(
6515 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
6516 "\"autopilot\""
6517 );
6518 assert_eq!(
6519 serde_json::to_string(&AgentMode::Shell).unwrap(),
6520 "\"shell\""
6521 );
6522 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
6523 assert_eq!(parsed, AgentMode::Plan);
6524 }
6525
6526 #[test]
6527 fn connection_state_distinguishes_variants() {
6528 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
6531 }
6532
6533 #[test]
6539 fn session_event_round_trips_agent_id_on_envelope() {
6540 let wire = json!({
6541 "id": "evt-1",
6542 "timestamp": "2026-04-30T12:00:00Z",
6543 "parentId": null,
6544 "agentId": "sub-agent-42",
6545 "type": "assistant.message",
6546 "data": { "message": "hi" }
6547 });
6548
6549 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
6550 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6551
6552 let roundtripped = serde_json::to_value(&event).unwrap();
6554 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6555
6556 let main_agent_event: SessionEvent = serde_json::from_value(json!({
6558 "id": "evt-2",
6559 "timestamp": "2026-04-30T12:00:01Z",
6560 "parentId": null,
6561 "type": "session.idle",
6562 "data": {}
6563 }))
6564 .unwrap();
6565 assert!(main_agent_event.agent_id.is_none());
6566 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
6567 assert!(roundtripped.get("agentId").is_none());
6568 }
6569
6570 #[test]
6572 fn typed_session_event_round_trips_agent_id_on_envelope() {
6573 let wire = json!({
6574 "id": "evt-1",
6575 "timestamp": "2026-04-30T12:00:00Z",
6576 "parentId": null,
6577 "agentId": "sub-agent-42",
6578 "type": "session.idle",
6579 "data": {}
6580 });
6581
6582 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
6583 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6584
6585 let roundtripped = serde_json::to_value(&event).unwrap();
6586 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6587 }
6588
6589 #[test]
6590 fn connection_state_variants_compile() {
6591 let _ = ConnectionState::Disconnected;
6595 let _ = ConnectionState::Connecting;
6596 let _ = ConnectionState::Connected;
6597 let _ = ConnectionState::Error;
6598 }
6599
6600 #[test]
6601 fn deserializes_runtime_attachment_variants() {
6602 let attachments: Vec<Attachment> = serde_json::from_value(json!([
6603 {
6604 "type": "file",
6605 "path": "/tmp/file.rs",
6606 "displayName": "file.rs",
6607 "lineRange": { "start": 7, "end": 12 }
6608 },
6609 {
6610 "type": "directory",
6611 "path": "/tmp/project",
6612 "displayName": "project"
6613 },
6614 {
6615 "type": "selection",
6616 "filePath": "/tmp/lib.rs",
6617 "displayName": "lib.rs",
6618 "text": "fn main() {}",
6619 "selection": {
6620 "start": { "line": 1, "character": 2 },
6621 "end": { "line": 3, "character": 4 }
6622 }
6623 },
6624 {
6625 "type": "blob",
6626 "data": "Zm9v",
6627 "mimeType": "image/png",
6628 "displayName": "image.png"
6629 },
6630 {
6631 "type": "github_reference",
6632 "number": 42,
6633 "title": "Fix rendering",
6634 "referenceType": "issue",
6635 "state": "open",
6636 "url": "https://github.com/example/repo/issues/42"
6637 }
6638 ]))
6639 .expect("attachments should deserialize");
6640
6641 assert_eq!(attachments.len(), 5);
6642 assert!(matches!(
6643 &attachments[0],
6644 Attachment::File {
6645 path,
6646 display_name,
6647 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
6648 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
6649 ));
6650 assert!(matches!(
6651 &attachments[1],
6652 Attachment::Directory { path, display_name }
6653 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
6654 ));
6655 assert!(matches!(
6656 &attachments[2],
6657 Attachment::Selection {
6658 file_path,
6659 display_name,
6660 selection:
6661 AttachmentSelectionRange {
6662 start: AttachmentSelectionPosition { line: 1, character: 2 },
6663 end: AttachmentSelectionPosition { line: 3, character: 4 },
6664 },
6665 ..
6666 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
6667 ));
6668 assert!(matches!(
6669 &attachments[3],
6670 Attachment::Blob {
6671 data,
6672 mime_type,
6673 display_name,
6674 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
6675 ));
6676 assert!(matches!(
6677 &attachments[4],
6678 Attachment::GitHubReference {
6679 number: 42,
6680 title,
6681 reference_type: GitHubReferenceType::Issue,
6682 state,
6683 url,
6684 } if title == "Fix rendering"
6685 && state == "open"
6686 && url == "https://github.com/example/repo/issues/42"
6687 ));
6688 }
6689
6690 #[test]
6691 fn ensures_display_names_for_variants_that_support_them() {
6692 let mut attachments = vec![
6693 Attachment::File {
6694 path: PathBuf::from("/tmp/file.rs"),
6695 display_name: None,
6696 line_range: None,
6697 },
6698 Attachment::Selection {
6699 file_path: PathBuf::from("/tmp/src/lib.rs"),
6700 display_name: None,
6701 text: "fn main() {}".to_string(),
6702 selection: AttachmentSelectionRange {
6703 start: AttachmentSelectionPosition {
6704 line: 0,
6705 character: 0,
6706 },
6707 end: AttachmentSelectionPosition {
6708 line: 0,
6709 character: 10,
6710 },
6711 },
6712 },
6713 Attachment::Blob {
6714 data: "Zm9v".to_string(),
6715 mime_type: "image/png".to_string(),
6716 display_name: None,
6717 },
6718 Attachment::GitHubReference {
6719 number: 7,
6720 title: "Track regressions".to_string(),
6721 reference_type: GitHubReferenceType::Issue,
6722 state: "open".to_string(),
6723 url: "https://example.com/issues/7".to_string(),
6724 },
6725 ];
6726
6727 ensure_attachment_display_names(&mut attachments);
6728
6729 assert_eq!(attachments[0].display_name(), Some("file.rs"));
6730 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
6731 assert_eq!(attachments[2].display_name(), Some("attachment"));
6732 assert_eq!(attachments[3].display_name(), None);
6733 assert_eq!(
6734 attachments[3].label(),
6735 Some("Track regressions".to_string())
6736 );
6737 }
6738
6739 #[test]
6740 fn github_anchored_attachment_variants_round_trip() {
6741 let cases = vec![
6742 (
6743 "github_commit",
6744 json!({
6745 "type": "github_commit",
6746 "message": "Fix the thing",
6747 "oid": "abc123",
6748 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
6749 "url": "https://github.com/octocat/repo/commit/abc123"
6750 }),
6751 ),
6752 (
6753 "github_release",
6754 json!({
6755 "type": "github_release",
6756 "name": "v1.2.3",
6757 "repo": { "name": "repo", "owner": "octocat" },
6758 "tagName": "v1.2.3",
6759 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
6760 }),
6761 ),
6762 (
6763 "github_actions_job",
6764 json!({
6765 "type": "github_actions_job",
6766 "conclusion": "failure",
6767 "jobId": 99,
6768 "jobName": "build",
6769 "repo": { "name": "repo", "owner": "octocat" },
6770 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
6771 "workflowName": "CI"
6772 }),
6773 ),
6774 (
6775 "github_repository",
6776 json!({
6777 "type": "github_repository",
6778 "description": "An example repository",
6779 "ref": "main",
6780 "repo": { "name": "repo", "owner": "octocat" },
6781 "url": "https://github.com/octocat/repo"
6782 }),
6783 ),
6784 (
6785 "github_file_diff",
6786 json!({
6787 "type": "github_file_diff",
6788 "base": {
6789 "path": "src/lib.rs",
6790 "ref": "main",
6791 "repo": { "name": "repo", "owner": "octocat" }
6792 },
6793 "head": {
6794 "path": "src/lib.rs",
6795 "ref": "feature",
6796 "repo": { "name": "repo", "owner": "octocat" }
6797 },
6798 "url": "https://github.com/octocat/repo/compare/main...feature"
6799 }),
6800 ),
6801 (
6802 "github_tree_comparison",
6803 json!({
6804 "type": "github_tree_comparison",
6805 "base": {
6806 "repo": { "name": "repo", "owner": "octocat" },
6807 "revision": "main"
6808 },
6809 "head": {
6810 "repo": { "name": "repo", "owner": "octocat" },
6811 "revision": "feature"
6812 },
6813 "url": "https://github.com/octocat/repo/compare/main...feature"
6814 }),
6815 ),
6816 (
6817 "github_url",
6818 json!({
6819 "type": "github_url",
6820 "url": "https://github.com/octocat/repo/wiki"
6821 }),
6822 ),
6823 (
6824 "github_file",
6825 json!({
6826 "type": "github_file",
6827 "path": "src/main.rs",
6828 "ref": "main",
6829 "repo": { "name": "repo", "owner": "octocat" },
6830 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
6831 }),
6832 ),
6833 (
6834 "github_snippet",
6835 json!({
6836 "type": "github_snippet",
6837 "lineRange": { "start": 10, "end": 20 },
6838 "path": "src/main.rs",
6839 "ref": "main",
6840 "repo": { "name": "repo", "owner": "octocat" },
6841 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
6842 }),
6843 ),
6844 ];
6845
6846 for (expected_type, input) in cases {
6847 let attachment: Attachment = serde_json::from_value(input.clone())
6848 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
6849
6850 let serialized_string = serde_json::to_string(&attachment)
6855 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
6856
6857 assert_eq!(
6859 serialized_string.matches("\"type\":").count(),
6860 1,
6861 "{expected_type} must serialize a single `type` key"
6862 );
6863
6864 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
6865 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
6866 assert_eq!(
6867 serialized.get("type").and_then(|value| value.as_str()),
6868 Some(expected_type),
6869 "{expected_type} must serialize the correct discriminator"
6870 );
6871
6872 assert_eq!(
6874 serialized, input,
6875 "{expected_type} should round-trip without data loss"
6876 );
6877 let reparsed: Attachment = serde_json::from_value(serialized)
6878 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
6879 assert_eq!(
6880 reparsed, attachment,
6881 "{expected_type} should re-deserialize to the same value"
6882 );
6883 }
6884 }
6885}
6886
6887#[cfg(test)]
6888mod permission_builder_tests {
6889 use std::sync::Arc;
6890
6891 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
6892 use crate::permission;
6893 use crate::types::{
6894 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
6895 SessionId,
6896 };
6897
6898 fn data() -> PermissionRequestData {
6899 PermissionRequestData {
6900 extra: serde_json::json!({"tool": "shell"}),
6901 ..Default::default()
6902 }
6903 }
6904
6905 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6908 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6909 }
6910
6911 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6912 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6913 }
6914
6915 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
6916 handler
6917 .handle(SessionId::from("s1"), RequestId::new("1"), data())
6918 .await
6919 }
6920
6921 #[tokio::test]
6922 async fn approve_all_with_handler_present_approves() {
6923 let cfg = SessionConfig::default()
6924 .with_permission_handler(Arc::new(ApproveAllHandler))
6925 .approve_all_permissions();
6926 let h = resolve_create(cfg).expect("policy + handler yields handler");
6927 assert!(matches!(
6928 dispatch(&h).await,
6929 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6930 ));
6931 }
6932
6933 #[tokio::test]
6934 async fn approve_all_standalone_produces_handler() {
6935 let cfg = SessionConfig::default().approve_all_permissions();
6936 let h = resolve_create(cfg).expect("policy alone yields handler");
6937 assert!(matches!(
6938 dispatch(&h).await,
6939 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6940 ));
6941 }
6942
6943 #[tokio::test]
6946 async fn approve_all_is_order_independent() {
6947 let a = SessionConfig::default()
6948 .with_permission_handler(Arc::new(ApproveAllHandler))
6949 .approve_all_permissions();
6950 let b = SessionConfig::default()
6951 .approve_all_permissions()
6952 .with_permission_handler(Arc::new(ApproveAllHandler));
6953 let ha = resolve_create(a).unwrap();
6954 let hb = resolve_create(b).unwrap();
6955 assert!(matches!(
6956 dispatch(&ha).await,
6957 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6958 ));
6959 assert!(matches!(
6960 dispatch(&hb).await,
6961 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6962 ));
6963 }
6964
6965 #[tokio::test]
6966 async fn deny_all_is_order_independent() {
6967 let a = SessionConfig::default()
6968 .with_permission_handler(Arc::new(ApproveAllHandler))
6969 .deny_all_permissions();
6970 let b = SessionConfig::default()
6971 .deny_all_permissions()
6972 .with_permission_handler(Arc::new(ApproveAllHandler));
6973 let ha = resolve_create(a).unwrap();
6974 let hb = resolve_create(b).unwrap();
6975 assert!(matches!(
6976 dispatch(&ha).await,
6977 PermissionResult::Decision(PermissionDecision::Reject(_))
6978 ));
6979 assert!(matches!(
6980 dispatch(&hb).await,
6981 PermissionResult::Decision(PermissionDecision::Reject(_))
6982 ));
6983 }
6984
6985 #[tokio::test]
6986 async fn approve_permissions_if_consults_predicate() {
6987 let cfg = SessionConfig::default().approve_permissions_if(|d| {
6988 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6989 });
6990 let h = resolve_create(cfg).unwrap();
6991 assert!(matches!(
6992 dispatch(&h).await,
6993 PermissionResult::Decision(PermissionDecision::Reject(_))
6994 ));
6995 }
6996
6997 #[tokio::test]
6998 async fn approve_permissions_if_is_order_independent() {
6999 let predicate = |d: &PermissionRequestData| {
7000 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
7001 };
7002 let a = SessionConfig::default()
7003 .with_permission_handler(Arc::new(ApproveAllHandler))
7004 .approve_permissions_if(predicate);
7005 let b = SessionConfig::default()
7006 .approve_permissions_if(predicate)
7007 .with_permission_handler(Arc::new(ApproveAllHandler));
7008 let ha = resolve_create(a).unwrap();
7009 let hb = resolve_create(b).unwrap();
7010 assert!(matches!(
7011 dispatch(&ha).await,
7012 PermissionResult::Decision(PermissionDecision::Reject(_))
7013 ));
7014 assert!(matches!(
7015 dispatch(&hb).await,
7016 PermissionResult::Decision(PermissionDecision::Reject(_))
7017 ));
7018 }
7019
7020 #[tokio::test]
7021 async fn resume_session_config_approve_all_works() {
7022 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
7023 .with_permission_handler(Arc::new(ApproveAllHandler))
7024 .approve_all_permissions();
7025 let h = resolve_resume(cfg).unwrap();
7026 assert!(matches!(
7027 dispatch(&h).await,
7028 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7029 ));
7030 }
7031
7032 #[tokio::test]
7033 async fn resume_session_config_approve_all_is_order_independent() {
7034 let a = ResumeSessionConfig::new(SessionId::from("s1"))
7035 .with_permission_handler(Arc::new(ApproveAllHandler))
7036 .approve_all_permissions();
7037 let b = ResumeSessionConfig::new(SessionId::from("s1"))
7038 .approve_all_permissions()
7039 .with_permission_handler(Arc::new(ApproveAllHandler));
7040 let ha = resolve_resume(a).unwrap();
7041 let hb = resolve_resume(b).unwrap();
7042 assert!(matches!(
7043 dispatch(&ha).await,
7044 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7045 ));
7046 assert!(matches!(
7047 dispatch(&hb).await,
7048 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7049 ));
7050 }
7051}