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(skip)]
367 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
368}
369
370#[inline]
371fn is_false(b: &bool) -> bool {
372 !*b
373}
374
375#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
378#[serde(rename_all = "lowercase")]
379pub enum DeferMode {
380 Auto,
382 Never,
384}
385
386impl Tool {
387 pub fn new(name: impl Into<String>) -> Self {
407 Self {
408 name: name.into(),
409 ..Default::default()
410 }
411 }
412
413 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
416 self.namespaced_name = Some(namespaced_name.into());
417 self
418 }
419
420 pub fn with_description(mut self, description: impl Into<String>) -> Self {
422 self.description = description.into();
423 self
424 }
425
426 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
428 self.instructions = Some(instructions.into());
429 self
430 }
431
432 pub fn with_parameters(mut self, parameters: Value) -> Self {
446 self.parameters = crate::tool::tool_parameters(parameters);
447 self
448 }
449
450 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
454 self.overrides_built_in_tool = overrides;
455 self
456 }
457
458 pub fn with_skip_permission(mut self, skip: bool) -> Self {
462 self.skip_permission = skip;
463 self
464 }
465
466 pub fn with_defer(mut self, defer: DeferMode) -> Self {
470 self.defer = Some(defer);
471 self
472 }
473
474 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
478 self.handler = Some(handler);
479 self
480 }
481
482 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
487 self.handler.as_ref()
488 }
489}
490
491impl std::fmt::Debug for Tool {
492 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
493 f.debug_struct("Tool")
494 .field("name", &self.name)
495 .field("namespaced_name", &self.namespaced_name)
496 .field("description", &self.description)
497 .field("instructions", &self.instructions)
498 .field("parameters", &self.parameters)
499 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
500 .field("skip_permission", &self.skip_permission)
501 .field("defer", &self.defer)
502 .field(
503 "handler",
504 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
505 )
506 .finish()
507 }
508}
509
510#[non_exhaustive]
513#[derive(Debug, Clone)]
514pub struct CommandContext {
515 pub session_id: SessionId,
517 pub command: String,
519 pub command_name: String,
521 pub args: String,
523}
524
525#[async_trait::async_trait]
531pub trait CommandHandler: Send + Sync {
532 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
534}
535
536#[non_exhaustive]
542#[derive(Clone)]
543pub struct CommandDefinition {
544 pub name: String,
546 pub description: Option<String>,
548 pub handler: Arc<dyn CommandHandler>,
550}
551
552impl CommandDefinition {
553 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
556 Self {
557 name: name.into(),
558 description: None,
559 handler,
560 }
561 }
562
563 pub fn with_description(mut self, description: impl Into<String>) -> Self {
565 self.description = Some(description.into());
566 self
567 }
568}
569
570impl std::fmt::Debug for CommandDefinition {
571 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
572 f.debug_struct("CommandDefinition")
573 .field("name", &self.name)
574 .field("description", &self.description)
575 .field("handler", &"<set>")
576 .finish()
577 }
578}
579
580impl Serialize for CommandDefinition {
581 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
582 use serde::ser::SerializeStruct;
583 let len = if self.description.is_some() { 2 } else { 1 };
584 let mut state = serializer.serialize_struct("CommandDefinition", len)?;
585 state.serialize_field("name", &self.name)?;
586 if let Some(description) = &self.description {
587 state.serialize_field("description", description)?;
588 }
589 state.end()
590 }
591}
592
593#[derive(Debug, Clone, Default, Serialize, Deserialize)]
600#[serde(rename_all = "camelCase")]
601#[non_exhaustive]
602pub struct CustomAgentConfig {
603 pub name: String,
605 #[serde(default, skip_serializing_if = "Option::is_none")]
607 pub display_name: Option<String>,
608 #[serde(default, skip_serializing_if = "Option::is_none")]
610 pub description: Option<String>,
611 #[serde(default, skip_serializing_if = "Option::is_none")]
613 pub tools: Option<Vec<String>>,
614 pub prompt: String,
616 #[serde(default, skip_serializing_if = "Option::is_none")]
618 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
619 #[serde(default, skip_serializing_if = "Option::is_none")]
621 pub infer: Option<bool>,
622 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub skills: Option<Vec<String>>,
625 #[serde(default, skip_serializing_if = "Option::is_none")]
630 pub model: Option<String>,
631}
632
633impl CustomAgentConfig {
634 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
641 Self {
642 name: name.into(),
643 prompt: prompt.into(),
644 ..Self::default()
645 }
646 }
647
648 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
650 self.display_name = Some(display_name.into());
651 self
652 }
653
654 pub fn with_description(mut self, description: impl Into<String>) -> Self {
656 self.description = Some(description.into());
657 self
658 }
659
660 pub fn with_tools<I, S>(mut self, tools: I) -> Self
663 where
664 I: IntoIterator<Item = S>,
665 S: Into<String>,
666 {
667 self.tools = Some(tools.into_iter().map(Into::into).collect());
668 self
669 }
670
671 pub fn with_mcp_servers(mut self, mcp_servers: IndexMap<String, McpServerConfig>) -> Self {
673 self.mcp_servers = Some(mcp_servers);
674 self
675 }
676
677 pub fn with_infer(mut self, infer: bool) -> Self {
679 self.infer = Some(infer);
680 self
681 }
682
683 pub fn with_skills<I, S>(mut self, skills: I) -> Self
685 where
686 I: IntoIterator<Item = S>,
687 S: Into<String>,
688 {
689 self.skills = Some(skills.into_iter().map(Into::into).collect());
690 self
691 }
692
693 pub fn with_model(mut self, model: impl Into<String>) -> Self {
695 self.model = Some(model.into());
696 self
697 }
698}
699
700#[derive(Debug, Clone, Default, Serialize, Deserialize)]
707#[serde(rename_all = "camelCase")]
708pub struct DefaultAgentConfig {
709 #[serde(default, skip_serializing_if = "Option::is_none")]
711 pub excluded_tools: Option<Vec<String>>,
712}
713
714#[derive(Debug, Clone, Default, Serialize, Deserialize)]
720#[serde(rename_all = "camelCase")]
721#[non_exhaustive]
722pub struct LargeToolOutputConfig {
723 #[serde(default, skip_serializing_if = "Option::is_none")]
725 pub enabled: Option<bool>,
726 #[serde(default, skip_serializing_if = "Option::is_none")]
729 pub max_size_bytes: Option<u64>,
730 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
733 pub output_directory: Option<PathBuf>,
734}
735
736impl LargeToolOutputConfig {
737 pub fn new() -> Self {
740 Self::default()
741 }
742
743 pub fn with_enabled(mut self, enabled: bool) -> Self {
745 self.enabled = Some(enabled);
746 self
747 }
748
749 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
751 self.max_size_bytes = Some(max_size_bytes);
752 self
753 }
754
755 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
757 self.output_directory = Some(output_directory.into());
758 self
759 }
760}
761
762#[derive(Debug, Clone, Default, Serialize, Deserialize)]
768#[serde(rename_all = "camelCase")]
769#[non_exhaustive]
770pub struct ToolSearchConfig {
771 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub enabled: Option<bool>,
774 #[serde(default, skip_serializing_if = "Option::is_none")]
777 pub defer_threshold: Option<u32>,
778}
779
780impl ToolSearchConfig {
781 pub fn new() -> Self {
784 Self::default()
785 }
786
787 pub fn with_enabled(mut self, enabled: bool) -> Self {
789 self.enabled = Some(enabled);
790 self
791 }
792
793 pub fn with_defer_threshold(mut self, defer_threshold: u32) -> Self {
796 self.defer_threshold = Some(defer_threshold);
797 self
798 }
799}
800
801#[derive(Debug, Clone, Default, Serialize, Deserialize)]
808#[serde(rename_all = "camelCase")]
809#[non_exhaustive]
810pub struct InfiniteSessionConfig {
811 #[serde(default, skip_serializing_if = "Option::is_none")]
813 pub enabled: Option<bool>,
814 #[serde(default, skip_serializing_if = "Option::is_none")]
817 pub background_compaction_threshold: Option<f64>,
818 #[serde(default, skip_serializing_if = "Option::is_none")]
821 pub buffer_exhaustion_threshold: Option<f64>,
822}
823
824impl InfiniteSessionConfig {
825 pub fn new() -> Self {
828 Self::default()
829 }
830
831 pub fn with_enabled(mut self, enabled: bool) -> Self {
834 self.enabled = Some(enabled);
835 self
836 }
837
838 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
841 self.background_compaction_threshold = Some(threshold);
842 self
843 }
844
845 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
848 self.buffer_exhaustion_threshold = Some(threshold);
849 self
850 }
851}
852
853#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
864#[serde(rename_all = "camelCase")]
865#[non_exhaustive]
866pub struct MemoryConfiguration {
867 pub enabled: bool,
869}
870
871impl MemoryConfiguration {
872 pub fn enabled() -> Self {
874 Self { enabled: true }
875 }
876
877 pub fn disabled() -> Self {
879 Self { enabled: false }
880 }
881
882 pub fn with_enabled(mut self, enabled: bool) -> Self {
884 self.enabled = enabled;
885 self
886 }
887}
888
889#[derive(Debug, Clone, Serialize, Deserialize)]
891#[serde(rename_all = "camelCase")]
892#[non_exhaustive]
893pub struct CloudSessionRepository {
894 pub owner: String,
896 pub name: String,
898 #[serde(skip_serializing_if = "Option::is_none")]
900 pub branch: Option<String>,
901}
902
903impl CloudSessionRepository {
904 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
906 Self {
907 owner: owner.into(),
908 name: name.into(),
909 branch: None,
910 }
911 }
912
913 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
915 self.branch = Some(branch.into());
916 self
917 }
918}
919
920#[derive(Debug, Clone, Default, Serialize, Deserialize)]
922#[serde(rename_all = "camelCase")]
923#[non_exhaustive]
924pub struct CloudSessionOptions {
925 #[serde(skip_serializing_if = "Option::is_none")]
927 pub repository: Option<CloudSessionRepository>,
928}
929
930impl CloudSessionOptions {
931 pub fn with_repository(repository: CloudSessionRepository) -> Self {
933 Self {
934 repository: Some(repository),
935 }
936 }
937}
938
939#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
941#[serde(rename_all = "camelCase")]
942pub struct ExtensionInfo {
943 pub source: String,
945 pub name: String,
947}
948
949impl ExtensionInfo {
950 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
952 Self {
953 source: source.into(),
954 name: name.into(),
955 }
956 }
957}
958
959#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
970#[serde(rename_all = "camelCase")]
971pub struct CanvasProviderIdentity {
972 pub id: String,
974 #[serde(skip_serializing_if = "Option::is_none")]
976 pub name: Option<String>,
977}
978
979impl CanvasProviderIdentity {
980 pub fn new(id: impl Into<String>) -> Self {
982 Self {
983 id: id.into(),
984 name: None,
985 }
986 }
987
988 pub fn with_name(mut self, name: impl Into<String>) -> Self {
990 self.name = Some(name.into());
991 self
992 }
993}
994
995#[derive(Debug, Clone, Serialize, Deserialize)]
1029#[serde(tag = "type", rename_all = "lowercase")]
1030#[non_exhaustive]
1031pub enum McpServerConfig {
1032 #[serde(alias = "local")]
1036 Stdio(McpStdioServerConfig),
1037 Http(McpHttpServerConfig),
1039 Sse(McpHttpServerConfig),
1041}
1042
1043#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1047#[serde(rename_all = "camelCase")]
1048pub struct McpStdioServerConfig {
1049 #[serde(default, skip_serializing_if = "Option::is_none")]
1055 pub tools: Option<Vec<String>>,
1056 #[serde(default, skip_serializing_if = "Option::is_none")]
1058 pub timeout: Option<i64>,
1059 pub command: String,
1061 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1063 pub args: Vec<String>,
1064 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1067 pub env: HashMap<String, String>,
1068 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1070 pub working_directory: Option<String>,
1071}
1072
1073#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1077#[serde(rename_all = "camelCase")]
1078pub struct McpHttpServerConfig {
1079 #[serde(default, skip_serializing_if = "Option::is_none")]
1085 pub tools: Option<Vec<String>>,
1086 #[serde(default, skip_serializing_if = "Option::is_none")]
1088 pub timeout: Option<i64>,
1089 pub url: String,
1091 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1093 pub headers: HashMap<String, String>,
1094}
1095
1096#[derive(Clone, Default, Serialize, Deserialize)]
1102#[serde(rename_all = "camelCase")]
1103#[non_exhaustive]
1104pub struct ProviderConfig {
1105 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1108 pub provider_type: Option<String>,
1109 #[serde(default, skip_serializing_if = "Option::is_none")]
1112 pub wire_api: Option<String>,
1113 #[serde(default, skip_serializing_if = "Option::is_none")]
1118 pub transport: Option<String>,
1119 pub base_url: String,
1121 #[serde(default, skip_serializing_if = "Option::is_none")]
1123 pub api_key: Option<String>,
1124 #[serde(default, skip_serializing_if = "Option::is_none")]
1128 pub bearer_token: Option<String>,
1129 #[serde(skip)]
1132 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1133 #[serde(default, skip_serializing_if = "Option::is_none")]
1134 pub(crate) has_bearer_token_provider: Option<bool>,
1135 #[serde(default, skip_serializing_if = "Option::is_none")]
1137 pub azure: Option<AzureProviderOptions>,
1138 #[serde(default, skip_serializing_if = "Option::is_none")]
1140 pub headers: Option<HashMap<String, String>>,
1141 #[serde(default, skip_serializing_if = "Option::is_none")]
1145 pub model_id: Option<String>,
1146 #[serde(default, skip_serializing_if = "Option::is_none")]
1153 pub wire_model: Option<String>,
1154 #[serde(default, skip_serializing_if = "Option::is_none")]
1159 pub max_prompt_tokens: Option<i64>,
1160 #[serde(default, skip_serializing_if = "Option::is_none")]
1163 pub max_output_tokens: Option<i64>,
1164}
1165
1166impl std::fmt::Debug for ProviderConfig {
1167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1168 f.debug_struct("ProviderConfig")
1169 .field("provider_type", &self.provider_type)
1170 .field("wire_api", &self.wire_api)
1171 .field("transport", &self.transport)
1172 .field("base_url", &self.base_url)
1173 .field("api_key", &self.api_key)
1174 .field("bearer_token", &self.bearer_token)
1175 .field(
1176 "bearer_token_provider",
1177 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1178 )
1179 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1180 .field("azure", &self.azure)
1181 .field("headers", &self.headers)
1182 .field("model_id", &self.model_id)
1183 .field("wire_model", &self.wire_model)
1184 .field("max_prompt_tokens", &self.max_prompt_tokens)
1185 .field("max_output_tokens", &self.max_output_tokens)
1186 .finish()
1187 }
1188}
1189
1190impl ProviderConfig {
1191 pub fn new(base_url: impl Into<String>) -> Self {
1194 Self {
1195 base_url: base_url.into(),
1196 ..Self::default()
1197 }
1198 }
1199
1200 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1202 self.provider_type = Some(provider_type.into());
1203 self
1204 }
1205
1206 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1208 self.wire_api = Some(wire_api.into());
1209 self
1210 }
1211
1212 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1215 self.transport = Some(transport.into());
1216 self
1217 }
1218
1219 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1221 self.api_key = Some(api_key.into());
1222 self
1223 }
1224
1225 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1228 self.bearer_token = Some(bearer_token.into());
1229 self
1230 }
1231
1232 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1238 self.bearer_token_provider = Some(provider);
1239 self
1240 }
1241
1242 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1244 self.azure = Some(azure);
1245 self
1246 }
1247
1248 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1250 self.headers = Some(headers);
1251 self
1252 }
1253
1254 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1257 self.model_id = Some(model_id.into());
1258 self
1259 }
1260
1261 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1266 self.wire_model = Some(wire_model.into());
1267 self
1268 }
1269
1270 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1274 self.max_prompt_tokens = Some(max);
1275 self
1276 }
1277
1278 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1281 self.max_output_tokens = Some(max);
1282 self
1283 }
1284}
1285
1286#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1299#[serde(rename_all = "camelCase")]
1300#[non_exhaustive]
1301pub struct CapiSessionOptions {
1302 #[serde(default, skip_serializing_if = "Option::is_none")]
1308 pub enable_web_socket_responses: Option<bool>,
1309}
1310
1311impl CapiSessionOptions {
1312 pub fn new() -> Self {
1314 Self::default()
1315 }
1316
1317 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1319 self.enable_web_socket_responses = Some(enable);
1320 self
1321 }
1322}
1323
1324#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1326#[serde(rename_all = "camelCase")]
1327pub struct AzureProviderOptions {
1328 #[serde(default, skip_serializing_if = "Option::is_none")]
1330 pub api_version: Option<String>,
1331}
1332
1333#[derive(Clone, Default, Serialize, Deserialize)]
1344#[serde(rename_all = "camelCase")]
1345#[non_exhaustive]
1346pub struct NamedProviderConfig {
1347 pub name: String,
1350 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1353 pub provider_type: Option<String>,
1354 #[serde(default, skip_serializing_if = "Option::is_none")]
1357 pub wire_api: Option<String>,
1358 pub base_url: String,
1360 #[serde(default, skip_serializing_if = "Option::is_none")]
1362 pub api_key: Option<String>,
1363 #[serde(default, skip_serializing_if = "Option::is_none")]
1366 pub bearer_token: Option<String>,
1367 #[serde(skip)]
1370 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1371 #[serde(default, skip_serializing_if = "Option::is_none")]
1372 pub(crate) has_bearer_token_provider: Option<bool>,
1373 #[serde(default, skip_serializing_if = "Option::is_none")]
1375 pub azure: Option<AzureProviderOptions>,
1376 #[serde(default, skip_serializing_if = "Option::is_none")]
1378 pub headers: Option<HashMap<String, String>>,
1379}
1380
1381impl std::fmt::Debug for NamedProviderConfig {
1382 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1383 f.debug_struct("NamedProviderConfig")
1384 .field("name", &self.name)
1385 .field("provider_type", &self.provider_type)
1386 .field("wire_api", &self.wire_api)
1387 .field("base_url", &self.base_url)
1388 .field("api_key", &self.api_key)
1389 .field("bearer_token", &self.bearer_token)
1390 .field(
1391 "bearer_token_provider",
1392 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1393 )
1394 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1395 .field("azure", &self.azure)
1396 .field("headers", &self.headers)
1397 .finish()
1398 }
1399}
1400
1401impl NamedProviderConfig {
1402 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1405 Self {
1406 name: name.into(),
1407 base_url: base_url.into(),
1408 ..Self::default()
1409 }
1410 }
1411
1412 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1414 self.provider_type = Some(provider_type.into());
1415 self
1416 }
1417
1418 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1420 self.wire_api = Some(wire_api.into());
1421 self
1422 }
1423
1424 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1426 self.api_key = Some(api_key.into());
1427 self
1428 }
1429
1430 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1433 self.bearer_token = Some(bearer_token.into());
1434 self
1435 }
1436
1437 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1443 self.bearer_token_provider = Some(provider);
1444 self
1445 }
1446
1447 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1449 self.azure = Some(azure);
1450 self
1451 }
1452
1453 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1455 self.headers = Some(headers);
1456 self
1457 }
1458}
1459
1460fn prepare_bearer_token_providers(
1461 provider: &mut Option<ProviderConfig>,
1462 providers: &mut Option<Vec<NamedProviderConfig>>,
1463) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1464 let mut bearer_token_providers = HashMap::new();
1465
1466 if let Some(provider) = provider.as_mut()
1467 && let Some(token_provider) = provider.bearer_token_provider.take()
1468 {
1469 provider.has_bearer_token_provider = Some(true);
1470 bearer_token_providers.insert("default".to_string(), token_provider);
1471 }
1472
1473 if let Some(providers) = providers.as_mut() {
1474 for provider in providers {
1475 if let Some(token_provider) = provider.bearer_token_provider.take() {
1476 provider.has_bearer_token_provider = Some(true);
1477 bearer_token_providers.insert(provider.name.clone(), token_provider);
1478 }
1479 }
1480 }
1481
1482 bearer_token_providers
1483}
1484
1485#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1493#[serde(rename_all = "camelCase")]
1494#[non_exhaustive]
1495pub struct ProviderModelConfig {
1496 pub id: String,
1499 pub provider: String,
1501 #[serde(default, skip_serializing_if = "Option::is_none")]
1504 pub wire_model: Option<String>,
1505 #[serde(default, skip_serializing_if = "Option::is_none")]
1508 pub model_id: Option<String>,
1509 #[serde(default, skip_serializing_if = "Option::is_none")]
1511 pub name: Option<String>,
1512 #[serde(default, skip_serializing_if = "Option::is_none")]
1514 pub max_prompt_tokens: Option<i64>,
1515 #[serde(default, skip_serializing_if = "Option::is_none")]
1517 pub max_context_window_tokens: Option<i64>,
1518 #[serde(default, skip_serializing_if = "Option::is_none")]
1520 pub max_output_tokens: Option<i64>,
1521 #[serde(default, skip_serializing_if = "Option::is_none")]
1524 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1525}
1526
1527impl ProviderModelConfig {
1528 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1531 Self {
1532 id: id.into(),
1533 provider: provider.into(),
1534 ..Self::default()
1535 }
1536 }
1537
1538 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1540 self.wire_model = Some(wire_model.into());
1541 self
1542 }
1543
1544 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1547 self.model_id = Some(model_id.into());
1548 self
1549 }
1550
1551 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1553 self.name = Some(name.into());
1554 self
1555 }
1556
1557 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1559 self.max_prompt_tokens = Some(max);
1560 self
1561 }
1562
1563 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1565 self.max_context_window_tokens = Some(max);
1566 self
1567 }
1568
1569 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1571 self.max_output_tokens = Some(max);
1572 self
1573 }
1574
1575 pub fn with_capabilities(
1577 mut self,
1578 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1579 ) -> Self {
1580 self.capabilities = Some(capabilities);
1581 self
1582 }
1583}
1584
1585#[derive(Clone)]
1637#[non_exhaustive]
1638pub struct SessionConfig {
1639 pub session_id: Option<SessionId>,
1641 pub model: Option<String>,
1643 pub client_name: Option<String>,
1645 pub reasoning_effort: Option<String>,
1647 pub reasoning_summary: Option<ReasoningSummary>,
1651 pub context_tier: Option<String>,
1654 pub streaming: Option<bool>,
1656 pub system_message: Option<SystemMessageConfig>,
1658 pub tools: Option<Vec<Tool>>,
1660 pub canvases: Option<Vec<CanvasDeclaration>>,
1662 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1667 pub request_canvas_renderer: Option<bool>,
1669 pub request_extensions: Option<bool>,
1671 pub extension_sdk_path: Option<String>,
1675 pub extension_info: Option<ExtensionInfo>,
1677 pub canvas_provider: Option<CanvasProviderIdentity>,
1680 pub available_tools: Option<Vec<String>>,
1682 pub excluded_tools: Option<Vec<String>>,
1684 pub excluded_builtin_agents: Option<Vec<String>>,
1690 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
1692 pub mcp_oauth_token_storage: Option<String>,
1701 pub enable_config_discovery: Option<bool>,
1703 pub skip_embedding_retrieval: Option<bool>,
1705 pub embedding_cache_storage: Option<String>,
1708 pub organization_custom_instructions: Option<String>,
1710 pub enable_on_demand_instruction_discovery: Option<bool>,
1712 pub enable_file_hooks: Option<bool>,
1714 pub enable_host_git_operations: Option<bool>,
1716 pub enable_session_store: Option<bool>,
1718 pub enable_skills: Option<bool>,
1720 pub enable_mcp_apps: Option<bool>,
1747 pub skill_directories: Option<Vec<PathBuf>>,
1749 pub instruction_directories: Option<Vec<PathBuf>>,
1752 pub plugin_directories: Option<Vec<PathBuf>>,
1754 pub large_output: Option<LargeToolOutputConfig>,
1756 pub tool_search: Option<ToolSearchConfig>,
1760 pub disabled_skills: Option<Vec<String>>,
1763 pub hooks: Option<bool>,
1767 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1769 pub default_agent: Option<DefaultAgentConfig>,
1773 pub agent: Option<String>,
1776 pub infinite_sessions: Option<InfiniteSessionConfig>,
1779 pub provider: Option<ProviderConfig>,
1783 pub capi: Option<CapiSessionOptions>,
1789 pub providers: Option<Vec<NamedProviderConfig>>,
1796 pub models: Option<Vec<ProviderModelConfig>>,
1802 pub enable_session_telemetry: Option<bool>,
1810 pub enable_citations: Option<bool>,
1812 pub session_limits: Option<SessionLimitsConfig>,
1814 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1817 pub memory: Option<MemoryConfiguration>,
1819 pub config_directory: Option<PathBuf>,
1822 pub working_directory: Option<PathBuf>,
1825 pub github_token: Option<String>,
1831 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
1837 pub cloud: Option<CloudSessionOptions>,
1840 pub include_sub_agent_streaming_events: Option<bool>,
1844 pub commands: Option<Vec<CommandDefinition>>,
1848 #[doc(hidden)]
1855 pub exp_assignments: Option<Value>,
1856 pub enable_managed_settings: Option<bool>,
1863 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1868 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
1872 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
1875 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
1878 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
1882 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
1885 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
1888 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1892 pub(crate) permission_policy: Option<crate::permission::Policy>,
1896 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
1901 pub skip_custom_instructions: Option<bool>,
1905 pub custom_agents_local_only: Option<bool>,
1909 pub coauthor_enabled: Option<bool>,
1913 pub manage_schedule_enabled: Option<bool>,
1917}
1918
1919impl std::fmt::Debug for SessionConfig {
1920 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1921 f.debug_struct("SessionConfig")
1922 .field("session_id", &self.session_id)
1923 .field("model", &self.model)
1924 .field("client_name", &self.client_name)
1925 .field("reasoning_effort", &self.reasoning_effort)
1926 .field("reasoning_summary", &self.reasoning_summary)
1927 .field("context_tier", &self.context_tier)
1928 .field("streaming", &self.streaming)
1929 .field("system_message", &self.system_message)
1930 .field("tools", &self.tools)
1931 .field("canvases", &self.canvases)
1932 .field(
1933 "canvas_handler",
1934 &self.canvas_handler.as_ref().map(|_| "<set>"),
1935 )
1936 .field("request_canvas_renderer", &self.request_canvas_renderer)
1937 .field("request_extensions", &self.request_extensions)
1938 .field("extension_sdk_path", &self.extension_sdk_path)
1939 .field("extension_info", &self.extension_info)
1940 .field("canvas_provider", &self.canvas_provider)
1941 .field("available_tools", &self.available_tools)
1942 .field("excluded_tools", &self.excluded_tools)
1943 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
1944 .field("mcp_servers", &self.mcp_servers)
1945 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
1946 .field("embedding_cache_storage", &self.embedding_cache_storage)
1947 .field("enable_config_discovery", &self.enable_config_discovery)
1948 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
1949 .field(
1950 "organization_custom_instructions",
1951 &self
1952 .organization_custom_instructions
1953 .as_ref()
1954 .map(|_| "<redacted>"),
1955 )
1956 .field(
1957 "enable_on_demand_instruction_discovery",
1958 &self.enable_on_demand_instruction_discovery,
1959 )
1960 .field("enable_file_hooks", &self.enable_file_hooks)
1961 .field(
1962 "enable_host_git_operations",
1963 &self.enable_host_git_operations,
1964 )
1965 .field("enable_session_store", &self.enable_session_store)
1966 .field("enable_skills", &self.enable_skills)
1967 .field("enable_mcp_apps", &self.enable_mcp_apps)
1968 .field("skill_directories", &self.skill_directories)
1969 .field("instruction_directories", &self.instruction_directories)
1970 .field("plugin_directories", &self.plugin_directories)
1971 .field("large_output", &self.large_output)
1972 .field("tool_search", &self.tool_search)
1973 .field("disabled_skills", &self.disabled_skills)
1974 .field("hooks", &self.hooks)
1975 .field("custom_agents", &self.custom_agents)
1976 .field("default_agent", &self.default_agent)
1977 .field("agent", &self.agent)
1978 .field("infinite_sessions", &self.infinite_sessions)
1979 .field("provider", &self.provider)
1980 .field("capi", &self.capi)
1981 .field("enable_session_telemetry", &self.enable_session_telemetry)
1982 .field("enable_citations", &self.enable_citations)
1983 .field("session_limits", &self.session_limits)
1984 .field("model_capabilities", &self.model_capabilities)
1985 .field("memory", &self.memory)
1986 .field("config_directory", &self.config_directory)
1987 .field("working_directory", &self.working_directory)
1988 .field(
1989 "github_token",
1990 &self.github_token.as_ref().map(|_| "<redacted>"),
1991 )
1992 .field("remote_session", &self.remote_session)
1993 .field("cloud", &self.cloud)
1994 .field(
1995 "include_sub_agent_streaming_events",
1996 &self.include_sub_agent_streaming_events,
1997 )
1998 .field("commands", &self.commands)
1999 .field("exp_assignments", &self.exp_assignments)
2000 .field("enable_managed_settings", &self.enable_managed_settings)
2001 .field(
2002 "session_fs_provider",
2003 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2004 )
2005 .field(
2006 "permission_handler",
2007 &self.permission_handler.as_ref().map(|_| "<set>"),
2008 )
2009 .field(
2010 "elicitation_handler",
2011 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2012 )
2013 .field(
2014 "mcp_auth_handler",
2015 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
2016 )
2017 .field(
2018 "user_input_handler",
2019 &self.user_input_handler.as_ref().map(|_| "<set>"),
2020 )
2021 .field(
2022 "exit_plan_mode_handler",
2023 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2024 )
2025 .field(
2026 "auto_mode_switch_handler",
2027 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2028 )
2029 .field(
2030 "hooks_handler",
2031 &self.hooks_handler.as_ref().map(|_| "<set>"),
2032 )
2033 .field(
2034 "system_message_transform",
2035 &self.system_message_transform.as_ref().map(|_| "<set>"),
2036 )
2037 .finish()
2038 }
2039}
2040
2041impl Default for SessionConfig {
2042 fn default() -> Self {
2048 Self {
2049 session_id: None,
2050 model: None,
2051 client_name: None,
2052 reasoning_effort: None,
2053 reasoning_summary: None,
2054 context_tier: None,
2055 streaming: None,
2056 system_message: None,
2057 tools: None,
2058 canvases: None,
2059 canvas_handler: None,
2060 request_canvas_renderer: None,
2061 request_extensions: None,
2062 extension_sdk_path: None,
2063 extension_info: None,
2064 canvas_provider: None,
2065 available_tools: None,
2066 excluded_tools: None,
2067 excluded_builtin_agents: None,
2068 mcp_servers: None,
2069 mcp_oauth_token_storage: None,
2070 enable_config_discovery: None,
2071 skip_embedding_retrieval: None,
2072 organization_custom_instructions: None,
2073 enable_on_demand_instruction_discovery: None,
2074 enable_file_hooks: None,
2075 enable_host_git_operations: None,
2076 enable_session_store: None,
2077 enable_skills: None,
2078 embedding_cache_storage: None,
2079 enable_mcp_apps: None,
2080 skill_directories: None,
2081 instruction_directories: None,
2082 plugin_directories: None,
2083 large_output: None,
2084 tool_search: None,
2085 disabled_skills: None,
2086 hooks: None,
2087 custom_agents: None,
2088 default_agent: None,
2089 agent: None,
2090 infinite_sessions: None,
2091 provider: None,
2092 capi: None,
2093 providers: None,
2094 models: None,
2095 enable_session_telemetry: None,
2096 enable_citations: None,
2097 session_limits: None,
2098 model_capabilities: None,
2099 memory: None,
2100 config_directory: None,
2101 working_directory: None,
2102 github_token: None,
2103 remote_session: None,
2104 cloud: None,
2105 include_sub_agent_streaming_events: None,
2106 commands: None,
2107 exp_assignments: None,
2108 enable_managed_settings: None,
2109 session_fs_provider: None,
2110 permission_handler: None,
2111 elicitation_handler: None,
2112 mcp_auth_handler: None,
2113 user_input_handler: None,
2114 exit_plan_mode_handler: None,
2115 auto_mode_switch_handler: None,
2116 hooks_handler: None,
2117 permission_policy: None,
2118 system_message_transform: None,
2119 skip_custom_instructions: None,
2120 custom_agents_local_only: None,
2121 coauthor_enabled: None,
2122 manage_schedule_enabled: None,
2123 }
2124 }
2125}
2126
2127pub(crate) struct SessionConfigRuntime {
2133 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2134 pub permission_policy: Option<crate::permission::Policy>,
2135 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2136 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2137 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2138 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2139 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2140 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2141 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2142 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2143 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2144 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2145 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2146 pub commands: Option<Vec<CommandDefinition>>,
2147}
2148
2149impl SessionConfig {
2150 pub(crate) fn into_wire(
2162 mut self,
2163 session_id: Option<SessionId>,
2164 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2165 let permission_active =
2166 self.permission_handler.is_some() || self.permission_policy.is_some();
2167 let request_user_input = self.user_input_handler.is_some();
2168 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2169 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2170 let request_elicitation = self.elicitation_handler.is_some();
2171 let hooks_flag = self.hooks_handler.is_some();
2172
2173 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2174 if let Some(tools) = self.tools.as_mut() {
2175 for tool in tools.iter_mut() {
2176 if let Some(handler) = tool.handler.take()
2177 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2178 {
2179 return Err(crate::Error::with_message(
2180 crate::ErrorKind::InvalidConfig,
2181 format!("duplicate tool handler registered for name {:?}", tool.name),
2182 ));
2183 }
2184 }
2185 }
2186
2187 let wire_commands = self.commands.as_ref().map(|cmds| {
2188 cmds.iter()
2189 .map(|c| crate::wire::CommandWireDefinition {
2190 name: c.name.clone(),
2191 description: c.description.clone(),
2192 })
2193 .collect()
2194 });
2195 let wire_canvases = self.canvases.clone();
2196 let canvas_handler = self.canvas_handler.clone();
2197 let bearer_token_providers =
2198 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2199
2200 let wire = crate::wire::SessionCreateWire {
2201 session_id,
2202 model: self.model,
2203 client_name: self.client_name,
2204 reasoning_effort: self.reasoning_effort,
2205 reasoning_summary: self.reasoning_summary,
2206 context_tier: self.context_tier,
2207 streaming: self.streaming,
2208 system_message: self.system_message,
2209 tools: self.tools,
2210 canvases: wire_canvases,
2211 request_canvas_renderer: self.request_canvas_renderer,
2212 request_extensions: self.request_extensions,
2213 extension_sdk_path: self.extension_sdk_path,
2214 extension_info: self.extension_info,
2215 canvas_provider: self.canvas_provider,
2216 available_tools: self.available_tools,
2217 excluded_tools: self.excluded_tools,
2218 excluded_builtin_agents: self.excluded_builtin_agents,
2219 tool_filter_precedence: "excluded",
2220 mcp_servers: self.mcp_servers,
2221 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2222 embedding_cache_storage: self.embedding_cache_storage,
2223 env_value_mode: "direct",
2224 enable_config_discovery: self.enable_config_discovery,
2225 skip_embedding_retrieval: self.skip_embedding_retrieval,
2226 organization_custom_instructions: self.organization_custom_instructions,
2227 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2228 enable_file_hooks: self.enable_file_hooks,
2229 enable_host_git_operations: self.enable_host_git_operations,
2230 enable_session_store: self.enable_session_store,
2231 enable_skills: self.enable_skills,
2232 request_user_input,
2233 request_permission: permission_active,
2234 request_exit_plan_mode,
2235 request_auto_mode_switch,
2236 request_elicitation,
2237 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2238 hooks: hooks_flag,
2239 skill_directories: self.skill_directories,
2240 instruction_directories: self.instruction_directories,
2241 plugin_directories: self.plugin_directories,
2242 large_output: self.large_output,
2243 tool_search: self.tool_search,
2244 disabled_skills: self.disabled_skills,
2245 custom_agents: self.custom_agents,
2246 default_agent: self.default_agent,
2247 agent: self.agent,
2248 infinite_sessions: self.infinite_sessions,
2249 provider: self.provider,
2250 capi: self.capi,
2251 providers: self.providers,
2252 models: self.models,
2253 enable_session_telemetry: self.enable_session_telemetry,
2254 enable_citations: self.enable_citations,
2255 session_limits: self.session_limits,
2256 model_capabilities: self.model_capabilities,
2257 memory: self.memory,
2258 config_dir: self.config_directory,
2259 working_directory: self.working_directory,
2260 github_token: self.github_token,
2261 remote_session: self.remote_session,
2262 cloud: self.cloud,
2263 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2264 enable_github_telemetry_forwarding: None,
2265 commands: wire_commands,
2266 exp_assignments: self.exp_assignments,
2267 enable_managed_settings: self.enable_managed_settings,
2268 };
2269
2270 let runtime = SessionConfigRuntime {
2271 permission_handler: self.permission_handler,
2272 permission_policy: self.permission_policy,
2273 elicitation_handler: self.elicitation_handler,
2274 mcp_auth_handler: self.mcp_auth_handler,
2275 user_input_handler: self.user_input_handler,
2276 exit_plan_mode_handler: self.exit_plan_mode_handler,
2277 auto_mode_switch_handler: self.auto_mode_switch_handler,
2278 hooks_handler: self.hooks_handler,
2279 system_message_transform: self.system_message_transform,
2280 tool_handlers,
2281 canvas_handler,
2282 session_fs_provider: self.session_fs_provider,
2283 bearer_token_providers,
2284 commands: self.commands,
2285 };
2286
2287 Ok((wire, runtime))
2288 }
2289
2290 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2294 self.permission_handler = Some(handler);
2295 self
2296 }
2297
2298 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2301 self.elicitation_handler = Some(handler);
2302 self
2303 }
2304
2305 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2307 self.mcp_auth_handler = Some(handler);
2308 self
2309 }
2310
2311 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2314 self.user_input_handler = Some(handler);
2315 self
2316 }
2317
2318 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2320 self.exit_plan_mode_handler = Some(handler);
2321 self
2322 }
2323
2324 pub fn with_auto_mode_switch_handler(
2326 mut self,
2327 handler: Arc<dyn AutoModeSwitchHandler>,
2328 ) -> Self {
2329 self.auto_mode_switch_handler = Some(handler);
2330 self
2331 }
2332
2333 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2338 self.commands = Some(commands);
2339 self
2340 }
2341
2342 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2346 self.session_fs_provider = Some(provider);
2347 self
2348 }
2349
2350 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2353 self.hooks_handler = Some(hooks);
2354 self
2355 }
2356
2357 pub fn with_system_message_transform(
2361 mut self,
2362 transform: Arc<dyn SystemMessageTransform>,
2363 ) -> Self {
2364 self.system_message_transform = Some(transform);
2365 self
2366 }
2367
2368 pub fn approve_all_permissions(mut self) -> Self {
2374 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2375 self
2376 }
2377
2378 pub fn deny_all_permissions(mut self) -> Self {
2381 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2382 self
2383 }
2384
2385 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2390 where
2391 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2392 {
2393 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2394 self
2395 }
2396
2397 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2399 self.session_id = Some(id.into());
2400 self
2401 }
2402
2403 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2405 self.model = Some(model.into());
2406 self
2407 }
2408
2409 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2411 self.client_name = Some(name.into());
2412 self
2413 }
2414
2415 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2417 self.reasoning_effort = Some(effort.into());
2418 self
2419 }
2420
2421 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2423 self.reasoning_summary = Some(summary);
2424 self
2425 }
2426
2427 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2429 self.context_tier = Some(tier.into());
2430 self
2431 }
2432
2433 pub fn with_streaming(mut self, streaming: bool) -> Self {
2435 self.streaming = Some(streaming);
2436 self
2437 }
2438
2439 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2441 self.system_message = Some(system_message);
2442 self
2443 }
2444
2445 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2447 self.tools = Some(tools.into_iter().collect());
2448 self
2449 }
2450
2451 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2456 self.canvases = Some(canvases.into_iter().collect());
2457 self
2458 }
2459
2460 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2462 self.canvas_handler = Some(handler);
2463 self
2464 }
2465
2466 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2468 self.request_canvas_renderer = Some(request);
2469 self
2470 }
2471
2472 pub fn with_request_extensions(mut self, request: bool) -> Self {
2474 self.request_extensions = Some(request);
2475 self
2476 }
2477
2478 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2482 self.extension_sdk_path = Some(path.into());
2483 self
2484 }
2485
2486 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2488 self.extension_info = Some(extension_info);
2489 self
2490 }
2491
2492 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2495 self.canvas_provider = Some(canvas_provider);
2496 self
2497 }
2498
2499 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2501 where
2502 I: IntoIterator<Item = S>,
2503 S: Into<String>,
2504 {
2505 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2506 self
2507 }
2508
2509 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2511 where
2512 I: IntoIterator<Item = S>,
2513 S: Into<String>,
2514 {
2515 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2516 self
2517 }
2518
2519 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2521 where
2522 I: IntoIterator<Item = S>,
2523 S: Into<String>,
2524 {
2525 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2526 self
2527 }
2528
2529 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2531 self.mcp_servers = Some(servers);
2532 self
2533 }
2534
2535 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2543 self.mcp_oauth_token_storage = Some(mode.into());
2544 self
2545 }
2546
2547 pub fn with_embedding_cache_storage(
2549 mut self,
2550 embedding_cache_storage: impl Into<String>,
2551 ) -> Self {
2552 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2553 self
2554 }
2555
2556 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2558 self.enable_config_discovery = Some(enable);
2559 self
2560 }
2561
2562 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2564 self.skip_embedding_retrieval = Some(value);
2565 self
2566 }
2567
2568 pub fn with_organization_custom_instructions(
2570 mut self,
2571 instructions: impl Into<String>,
2572 ) -> Self {
2573 self.organization_custom_instructions = Some(instructions.into());
2574 self
2575 }
2576
2577 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2579 self.enable_on_demand_instruction_discovery = Some(value);
2580 self
2581 }
2582
2583 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2585 self.enable_file_hooks = Some(value);
2586 self
2587 }
2588
2589 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2591 self.enable_host_git_operations = Some(value);
2592 self
2593 }
2594
2595 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2597 self.enable_session_store = Some(value);
2598 self
2599 }
2600
2601 pub fn with_enable_skills(mut self, value: bool) -> Self {
2603 self.enable_skills = Some(value);
2604 self
2605 }
2606
2607 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2613 self.enable_mcp_apps = Some(enable);
2614 self
2615 }
2616
2617 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2619 where
2620 I: IntoIterator<Item = P>,
2621 P: Into<PathBuf>,
2622 {
2623 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2624 self
2625 }
2626
2627 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2631 where
2632 I: IntoIterator<Item = P>,
2633 P: Into<PathBuf>,
2634 {
2635 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2636 self
2637 }
2638
2639 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2641 where
2642 I: IntoIterator<Item = P>,
2643 P: Into<PathBuf>,
2644 {
2645 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2646 self
2647 }
2648
2649 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2651 self.large_output = Some(config);
2652 self
2653 }
2654
2655 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
2658 self.tool_search = Some(config);
2659 self
2660 }
2661
2662 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2664 where
2665 I: IntoIterator<Item = S>,
2666 S: Into<String>,
2667 {
2668 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2669 self
2670 }
2671
2672 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2674 mut self,
2675 agents: I,
2676 ) -> Self {
2677 self.custom_agents = Some(agents.into_iter().collect());
2678 self
2679 }
2680
2681 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2683 self.default_agent = Some(agent);
2684 self
2685 }
2686
2687 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2690 self.agent = Some(name.into());
2691 self
2692 }
2693
2694 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2697 self.infinite_sessions = Some(config);
2698 self
2699 }
2700
2701 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2703 self.provider = Some(provider);
2704 self
2705 }
2706
2707 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
2709 self.capi = Some(capi);
2710 self
2711 }
2712
2713 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
2719 self.providers = Some(providers);
2720 self
2721 }
2722
2723 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
2729 self.models = Some(models);
2730 self
2731 }
2732
2733 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2737 self.enable_session_telemetry = Some(enable);
2738 self
2739 }
2740
2741 pub fn with_enable_citations(mut self, enable: bool) -> Self {
2743 self.enable_citations = Some(enable);
2744 self
2745 }
2746
2747 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
2749 self.session_limits = Some(limits);
2750 self
2751 }
2752
2753 pub fn with_model_capabilities(
2755 mut self,
2756 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2757 ) -> Self {
2758 self.model_capabilities = Some(capabilities);
2759 self
2760 }
2761
2762 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
2764 self.memory = Some(memory);
2765 self
2766 }
2767
2768 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2770 self.config_directory = Some(dir.into());
2771 self
2772 }
2773
2774 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2777 self.working_directory = Some(dir.into());
2778 self
2779 }
2780
2781 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2786 self.github_token = Some(token.into());
2787 self
2788 }
2789
2790 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2793 self.include_sub_agent_streaming_events = Some(include);
2794 self
2795 }
2796
2797 pub fn with_remote_session(
2799 mut self,
2800 mode: crate::generated::api_types::RemoteSessionMode,
2801 ) -> Self {
2802 self.remote_session = Some(mode);
2803 self
2804 }
2805
2806 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
2808 self.cloud = Some(cloud);
2809 self
2810 }
2811
2812 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
2814 self.skip_custom_instructions = Some(value);
2815 self
2816 }
2817
2818 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
2820 self.custom_agents_local_only = Some(value);
2821 self
2822 }
2823
2824 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
2826 self.coauthor_enabled = Some(value);
2827 self
2828 }
2829
2830 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
2832 self.manage_schedule_enabled = Some(value);
2833 self
2834 }
2835
2836 #[doc(hidden)]
2844 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
2845 self.exp_assignments = Some(assignments);
2846 self
2847 }
2848
2849 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
2855 self.enable_managed_settings = Some(enabled);
2856 self
2857 }
2858}
2859#[derive(Clone)]
2866#[non_exhaustive]
2867pub struct ResumeSessionConfig {
2868 pub session_id: SessionId,
2870 pub model: Option<String>,
2873 pub client_name: Option<String>,
2875 pub reasoning_effort: Option<String>,
2877 pub reasoning_summary: Option<ReasoningSummary>,
2881 pub context_tier: Option<String>,
2884 pub streaming: Option<bool>,
2886 pub system_message: Option<SystemMessageConfig>,
2889 pub tools: Option<Vec<Tool>>,
2891 pub canvases: Option<Vec<CanvasDeclaration>>,
2893 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2896 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
2898 pub request_canvas_renderer: Option<bool>,
2900 pub request_extensions: Option<bool>,
2902 pub extension_sdk_path: Option<String>,
2906 pub extension_info: Option<ExtensionInfo>,
2908 pub canvas_provider: Option<CanvasProviderIdentity>,
2911 pub available_tools: Option<Vec<String>>,
2913 pub excluded_tools: Option<Vec<String>>,
2915 pub excluded_builtin_agents: Option<Vec<String>>,
2921 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
2923 pub mcp_oauth_token_storage: Option<String>,
2926 pub enable_config_discovery: Option<bool>,
2928 pub skip_embedding_retrieval: Option<bool>,
2930 pub embedding_cache_storage: Option<String>,
2932 pub organization_custom_instructions: Option<String>,
2934 pub enable_on_demand_instruction_discovery: Option<bool>,
2936 pub enable_file_hooks: Option<bool>,
2938 pub enable_host_git_operations: Option<bool>,
2940 pub enable_session_store: Option<bool>,
2942 pub enable_skills: Option<bool>,
2944 pub enable_mcp_apps: Option<bool>,
2950 pub skill_directories: Option<Vec<PathBuf>>,
2952 pub instruction_directories: Option<Vec<PathBuf>>,
2955 pub plugin_directories: Option<Vec<PathBuf>>,
2957 pub large_output: Option<LargeToolOutputConfig>,
2959 pub tool_search: Option<ToolSearchConfig>,
2962 pub disabled_skills: Option<Vec<String>>,
2964 pub hooks: Option<bool>,
2966 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2968 pub default_agent: Option<DefaultAgentConfig>,
2970 pub agent: Option<String>,
2972 pub infinite_sessions: Option<InfiniteSessionConfig>,
2974 pub provider: Option<ProviderConfig>,
2976 pub capi: Option<CapiSessionOptions>,
2982 pub providers: Option<Vec<NamedProviderConfig>>,
2988 pub models: Option<Vec<ProviderModelConfig>>,
2994 pub enable_session_telemetry: Option<bool>,
3002 pub enable_citations: Option<bool>,
3004 pub session_limits: Option<SessionLimitsConfig>,
3006 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3008 pub memory: Option<MemoryConfiguration>,
3010 pub config_directory: Option<PathBuf>,
3012 pub working_directory: Option<PathBuf>,
3014 pub github_token: Option<String>,
3017 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
3020 pub include_sub_agent_streaming_events: Option<bool>,
3022 pub commands: Option<Vec<CommandDefinition>>,
3026 #[doc(hidden)]
3031 pub exp_assignments: Option<Value>,
3032 pub enable_managed_settings: Option<bool>,
3038 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
3043 pub suppress_resume_event: Option<bool>,
3046 pub continue_pending_work: Option<bool>,
3054 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3057 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3060 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3062 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3065 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3068 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3071 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3073 pub(crate) permission_policy: Option<crate::permission::Policy>,
3075 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3077 pub skip_custom_instructions: Option<bool>,
3079 pub custom_agents_local_only: Option<bool>,
3081 pub coauthor_enabled: Option<bool>,
3083 pub manage_schedule_enabled: Option<bool>,
3085}
3086
3087impl std::fmt::Debug for ResumeSessionConfig {
3088 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3089 f.debug_struct("ResumeSessionConfig")
3090 .field("session_id", &self.session_id)
3091 .field("model", &self.model)
3092 .field("client_name", &self.client_name)
3093 .field("reasoning_effort", &self.reasoning_effort)
3094 .field("reasoning_summary", &self.reasoning_summary)
3095 .field("context_tier", &self.context_tier)
3096 .field("streaming", &self.streaming)
3097 .field("system_message", &self.system_message)
3098 .field("tools", &self.tools)
3099 .field("canvases", &self.canvases)
3100 .field(
3101 "canvas_handler",
3102 &self.canvas_handler.as_ref().map(|_| "<set>"),
3103 )
3104 .field("open_canvases", &self.open_canvases)
3105 .field("request_canvas_renderer", &self.request_canvas_renderer)
3106 .field("request_extensions", &self.request_extensions)
3107 .field("extension_sdk_path", &self.extension_sdk_path)
3108 .field("extension_info", &self.extension_info)
3109 .field("canvas_provider", &self.canvas_provider)
3110 .field("available_tools", &self.available_tools)
3111 .field("excluded_tools", &self.excluded_tools)
3112 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3113 .field("mcp_servers", &self.mcp_servers)
3114 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3115 .field("embedding_cache_storage", &self.embedding_cache_storage)
3116 .field("enable_config_discovery", &self.enable_config_discovery)
3117 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3118 .field(
3119 "organization_custom_instructions",
3120 &self
3121 .organization_custom_instructions
3122 .as_ref()
3123 .map(|_| "<redacted>"),
3124 )
3125 .field(
3126 "enable_on_demand_instruction_discovery",
3127 &self.enable_on_demand_instruction_discovery,
3128 )
3129 .field("enable_file_hooks", &self.enable_file_hooks)
3130 .field(
3131 "enable_host_git_operations",
3132 &self.enable_host_git_operations,
3133 )
3134 .field("enable_session_store", &self.enable_session_store)
3135 .field("enable_skills", &self.enable_skills)
3136 .field("enable_mcp_apps", &self.enable_mcp_apps)
3137 .field("skill_directories", &self.skill_directories)
3138 .field("instruction_directories", &self.instruction_directories)
3139 .field("plugin_directories", &self.plugin_directories)
3140 .field("large_output", &self.large_output)
3141 .field("tool_search", &self.tool_search)
3142 .field("disabled_skills", &self.disabled_skills)
3143 .field("hooks", &self.hooks)
3144 .field("custom_agents", &self.custom_agents)
3145 .field("default_agent", &self.default_agent)
3146 .field("agent", &self.agent)
3147 .field("infinite_sessions", &self.infinite_sessions)
3148 .field("provider", &self.provider)
3149 .field("capi", &self.capi)
3150 .field("enable_session_telemetry", &self.enable_session_telemetry)
3151 .field("enable_citations", &self.enable_citations)
3152 .field("session_limits", &self.session_limits)
3153 .field("model_capabilities", &self.model_capabilities)
3154 .field("memory", &self.memory)
3155 .field("config_directory", &self.config_directory)
3156 .field("working_directory", &self.working_directory)
3157 .field(
3158 "github_token",
3159 &self.github_token.as_ref().map(|_| "<redacted>"),
3160 )
3161 .field("remote_session", &self.remote_session)
3162 .field(
3163 "include_sub_agent_streaming_events",
3164 &self.include_sub_agent_streaming_events,
3165 )
3166 .field("commands", &self.commands)
3167 .field("exp_assignments", &self.exp_assignments)
3168 .field("enable_managed_settings", &self.enable_managed_settings)
3169 .field(
3170 "session_fs_provider",
3171 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3172 )
3173 .field(
3174 "permission_handler",
3175 &self.permission_handler.as_ref().map(|_| "<set>"),
3176 )
3177 .field(
3178 "elicitation_handler",
3179 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3180 )
3181 .field(
3182 "user_input_handler",
3183 &self.user_input_handler.as_ref().map(|_| "<set>"),
3184 )
3185 .field(
3186 "exit_plan_mode_handler",
3187 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3188 )
3189 .field(
3190 "auto_mode_switch_handler",
3191 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3192 )
3193 .field(
3194 "hooks_handler",
3195 &self.hooks_handler.as_ref().map(|_| "<set>"),
3196 )
3197 .field(
3198 "system_message_transform",
3199 &self.system_message_transform.as_ref().map(|_| "<set>"),
3200 )
3201 .field("suppress_resume_event", &self.suppress_resume_event)
3202 .field("continue_pending_work", &self.continue_pending_work)
3203 .finish()
3204 }
3205}
3206
3207impl ResumeSessionConfig {
3208 pub(crate) fn into_wire(
3216 mut self,
3217 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3218 let permission_active =
3219 self.permission_handler.is_some() || self.permission_policy.is_some();
3220 let request_user_input = self.user_input_handler.is_some();
3221 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3222 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3223 let request_elicitation = self.elicitation_handler.is_some();
3224 let hooks_flag = self.hooks_handler.is_some();
3225
3226 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3227 if let Some(tools) = self.tools.as_mut() {
3228 for tool in tools.iter_mut() {
3229 if let Some(handler) = tool.handler.take()
3230 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3231 {
3232 return Err(crate::Error::with_message(
3233 crate::ErrorKind::InvalidConfig,
3234 format!("duplicate tool handler registered for name {:?}", tool.name),
3235 ));
3236 }
3237 }
3238 }
3239
3240 let wire_commands = self.commands.as_ref().map(|cmds| {
3241 cmds.iter()
3242 .map(|c| crate::wire::CommandWireDefinition {
3243 name: c.name.clone(),
3244 description: c.description.clone(),
3245 })
3246 .collect()
3247 });
3248 let wire_canvases = self.canvases.clone();
3249 let canvas_handler = self.canvas_handler.clone();
3250 let bearer_token_providers =
3251 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3252
3253 let wire = crate::wire::SessionResumeWire {
3254 session_id: self.session_id,
3255 model: self.model,
3256 client_name: self.client_name,
3257 reasoning_effort: self.reasoning_effort,
3258 reasoning_summary: self.reasoning_summary,
3259 context_tier: self.context_tier,
3260 streaming: self.streaming,
3261 system_message: self.system_message,
3262 tools: self.tools,
3263 canvases: wire_canvases,
3264 open_canvases: self.open_canvases,
3265 request_canvas_renderer: self.request_canvas_renderer,
3266 request_extensions: self.request_extensions,
3267 extension_sdk_path: self.extension_sdk_path,
3268 extension_info: self.extension_info,
3269 canvas_provider: self.canvas_provider,
3270 available_tools: self.available_tools,
3271 excluded_tools: self.excluded_tools,
3272 excluded_builtin_agents: self.excluded_builtin_agents,
3273 tool_filter_precedence: "excluded",
3274 mcp_servers: self.mcp_servers,
3275 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3276 embedding_cache_storage: self.embedding_cache_storage,
3277 env_value_mode: "direct",
3278 enable_config_discovery: self.enable_config_discovery,
3279 skip_embedding_retrieval: self.skip_embedding_retrieval,
3280 organization_custom_instructions: self.organization_custom_instructions,
3281 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3282 enable_file_hooks: self.enable_file_hooks,
3283 enable_host_git_operations: self.enable_host_git_operations,
3284 enable_session_store: self.enable_session_store,
3285 enable_skills: self.enable_skills,
3286 request_user_input,
3287 request_permission: permission_active,
3288 request_exit_plan_mode,
3289 request_auto_mode_switch,
3290 request_elicitation,
3291 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3292 hooks: hooks_flag,
3293 skill_directories: self.skill_directories,
3294 instruction_directories: self.instruction_directories,
3295 plugin_directories: self.plugin_directories,
3296 large_output: self.large_output,
3297 tool_search: self.tool_search,
3298 disabled_skills: self.disabled_skills,
3299 custom_agents: self.custom_agents,
3300 default_agent: self.default_agent,
3301 agent: self.agent,
3302 infinite_sessions: self.infinite_sessions,
3303 provider: self.provider,
3304 capi: self.capi,
3305 providers: self.providers,
3306 models: self.models,
3307 enable_session_telemetry: self.enable_session_telemetry,
3308 enable_citations: self.enable_citations,
3309 session_limits: self.session_limits,
3310 model_capabilities: self.model_capabilities,
3311 memory: self.memory,
3312 config_dir: self.config_directory,
3313 working_directory: self.working_directory,
3314 github_token: self.github_token,
3315 remote_session: self.remote_session,
3316 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3317 enable_github_telemetry_forwarding: None,
3318 commands: wire_commands,
3319 exp_assignments: self.exp_assignments,
3320 enable_managed_settings: self.enable_managed_settings,
3321 suppress_resume_event: self.suppress_resume_event,
3322 continue_pending_work: self.continue_pending_work,
3323 };
3324
3325 let runtime = SessionConfigRuntime {
3326 permission_handler: self.permission_handler,
3327 permission_policy: self.permission_policy,
3328 elicitation_handler: self.elicitation_handler,
3329 mcp_auth_handler: self.mcp_auth_handler,
3330 user_input_handler: self.user_input_handler,
3331 exit_plan_mode_handler: self.exit_plan_mode_handler,
3332 auto_mode_switch_handler: self.auto_mode_switch_handler,
3333 hooks_handler: self.hooks_handler,
3334 system_message_transform: self.system_message_transform,
3335 tool_handlers,
3336 canvas_handler,
3337 session_fs_provider: self.session_fs_provider,
3338 bearer_token_providers,
3339 commands: self.commands,
3340 };
3341
3342 Ok((wire, runtime))
3343 }
3344
3345 pub fn new(session_id: SessionId) -> Self {
3350 Self {
3351 session_id,
3352 model: None,
3353 client_name: None,
3354 reasoning_effort: None,
3355 reasoning_summary: None,
3356 context_tier: None,
3357 streaming: None,
3358 system_message: None,
3359 tools: None,
3360 canvases: None,
3361 canvas_handler: None,
3362 open_canvases: None,
3363 request_canvas_renderer: None,
3364 request_extensions: None,
3365 extension_sdk_path: None,
3366 extension_info: None,
3367 canvas_provider: None,
3368 available_tools: None,
3369 excluded_tools: None,
3370 excluded_builtin_agents: None,
3371 mcp_servers: None,
3372 mcp_oauth_token_storage: None,
3373 enable_config_discovery: None,
3374 skip_embedding_retrieval: None,
3375 organization_custom_instructions: None,
3376 enable_on_demand_instruction_discovery: None,
3377 enable_file_hooks: None,
3378 enable_host_git_operations: None,
3379 enable_session_store: None,
3380 enable_skills: None,
3381 embedding_cache_storage: None,
3382 enable_mcp_apps: None,
3383 skill_directories: None,
3384 instruction_directories: None,
3385 plugin_directories: None,
3386 large_output: None,
3387 tool_search: None,
3388 disabled_skills: None,
3389 hooks: None,
3390 custom_agents: None,
3391 default_agent: None,
3392 agent: None,
3393 infinite_sessions: None,
3394 provider: None,
3395 capi: None,
3396 providers: None,
3397 models: None,
3398 enable_session_telemetry: None,
3399 enable_citations: None,
3400 session_limits: None,
3401 model_capabilities: None,
3402 memory: None,
3403 config_directory: None,
3404 working_directory: None,
3405 github_token: None,
3406 remote_session: None,
3407 include_sub_agent_streaming_events: None,
3408 commands: None,
3409 exp_assignments: None,
3410 enable_managed_settings: None,
3411 session_fs_provider: None,
3412 suppress_resume_event: None,
3413 continue_pending_work: None,
3414 permission_handler: None,
3415 elicitation_handler: None,
3416 mcp_auth_handler: None,
3417 user_input_handler: None,
3418 exit_plan_mode_handler: None,
3419 auto_mode_switch_handler: None,
3420 hooks_handler: None,
3421 permission_policy: None,
3422 system_message_transform: None,
3423 skip_custom_instructions: None,
3424 custom_agents_local_only: None,
3425 coauthor_enabled: None,
3426 manage_schedule_enabled: None,
3427 }
3428 }
3429
3430 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3432 self.permission_handler = Some(handler);
3433 self
3434 }
3435
3436 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3438 self.elicitation_handler = Some(handler);
3439 self
3440 }
3441
3442 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3444 self.mcp_auth_handler = Some(handler);
3445 self
3446 }
3447
3448 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3450 self.user_input_handler = Some(handler);
3451 self
3452 }
3453
3454 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3456 self.exit_plan_mode_handler = Some(handler);
3457 self
3458 }
3459
3460 pub fn with_auto_mode_switch_handler(
3462 mut self,
3463 handler: Arc<dyn AutoModeSwitchHandler>,
3464 ) -> Self {
3465 self.auto_mode_switch_handler = Some(handler);
3466 self
3467 }
3468
3469 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3472 self.hooks_handler = Some(hooks);
3473 self
3474 }
3475
3476 pub fn with_system_message_transform(
3478 mut self,
3479 transform: Arc<dyn SystemMessageTransform>,
3480 ) -> Self {
3481 self.system_message_transform = Some(transform);
3482 self
3483 }
3484
3485 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3489 self.commands = Some(commands);
3490 self
3491 }
3492
3493 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3496 self.session_fs_provider = Some(provider);
3497 self
3498 }
3499
3500 pub fn approve_all_permissions(mut self) -> Self {
3503 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3504 self
3505 }
3506
3507 pub fn deny_all_permissions(mut self) -> Self {
3510 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3511 self
3512 }
3513
3514 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3517 where
3518 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3519 {
3520 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3521 self
3522 }
3523
3524 pub fn with_model(mut self, model: impl Into<String>) -> Self {
3526 self.model = Some(model.into());
3527 self
3528 }
3529
3530 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3532 self.client_name = Some(name.into());
3533 self
3534 }
3535
3536 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3538 self.reasoning_effort = Some(effort.into());
3539 self
3540 }
3541
3542 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3544 self.reasoning_summary = Some(summary);
3545 self
3546 }
3547
3548 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3551 self.context_tier = Some(tier.into());
3552 self
3553 }
3554
3555 pub fn with_streaming(mut self, streaming: bool) -> Self {
3557 self.streaming = Some(streaming);
3558 self
3559 }
3560
3561 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3564 self.system_message = Some(system_message);
3565 self
3566 }
3567
3568 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3570 self.tools = Some(tools.into_iter().collect());
3571 self
3572 }
3573
3574 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3576 self.canvases = Some(canvases.into_iter().collect());
3577 self
3578 }
3579
3580 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
3582 self.canvas_handler = Some(handler);
3583 self
3584 }
3585
3586 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
3588 mut self,
3589 open_canvases: I,
3590 ) -> Self {
3591 self.open_canvases = Some(open_canvases.into_iter().collect());
3592 self
3593 }
3594
3595 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
3597 self.request_canvas_renderer = Some(request);
3598 self
3599 }
3600
3601 pub fn with_request_extensions(mut self, request: bool) -> Self {
3603 self.request_extensions = Some(request);
3604 self
3605 }
3606
3607 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
3611 self.extension_sdk_path = Some(path.into());
3612 self
3613 }
3614
3615 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
3617 self.extension_info = Some(extension_info);
3618 self
3619 }
3620
3621 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
3624 self.canvas_provider = Some(canvas_provider);
3625 self
3626 }
3627
3628 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
3630 where
3631 I: IntoIterator<Item = S>,
3632 S: Into<String>,
3633 {
3634 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
3635 self
3636 }
3637
3638 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
3640 where
3641 I: IntoIterator<Item = S>,
3642 S: Into<String>,
3643 {
3644 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
3645 self
3646 }
3647
3648 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
3650 where
3651 I: IntoIterator<Item = S>,
3652 S: Into<String>,
3653 {
3654 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
3655 self
3656 }
3657
3658 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
3660 self.mcp_servers = Some(servers);
3661 self
3662 }
3663
3664 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3667 self.mcp_oauth_token_storage = Some(mode.into());
3668 self
3669 }
3670
3671 pub fn with_embedding_cache_storage(
3673 mut self,
3674 embedding_cache_storage: impl Into<String>,
3675 ) -> Self {
3676 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3677 self
3678 }
3679
3680 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3682 self.enable_config_discovery = Some(enable);
3683 self
3684 }
3685
3686 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3688 self.skip_embedding_retrieval = Some(value);
3689 self
3690 }
3691
3692 pub fn with_organization_custom_instructions(
3694 mut self,
3695 instructions: impl Into<String>,
3696 ) -> Self {
3697 self.organization_custom_instructions = Some(instructions.into());
3698 self
3699 }
3700
3701 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3703 self.enable_on_demand_instruction_discovery = Some(value);
3704 self
3705 }
3706
3707 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3709 self.enable_file_hooks = Some(value);
3710 self
3711 }
3712
3713 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3715 self.enable_host_git_operations = Some(value);
3716 self
3717 }
3718
3719 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3721 self.enable_session_store = Some(value);
3722 self
3723 }
3724
3725 pub fn with_enable_skills(mut self, value: bool) -> Self {
3727 self.enable_skills = Some(value);
3728 self
3729 }
3730
3731 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3737 self.enable_mcp_apps = Some(enable);
3738 self
3739 }
3740
3741 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3743 where
3744 I: IntoIterator<Item = P>,
3745 P: Into<PathBuf>,
3746 {
3747 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3748 self
3749 }
3750
3751 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3755 where
3756 I: IntoIterator<Item = P>,
3757 P: Into<PathBuf>,
3758 {
3759 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3760 self
3761 }
3762
3763 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3765 where
3766 I: IntoIterator<Item = P>,
3767 P: Into<PathBuf>,
3768 {
3769 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3770 self
3771 }
3772
3773 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3775 self.large_output = Some(config);
3776 self
3777 }
3778
3779 pub fn with_tool_search(mut self, config: ToolSearchConfig) -> Self {
3782 self.tool_search = Some(config);
3783 self
3784 }
3785
3786 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3788 where
3789 I: IntoIterator<Item = S>,
3790 S: Into<String>,
3791 {
3792 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3793 self
3794 }
3795
3796 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3798 mut self,
3799 agents: I,
3800 ) -> Self {
3801 self.custom_agents = Some(agents.into_iter().collect());
3802 self
3803 }
3804
3805 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3807 self.default_agent = Some(agent);
3808 self
3809 }
3810
3811 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3813 self.agent = Some(name.into());
3814 self
3815 }
3816
3817 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3819 self.infinite_sessions = Some(config);
3820 self
3821 }
3822
3823 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3825 self.provider = Some(provider);
3826 self
3827 }
3828
3829 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3831 self.capi = Some(capi);
3832 self
3833 }
3834
3835 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3841 self.providers = Some(providers);
3842 self
3843 }
3844
3845 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3851 self.models = Some(models);
3852 self
3853 }
3854
3855 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3859 self.enable_session_telemetry = Some(enable);
3860 self
3861 }
3862
3863 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3865 self.enable_citations = Some(enable);
3866 self
3867 }
3868
3869 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3871 self.session_limits = Some(limits);
3872 self
3873 }
3874
3875 pub fn with_model_capabilities(
3877 mut self,
3878 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3879 ) -> Self {
3880 self.model_capabilities = Some(capabilities);
3881 self
3882 }
3883
3884 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3886 self.memory = Some(memory);
3887 self
3888 }
3889
3890 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3892 self.config_directory = Some(dir.into());
3893 self
3894 }
3895
3896 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3898 self.working_directory = Some(dir.into());
3899 self
3900 }
3901
3902 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3906 self.github_token = Some(token.into());
3907 self
3908 }
3909
3910 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3912 self.include_sub_agent_streaming_events = Some(include);
3913 self
3914 }
3915
3916 pub fn with_remote_session(
3918 mut self,
3919 mode: crate::generated::api_types::RemoteSessionMode,
3920 ) -> Self {
3921 self.remote_session = Some(mode);
3922 self
3923 }
3924
3925 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
3928 self.suppress_resume_event = Some(suppress);
3929 self
3930 }
3931
3932 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
3938 self.continue_pending_work = Some(continue_pending);
3939 self
3940 }
3941
3942 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3944 self.skip_custom_instructions = Some(value);
3945 self
3946 }
3947
3948 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3950 self.custom_agents_local_only = Some(value);
3951 self
3952 }
3953
3954 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3956 self.coauthor_enabled = Some(value);
3957 self
3958 }
3959
3960 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3962 self.manage_schedule_enabled = Some(value);
3963 self
3964 }
3965
3966 #[doc(hidden)]
3970 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
3971 self.exp_assignments = Some(assignments);
3972 self
3973 }
3974
3975 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3978 self.enable_managed_settings = Some(enabled);
3979 self
3980 }
3981}
3982
3983#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3989#[serde(rename_all = "camelCase")]
3990#[non_exhaustive]
3991pub struct SystemMessageConfig {
3992 #[serde(skip_serializing_if = "Option::is_none")]
3994 pub mode: Option<String>,
3995 #[serde(skip_serializing_if = "Option::is_none")]
3997 pub content: Option<String>,
3998 #[serde(skip_serializing_if = "Option::is_none")]
4000 pub sections: Option<HashMap<String, SectionOverride>>,
4001}
4002
4003impl SystemMessageConfig {
4004 pub fn new() -> Self {
4007 Self::default()
4008 }
4009
4010 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
4013 self.mode = Some(mode.into());
4014 self
4015 }
4016
4017 pub fn with_content(mut self, content: impl Into<String>) -> Self {
4020 self.content = Some(content.into());
4021 self
4022 }
4023
4024 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
4026 self.sections = Some(sections);
4027 self
4028 }
4029}
4030
4031#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4037#[serde(rename_all = "camelCase")]
4038pub struct SectionOverride {
4039 #[serde(skip_serializing_if = "Option::is_none")]
4042 pub action: Option<String>,
4043 #[serde(skip_serializing_if = "Option::is_none")]
4045 pub content: Option<String>,
4046}
4047
4048#[derive(Debug, Clone, Serialize, Deserialize)]
4050#[serde(rename_all = "camelCase")]
4051pub struct CreateSessionResult {
4052 pub session_id: SessionId,
4054 #[serde(skip_serializing_if = "Option::is_none")]
4056 pub workspace_path: Option<PathBuf>,
4057 #[serde(default, alias = "remote_url")]
4059 pub remote_url: Option<String>,
4060 #[serde(skip_serializing_if = "Option::is_none")]
4062 pub capabilities: Option<SessionCapabilities>,
4063}
4064
4065#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4067#[serde(rename_all = "camelCase")]
4068pub(crate) struct ResumeSessionResult {
4069 #[serde(default)]
4071 pub session_id: Option<SessionId>,
4072 #[serde(default, skip_serializing_if = "Option::is_none")]
4074 pub workspace_path: Option<PathBuf>,
4075 #[serde(default, alias = "remote_url")]
4077 pub remote_url: Option<String>,
4078 #[serde(default, skip_serializing_if = "Option::is_none")]
4080 pub capabilities: Option<SessionCapabilities>,
4081 #[serde(
4083 default,
4084 alias = "openCanvasInstances",
4085 skip_serializing_if = "Option::is_none"
4086 )]
4087 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4088}
4089
4090#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4092#[serde(rename_all = "lowercase")]
4093pub enum LogLevel {
4094 #[default]
4096 Info,
4097 Warning,
4099 Error,
4101}
4102
4103#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4108#[serde(rename_all = "camelCase")]
4109pub struct LogOptions {
4110 #[serde(skip_serializing_if = "Option::is_none")]
4112 pub level: Option<LogLevel>,
4113 #[serde(skip_serializing_if = "Option::is_none")]
4116 pub ephemeral: Option<bool>,
4117}
4118
4119impl LogOptions {
4120 pub fn with_level(mut self, level: LogLevel) -> Self {
4122 self.level = Some(level);
4123 self
4124 }
4125
4126 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4128 self.ephemeral = Some(ephemeral);
4129 self
4130 }
4131}
4132
4133#[derive(Debug, Clone, Default)]
4137pub struct SetModelOptions {
4138 pub reasoning_effort: Option<String>,
4141 pub reasoning_summary: Option<ReasoningSummary>,
4145 pub context_tier: Option<ContextTier>,
4148 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4152}
4153
4154impl SetModelOptions {
4155 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4157 self.reasoning_effort = Some(effort.into());
4158 self
4159 }
4160
4161 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4163 self.reasoning_summary = Some(summary);
4164 self
4165 }
4166
4167 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4169 self.context_tier = Some(tier);
4170 self
4171 }
4172
4173 pub fn with_model_capabilities(
4175 mut self,
4176 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4177 ) -> Self {
4178 self.model_capabilities = Some(caps);
4179 self
4180 }
4181}
4182
4183#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4190#[serde(rename_all = "camelCase")]
4191pub struct PingResponse {
4192 #[serde(default)]
4194 pub message: String,
4195 #[serde(default)]
4197 pub timestamp: String,
4198 #[serde(skip_serializing_if = "Option::is_none")]
4200 pub protocol_version: Option<u32>,
4201}
4202
4203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4205#[serde(rename_all = "camelCase")]
4206pub struct AttachmentLineRange {
4207 pub start: u32,
4209 pub end: u32,
4211}
4212
4213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4215#[serde(rename_all = "camelCase")]
4216pub struct AttachmentSelectionPosition {
4217 pub line: u32,
4219 pub character: u32,
4221}
4222
4223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4225#[serde(rename_all = "camelCase")]
4226pub struct AttachmentSelectionRange {
4227 pub start: AttachmentSelectionPosition,
4229 pub end: AttachmentSelectionPosition,
4231}
4232
4233#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4235#[serde(rename_all = "snake_case")]
4236#[non_exhaustive]
4237pub enum GitHubReferenceType {
4238 Issue,
4240 Pr,
4242 Discussion,
4244}
4245
4246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4252#[serde(rename_all = "camelCase")]
4253pub struct GitHubRepoPointer {
4254 #[serde(skip_serializing_if = "Option::is_none")]
4256 pub id: Option<i64>,
4257 pub name: String,
4259 pub owner: String,
4261}
4262
4263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4265#[serde(rename_all = "camelCase")]
4266pub struct GitHubFileDiffSide {
4267 pub path: String,
4269 pub r#ref: String,
4271 pub repo: GitHubRepoPointer,
4273}
4274
4275#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4277#[serde(rename_all = "camelCase")]
4278pub struct GitHubTreeComparisonSide {
4279 pub repo: GitHubRepoPointer,
4281 pub revision: String,
4283}
4284
4285#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4287#[serde(rename_all = "camelCase")]
4288pub struct GitHubSnippetLineRange {
4289 pub start: i64,
4291 pub end: i64,
4293}
4294
4295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4297#[serde(
4298 tag = "type",
4299 rename_all = "camelCase",
4300 rename_all_fields = "camelCase"
4301)]
4302#[non_exhaustive]
4303pub enum Attachment {
4304 File {
4306 path: PathBuf,
4308 #[serde(skip_serializing_if = "Option::is_none")]
4310 display_name: Option<String>,
4311 #[serde(skip_serializing_if = "Option::is_none")]
4313 line_range: Option<AttachmentLineRange>,
4314 },
4315 Directory {
4317 path: PathBuf,
4319 #[serde(skip_serializing_if = "Option::is_none")]
4321 display_name: Option<String>,
4322 },
4323 Selection {
4325 file_path: PathBuf,
4327 text: String,
4329 #[serde(skip_serializing_if = "Option::is_none")]
4331 display_name: Option<String>,
4332 selection: AttachmentSelectionRange,
4334 },
4335 Blob {
4337 data: String,
4339 mime_type: String,
4341 #[serde(skip_serializing_if = "Option::is_none")]
4343 display_name: Option<String>,
4344 },
4345 #[serde(rename = "github_reference")]
4347 GitHubReference {
4348 number: u64,
4350 title: String,
4352 reference_type: GitHubReferenceType,
4354 state: String,
4356 url: String,
4358 },
4359 #[serde(rename = "github_commit")]
4361 GitHubCommit {
4362 message: String,
4364 oid: String,
4366 repo: GitHubRepoPointer,
4368 url: String,
4370 },
4371 #[serde(rename = "github_release")]
4373 GitHubRelease {
4374 name: String,
4376 repo: GitHubRepoPointer,
4378 tag_name: String,
4380 url: String,
4382 },
4383 #[serde(rename = "github_actions_job")]
4385 GitHubActionsJob {
4386 #[serde(skip_serializing_if = "Option::is_none")]
4389 conclusion: Option<String>,
4390 job_id: i64,
4392 job_name: String,
4394 repo: GitHubRepoPointer,
4396 url: String,
4398 workflow_name: String,
4400 },
4401 #[serde(rename = "github_repository")]
4403 GitHubRepository {
4404 #[serde(skip_serializing_if = "Option::is_none")]
4406 description: Option<String>,
4407 #[serde(skip_serializing_if = "Option::is_none")]
4410 r#ref: Option<String>,
4411 repo: GitHubRepoPointer,
4413 url: String,
4415 },
4416 #[serde(rename = "github_file_diff")]
4418 GitHubFileDiff {
4419 #[serde(skip_serializing_if = "Option::is_none")]
4421 base: Option<GitHubFileDiffSide>,
4422 #[serde(skip_serializing_if = "Option::is_none")]
4424 head: Option<GitHubFileDiffSide>,
4425 url: String,
4427 },
4428 #[serde(rename = "github_tree_comparison")]
4430 GitHubTreeComparison {
4431 base: GitHubTreeComparisonSide,
4433 head: GitHubTreeComparisonSide,
4435 url: String,
4437 },
4438 #[serde(rename = "github_url")]
4440 GitHubUrl {
4441 url: String,
4443 },
4444 #[serde(rename = "github_file")]
4446 GitHubFile {
4447 path: String,
4449 r#ref: String,
4451 repo: GitHubRepoPointer,
4453 url: String,
4455 },
4456 #[serde(rename = "github_snippet")]
4458 GitHubSnippet {
4459 line_range: GitHubSnippetLineRange,
4461 path: String,
4463 r#ref: String,
4465 repo: GitHubRepoPointer,
4467 url: String,
4469 },
4470}
4471
4472impl Attachment {
4473 pub fn display_name(&self) -> Option<&str> {
4475 match self {
4476 Self::File { display_name, .. }
4477 | Self::Directory { display_name, .. }
4478 | Self::Selection { display_name, .. }
4479 | Self::Blob { display_name, .. } => display_name.as_deref(),
4480 Self::GitHubReference { .. }
4481 | Self::GitHubCommit { .. }
4482 | Self::GitHubRelease { .. }
4483 | Self::GitHubActionsJob { .. }
4484 | Self::GitHubRepository { .. }
4485 | Self::GitHubFileDiff { .. }
4486 | Self::GitHubTreeComparison { .. }
4487 | Self::GitHubUrl { .. }
4488 | Self::GitHubFile { .. }
4489 | Self::GitHubSnippet { .. } => None,
4490 }
4491 }
4492
4493 pub fn label(&self) -> Option<String> {
4495 if let Some(display_name) = self
4496 .display_name()
4497 .map(str::trim)
4498 .filter(|name| !name.is_empty())
4499 {
4500 return Some(display_name.to_string());
4501 }
4502
4503 match self {
4504 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4505 format!("#{}", number)
4506 } else {
4507 title.trim().to_string()
4508 }),
4509 _ => self.derived_display_name(),
4510 }
4511 }
4512
4513 pub fn ensure_display_name(&mut self) {
4515 if self
4516 .display_name()
4517 .map(str::trim)
4518 .is_some_and(|name| !name.is_empty())
4519 {
4520 return;
4521 }
4522
4523 let Some(derived_display_name) = self.derived_display_name() else {
4524 return;
4525 };
4526
4527 match self {
4528 Self::File { display_name, .. }
4529 | Self::Directory { display_name, .. }
4530 | Self::Selection { display_name, .. }
4531 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4532 Self::GitHubReference { .. }
4533 | Self::GitHubCommit { .. }
4534 | Self::GitHubRelease { .. }
4535 | Self::GitHubActionsJob { .. }
4536 | Self::GitHubRepository { .. }
4537 | Self::GitHubFileDiff { .. }
4538 | Self::GitHubTreeComparison { .. }
4539 | Self::GitHubUrl { .. }
4540 | Self::GitHubFile { .. }
4541 | Self::GitHubSnippet { .. } => {}
4542 }
4543 }
4544
4545 fn derived_display_name(&self) -> Option<String> {
4546 match self {
4547 Self::File { path, .. } | Self::Directory { path, .. } => {
4548 Some(attachment_name_from_path(path))
4549 }
4550 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4551 Self::Blob { .. } => Some("attachment".to_string()),
4552 Self::GitHubReference { .. }
4553 | Self::GitHubCommit { .. }
4554 | Self::GitHubRelease { .. }
4555 | Self::GitHubActionsJob { .. }
4556 | Self::GitHubRepository { .. }
4557 | Self::GitHubFileDiff { .. }
4558 | Self::GitHubTreeComparison { .. }
4559 | Self::GitHubUrl { .. }
4560 | Self::GitHubFile { .. }
4561 | Self::GitHubSnippet { .. } => None,
4562 }
4563 }
4564}
4565
4566fn attachment_name_from_path(path: &Path) -> String {
4567 path.file_name()
4568 .map(|name| name.to_string_lossy().into_owned())
4569 .filter(|name| !name.is_empty())
4570 .unwrap_or_else(|| {
4571 let full = path.to_string_lossy();
4572 if full.is_empty() {
4573 "attachment".to_string()
4574 } else {
4575 full.into_owned()
4576 }
4577 })
4578}
4579
4580pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
4582 for attachment in attachments {
4583 attachment.ensure_display_name();
4584 }
4585}
4586
4587#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4592#[serde(rename_all = "lowercase")]
4593#[non_exhaustive]
4594pub enum DeliveryMode {
4595 Enqueue,
4597 Immediate,
4599}
4600
4601#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4606#[serde(rename_all = "lowercase")]
4607#[non_exhaustive]
4608pub enum AgentMode {
4609 Interactive,
4611 Plan,
4613 Autopilot,
4615 Shell,
4617}
4618
4619#[derive(Debug, Clone)]
4648#[non_exhaustive]
4649pub struct MessageOptions {
4650 pub prompt: String,
4652 pub mode: Option<DeliveryMode>,
4658 pub agent_mode: Option<AgentMode>,
4662 pub attachments: Option<Vec<Attachment>>,
4664 pub wait_timeout: Option<Duration>,
4667 pub request_headers: Option<HashMap<String, String>>,
4671 pub traceparent: Option<String>,
4678 pub tracestate: Option<String>,
4682 pub display_prompt: Option<String>,
4684}
4685
4686impl MessageOptions {
4687 pub fn new(prompt: impl Into<String>) -> Self {
4689 Self {
4690 prompt: prompt.into(),
4691 mode: None,
4692 agent_mode: None,
4693 attachments: None,
4694 wait_timeout: None,
4695 request_headers: None,
4696 traceparent: None,
4697 tracestate: None,
4698 display_prompt: None,
4699 }
4700 }
4701
4702 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
4708 self.mode = Some(mode);
4709 self
4710 }
4711
4712 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
4716 self.agent_mode = Some(agent_mode);
4717 self
4718 }
4719
4720 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
4722 self.attachments = Some(attachments);
4723 self
4724 }
4725
4726 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
4728 self.wait_timeout = Some(timeout);
4729 self
4730 }
4731
4732 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
4734 self.request_headers = Some(headers);
4735 self
4736 }
4737
4738 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
4743 self.traceparent = ctx.traceparent;
4744 self.tracestate = ctx.tracestate;
4745 self
4746 }
4747
4748 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
4750 self.traceparent = Some(traceparent.into());
4751 self
4752 }
4753
4754 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
4756 self.tracestate = Some(tracestate.into());
4757 self
4758 }
4759
4760 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
4762 self.display_prompt = Some(display_prompt.into());
4763 self
4764 }
4765}
4766
4767impl From<&str> for MessageOptions {
4768 fn from(prompt: &str) -> Self {
4769 Self::new(prompt)
4770 }
4771}
4772
4773impl From<String> for MessageOptions {
4774 fn from(prompt: String) -> Self {
4775 Self::new(prompt)
4776 }
4777}
4778
4779impl From<&String> for MessageOptions {
4780 fn from(prompt: &String) -> Self {
4781 Self::new(prompt.clone())
4782 }
4783}
4784
4785#[derive(Debug, Clone, Serialize, Deserialize)]
4787#[serde(rename_all = "camelCase")]
4788#[non_exhaustive]
4789pub struct GetStatusResponse {
4790 pub version: String,
4792 pub protocol_version: u32,
4794}
4795
4796#[derive(Debug, Clone, Serialize, Deserialize)]
4798#[serde(rename_all = "camelCase")]
4799#[non_exhaustive]
4800pub struct GetAuthStatusResponse {
4801 pub is_authenticated: bool,
4803 #[serde(skip_serializing_if = "Option::is_none")]
4806 pub auth_type: Option<String>,
4807 #[serde(skip_serializing_if = "Option::is_none")]
4809 pub host: Option<String>,
4810 #[serde(skip_serializing_if = "Option::is_none")]
4812 pub login: Option<String>,
4813 #[serde(skip_serializing_if = "Option::is_none")]
4815 pub status_message: Option<String>,
4816}
4817
4818#[derive(Debug, Clone, Serialize, Deserialize)]
4822#[serde(rename_all = "camelCase")]
4823pub struct SessionEventNotification {
4824 pub session_id: SessionId,
4826 pub event: SessionEvent,
4828}
4829
4830#[derive(Debug, Clone, Serialize, Deserialize)]
4837#[serde(rename_all = "camelCase")]
4838pub struct SessionEvent {
4839 pub id: String,
4841 pub timestamp: String,
4843 pub parent_id: Option<String>,
4845 #[serde(skip_serializing_if = "Option::is_none")]
4847 pub ephemeral: Option<bool>,
4848 #[serde(skip_serializing_if = "Option::is_none")]
4851 pub agent_id: Option<String>,
4852 #[serde(skip_serializing_if = "Option::is_none")]
4854 pub debug_cli_received_at_ms: Option<i64>,
4855 #[serde(skip_serializing_if = "Option::is_none")]
4857 pub debug_ws_forwarded_at_ms: Option<i64>,
4858 #[serde(rename = "type")]
4860 pub event_type: String,
4861 pub data: Value,
4863}
4864
4865impl SessionEvent {
4866 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
4871 use serde::de::IntoDeserializer;
4872 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
4873 self.event_type.as_str().into_deserializer();
4874 crate::generated::SessionEventType::deserialize(deserializer)
4875 .unwrap_or(crate::generated::SessionEventType::Unknown)
4876 }
4877
4878 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
4884 serde_json::from_value(self.data.clone()).ok()
4885 }
4886
4887 pub fn is_transient_error(&self) -> bool {
4891 self.event_type == "session.error"
4892 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
4893 }
4894}
4895
4896#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4901#[serde(rename_all = "camelCase")]
4902#[non_exhaustive]
4903pub struct ToolInvocation {
4904 pub session_id: SessionId,
4906 pub tool_call_id: String,
4908 pub tool_name: String,
4910 pub arguments: Value,
4912 #[serde(skip)]
4920 pub available_tools: Option<Vec<CurrentToolMetadata>>,
4921 #[serde(default, skip_serializing_if = "Option::is_none")]
4926 pub traceparent: Option<String>,
4927 #[serde(default, skip_serializing_if = "Option::is_none")]
4930 pub tracestate: Option<String>,
4931}
4932
4933impl ToolInvocation {
4934 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
4955 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
4956 }
4957
4958 pub fn trace_context(&self) -> TraceContext {
4961 TraceContext {
4962 traceparent: self.traceparent.clone(),
4963 tracestate: self.tracestate.clone(),
4964 }
4965 }
4966}
4967
4968#[derive(Debug, Clone, Serialize, Deserialize)]
4970#[serde(rename_all = "camelCase")]
4971pub struct ToolBinaryResult {
4972 pub data: String,
4974 pub mime_type: String,
4976 pub r#type: String,
4978 #[serde(default, skip_serializing_if = "Option::is_none")]
4980 pub description: Option<String>,
4981}
4982
4983#[derive(Debug, Clone, Serialize, Deserialize)]
4990#[serde(rename_all = "camelCase")]
4991#[non_exhaustive]
4992pub struct ToolResultExpanded {
4993 pub text_result_for_llm: String,
4995 pub result_type: String,
4997 #[serde(default, skip_serializing_if = "Option::is_none")]
4999 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
5000 #[serde(skip_serializing_if = "Option::is_none")]
5002 pub session_log: Option<String>,
5003 #[serde(skip_serializing_if = "Option::is_none")]
5005 pub error: Option<String>,
5006 #[serde(default, skip_serializing_if = "Option::is_none")]
5008 pub tool_telemetry: Option<HashMap<String, Value>>,
5009 #[serde(default, skip_serializing_if = "Option::is_none")]
5011 pub tool_references: Option<Vec<String>>,
5012}
5013
5014impl ToolResultExpanded {
5015 pub fn new(text_result_for_llm: impl Into<String>, result_type: impl Into<String>) -> Self {
5019 Self {
5020 text_result_for_llm: text_result_for_llm.into(),
5021 result_type: result_type.into(),
5022 binary_results_for_llm: None,
5023 session_log: None,
5024 error: None,
5025 tool_telemetry: None,
5026 tool_references: None,
5027 }
5028 }
5029
5030 pub fn with_binary_results(mut self, results: Vec<ToolBinaryResult>) -> Self {
5032 self.binary_results_for_llm = Some(results);
5033 self
5034 }
5035
5036 pub fn with_session_log(mut self, session_log: impl Into<String>) -> Self {
5038 self.session_log = Some(session_log.into());
5039 self
5040 }
5041
5042 pub fn with_error(mut self, error: impl Into<String>) -> Self {
5044 self.error = Some(error.into());
5045 self
5046 }
5047
5048 pub fn with_tool_telemetry(mut self, telemetry: HashMap<String, Value>) -> Self {
5050 self.tool_telemetry = Some(telemetry);
5051 self
5052 }
5053
5054 pub fn with_tool_references<I, S>(mut self, references: I) -> Self
5056 where
5057 I: IntoIterator<Item = S>,
5058 S: Into<String>,
5059 {
5060 self.tool_references = Some(references.into_iter().map(Into::into).collect());
5061 self
5062 }
5063}
5064
5065#[derive(Debug, Clone, Serialize, Deserialize)]
5067#[serde(untagged)]
5068#[non_exhaustive]
5069pub enum ToolResult {
5070 Text(String),
5072 Expanded(ToolResultExpanded),
5074}
5075
5076#[derive(Debug, Clone, Serialize, Deserialize)]
5078#[serde(rename_all = "camelCase")]
5079pub struct ToolResultResponse {
5080 pub result: ToolResult,
5082}
5083
5084#[derive(Debug, Clone, Serialize, Deserialize)]
5086#[serde(rename_all = "camelCase")]
5087pub struct SessionMetadata {
5088 pub session_id: SessionId,
5090 pub start_time: String,
5092 pub modified_time: String,
5094 #[serde(skip_serializing_if = "Option::is_none")]
5096 pub summary: Option<String>,
5097 pub is_remote: bool,
5099}
5100
5101#[derive(Debug, Clone, Serialize, Deserialize)]
5103#[serde(rename_all = "camelCase")]
5104pub struct ListSessionsResponse {
5105 pub sessions: Vec<SessionMetadata>,
5107}
5108
5109#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5113#[serde(rename_all = "camelCase")]
5114pub struct SessionListFilter {
5115 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
5117 pub working_directory: Option<String>,
5118 #[serde(default, skip_serializing_if = "Option::is_none")]
5120 pub git_root: Option<String>,
5121 #[serde(default, skip_serializing_if = "Option::is_none")]
5123 pub repository: Option<String>,
5124 #[serde(default, skip_serializing_if = "Option::is_none")]
5126 pub branch: Option<String>,
5127}
5128
5129#[derive(Debug, Clone, Serialize, Deserialize)]
5131#[serde(rename_all = "camelCase")]
5132pub struct GetSessionMetadataResponse {
5133 #[serde(skip_serializing_if = "Option::is_none")]
5135 pub session: Option<SessionMetadata>,
5136}
5137
5138#[derive(Debug, Clone, Serialize, Deserialize)]
5140#[serde(rename_all = "camelCase")]
5141pub struct GetLastSessionIdResponse {
5142 #[serde(skip_serializing_if = "Option::is_none")]
5144 pub session_id: Option<SessionId>,
5145}
5146
5147#[derive(Debug, Clone, Serialize, Deserialize)]
5149#[serde(rename_all = "camelCase")]
5150pub struct GetForegroundSessionResponse {
5151 #[serde(skip_serializing_if = "Option::is_none")]
5153 pub session_id: Option<SessionId>,
5154}
5155
5156#[derive(Debug, Clone, Serialize, Deserialize)]
5158#[serde(rename_all = "camelCase")]
5159pub struct GetMessagesResponse {
5160 pub events: Vec<SessionEvent>,
5162}
5163
5164#[derive(Debug, Clone, Serialize, Deserialize)]
5166#[serde(rename_all = "camelCase")]
5167pub struct ElicitationResult {
5168 pub action: String,
5170 #[serde(skip_serializing_if = "Option::is_none")]
5172 pub content: Option<Value>,
5173}
5174
5175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5181#[serde(rename_all = "camelCase")]
5182#[non_exhaustive]
5183pub enum ElicitationMode {
5184 Form,
5186 Url,
5188 #[serde(other)]
5190 Unknown,
5191}
5192
5193#[derive(Debug, Clone, Serialize, Deserialize)]
5200#[serde(rename_all = "camelCase")]
5201pub struct ElicitationRequest {
5202 pub message: String,
5204 #[serde(skip_serializing_if = "Option::is_none")]
5206 pub requested_schema: Option<Value>,
5207 #[serde(skip_serializing_if = "Option::is_none")]
5209 pub mode: Option<ElicitationMode>,
5210 #[serde(skip_serializing_if = "Option::is_none")]
5212 pub elicitation_source: Option<String>,
5213 #[serde(skip_serializing_if = "Option::is_none")]
5215 pub url: Option<String>,
5216}
5217
5218#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5223#[serde(rename_all = "camelCase")]
5224pub struct SessionCapabilities {
5225 #[serde(skip_serializing_if = "Option::is_none")]
5227 pub ui: Option<UiCapabilities>,
5228}
5229
5230#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5232#[serde(rename_all = "camelCase")]
5233pub struct UiCapabilities {
5234 #[serde(skip_serializing_if = "Option::is_none")]
5236 pub elicitation: Option<bool>,
5237 #[serde(skip_serializing_if = "Option::is_none")]
5248 pub mcp_apps: Option<bool>,
5249 #[serde(skip_serializing_if = "Option::is_none")]
5251 pub canvases: Option<bool>,
5252}
5253
5254#[derive(Debug, Clone, Default)]
5256pub struct UiInputOptions<'a> {
5257 pub title: Option<&'a str>,
5259 pub description: Option<&'a str>,
5261 pub min_length: Option<u64>,
5263 pub max_length: Option<u64>,
5265 pub format: Option<InputFormat>,
5267 pub default: Option<&'a str>,
5269}
5270
5271#[derive(Debug, Clone, Copy)]
5273#[non_exhaustive]
5274pub enum InputFormat {
5275 Email,
5277 Uri,
5279 Date,
5281 DateTime,
5283}
5284
5285impl InputFormat {
5286 pub fn as_str(&self) -> &'static str {
5288 match self {
5289 Self::Email => "email",
5290 Self::Uri => "uri",
5291 Self::Date => "date",
5292 Self::DateTime => "date-time",
5293 }
5294 }
5295}
5296
5297pub use crate::generated::api_types::{
5302 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5303 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5304 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5305 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
5306};
5307
5308#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5314#[serde(rename_all = "kebab-case")]
5315#[non_exhaustive]
5316pub enum PermissionRequestKind {
5317 Shell,
5319 Write,
5321 Read,
5323 Url,
5325 Mcp,
5327 CustomTool,
5329 Memory,
5331 Hook,
5333 #[serde(other)]
5336 Unknown,
5337}
5338
5339#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5345#[serde(rename_all = "camelCase")]
5346pub struct PermissionRequestData {
5347 #[serde(default, skip_serializing_if = "Option::is_none")]
5351 pub kind: Option<PermissionRequestKind>,
5352 #[serde(default, skip_serializing_if = "Option::is_none")]
5355 pub tool_call_id: Option<String>,
5356 #[serde(flatten)]
5359 pub extra: Value,
5360}
5361
5362#[derive(Debug, Clone, Serialize, Deserialize)]
5364#[serde(rename_all = "camelCase")]
5365pub struct ExitPlanModeData {
5366 #[serde(default)]
5368 pub summary: String,
5369 #[serde(default, skip_serializing_if = "Option::is_none")]
5371 pub plan_content: Option<String>,
5372 #[serde(default)]
5374 pub actions: Vec<String>,
5375 #[serde(default = "default_recommended_action")]
5377 pub recommended_action: String,
5378}
5379
5380fn default_recommended_action() -> String {
5381 "autopilot".to_string()
5382}
5383
5384impl Default for ExitPlanModeData {
5385 fn default() -> Self {
5386 Self {
5387 summary: String::new(),
5388 plan_content: None,
5389 actions: Vec::new(),
5390 recommended_action: default_recommended_action(),
5391 }
5392 }
5393}
5394
5395#[cfg(test)]
5396mod tests {
5397 use std::path::PathBuf;
5398
5399 use serde_json::json;
5400
5401 use super::{
5402 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5403 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5404 CustomAgentConfig, DeliveryMode, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig,
5405 LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration,
5406 NamedProviderConfig, ProviderConfig, ProviderModelConfig, ReasoningSummary,
5407 ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, SystemMessageConfig, Tool,
5408 ToolBinaryResult, ToolResult, ToolResultExpanded, ToolResultResponse,
5409 ensure_attachment_display_names,
5410 };
5411 use crate::generated::session_events::TypedSessionEvent;
5412
5413 #[test]
5414 fn tool_builder_composes() {
5415 let tool = Tool::new("greet")
5416 .with_description("Say hello")
5417 .with_namespaced_name("hello/greet")
5418 .with_instructions("Pass the user's name")
5419 .with_parameters(json!({
5420 "type": "object",
5421 "properties": { "name": { "type": "string" } },
5422 "required": ["name"]
5423 }))
5424 .with_overrides_built_in_tool(true)
5425 .with_skip_permission(true);
5426 assert_eq!(tool.name, "greet");
5427 assert_eq!(tool.description, "Say hello");
5428 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5429 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5430 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5431 assert!(tool.overrides_built_in_tool);
5432 assert!(tool.skip_permission);
5433 }
5434
5435 #[test]
5436 fn tool_defer_serialization() {
5437 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5438 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5439 let value = serde_json::to_value(&tool).unwrap();
5440 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5441
5442 let plain = Tool::new("plain");
5443 let value = serde_json::to_value(&plain).unwrap();
5444 assert!(value.get("defer").is_none());
5445 }
5446
5447 #[test]
5448 fn custom_agent_config_builder_with_model() {
5449 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5450 .with_model("claude-haiku-4.5")
5451 .with_display_name("My Agent");
5452 assert_eq!(agent.name, "my-agent");
5453 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5454 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5455 }
5456
5457 #[test]
5458 fn custom_agent_config_serializes_model() {
5459 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5460 let wire = serde_json::to_value(&agent).unwrap();
5461 assert_eq!(wire["model"], "claude-haiku-4.5");
5462 assert_eq!(wire["name"], "model-agent");
5463 }
5464
5465 #[test]
5466 fn custom_agent_config_omits_model_when_none() {
5467 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5468 let wire = serde_json::to_value(&agent).unwrap();
5469 assert!(wire.get("model").is_none());
5470 }
5471
5472 #[test]
5473 #[should_panic(expected = "tool parameter schema must be a JSON object")]
5474 fn tool_with_parameters_panics_on_non_object_value() {
5475 let _ = Tool::new("noop").with_parameters(json!(null));
5476 }
5477
5478 #[test]
5479 fn tool_result_expanded_serializes_binary_results_for_llm() {
5480 let response = ToolResultResponse {
5481 result: ToolResult::Expanded(ToolResultExpanded {
5482 text_result_for_llm: "rendered chart".to_string(),
5483 result_type: "success".to_string(),
5484 binary_results_for_llm: Some(vec![ToolBinaryResult {
5485 data: "aW1n".to_string(),
5486 mime_type: "image/png".to_string(),
5487 r#type: "image".to_string(),
5488 description: Some("chart preview".to_string()),
5489 }]),
5490 session_log: None,
5491 error: None,
5492 tool_telemetry: None,
5493 tool_references: None,
5494 }),
5495 };
5496
5497 let wire = serde_json::to_value(&response).unwrap();
5498
5499 assert_eq!(
5500 wire,
5501 json!({
5502 "result": {
5503 "textResultForLlm": "rendered chart",
5504 "resultType": "success",
5505 "binaryResultsForLlm": [
5506 {
5507 "data": "aW1n",
5508 "mimeType": "image/png",
5509 "type": "image",
5510 "description": "chart preview"
5511 }
5512 ]
5513 }
5514 })
5515 );
5516 }
5517
5518 #[test]
5519 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
5520 let response = ToolResultResponse {
5521 result: ToolResult::Expanded(ToolResultExpanded {
5522 text_result_for_llm: "ok".to_string(),
5523 result_type: "success".to_string(),
5524 binary_results_for_llm: None,
5525 session_log: None,
5526 error: None,
5527 tool_telemetry: None,
5528 tool_references: None,
5529 }),
5530 };
5531
5532 let wire = serde_json::to_value(&response).unwrap();
5533
5534 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5535 assert!(wire["result"].get("binaryResultsForLlm").is_none());
5536 }
5537
5538 #[test]
5539 fn tool_result_expanded_serializes_tool_references() {
5540 let response = ToolResultResponse {
5541 result: ToolResult::Expanded(
5542 ToolResultExpanded::new("found 2 tools", "success")
5543 .with_tool_references(["get_weather", "check_status"]),
5544 ),
5545 };
5546
5547 let wire = serde_json::to_value(&response).unwrap();
5548
5549 assert_eq!(
5550 wire,
5551 json!({
5552 "result": {
5553 "textResultForLlm": "found 2 tools",
5554 "resultType": "success",
5555 "toolReferences": ["get_weather", "check_status"]
5556 }
5557 })
5558 );
5559 }
5560
5561 #[test]
5562 fn tool_result_expanded_omits_tool_references_when_none() {
5563 let response = ToolResultResponse {
5564 result: ToolResult::Expanded(ToolResultExpanded::new("ok", "success")),
5565 };
5566
5567 let wire = serde_json::to_value(&response).unwrap();
5568
5569 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5570 assert!(wire["result"].get("toolReferences").is_none());
5571 }
5572
5573 #[test]
5574 fn tool_result_expanded_with_tool_references_accepts_owned_strings() {
5575 let names: Vec<String> = vec!["alpha".to_string(), "beta".to_string()];
5578 let expanded = ToolResultExpanded::new("ok", "success").with_tool_references(names);
5579
5580 assert_eq!(
5581 expanded.tool_references.as_deref(),
5582 Some(["alpha".to_string(), "beta".to_string()].as_slice())
5583 );
5584 }
5585
5586 #[test]
5587 fn tool_result_expanded_deserializes_tool_references() {
5588 let wire = json!({
5589 "textResultForLlm": "found tools",
5590 "resultType": "success",
5591 "toolReferences": ["alpha", "beta"]
5592 });
5593
5594 let expanded: ToolResultExpanded = serde_json::from_value(wire).unwrap();
5595
5596 assert_eq!(
5597 expanded.tool_references.as_deref(),
5598 Some(["alpha".to_string(), "beta".to_string()].as_slice())
5599 );
5600 }
5601
5602 #[test]
5603 fn session_config_default_wire_flags_off_without_handlers() {
5604 let cfg = SessionConfig::default();
5605 assert_eq!(cfg.mcp_oauth_token_storage, None);
5606 let (wire, _runtime) = cfg
5610 .into_wire(Some(SessionId::from("default-flags")))
5611 .expect("default config has no duplicate handlers");
5612 assert!(!wire.request_user_input);
5613 assert!(!wire.request_permission);
5614 assert!(!wire.request_elicitation);
5615 assert!(!wire.request_exit_plan_mode);
5616 assert!(!wire.request_auto_mode_switch);
5617 assert!(!wire.hooks);
5618 assert!(!wire.request_mcp_apps);
5619 }
5620
5621 #[test]
5622 fn resume_session_config_new_wire_flags_off_without_handlers() {
5623 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
5624 assert_eq!(cfg.mcp_oauth_token_storage, None);
5625 let (wire, _runtime) = cfg
5626 .into_wire()
5627 .expect("default resume config has no duplicate handlers");
5628 assert!(!wire.request_user_input);
5629 assert!(!wire.request_permission);
5630 assert!(!wire.request_elicitation);
5631 assert!(!wire.request_exit_plan_mode);
5632 assert!(!wire.request_auto_mode_switch);
5633 assert!(!wire.hooks);
5634 assert!(!wire.request_mcp_apps);
5635 }
5636
5637 #[test]
5638 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5639 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
5640 assert_eq!(cfg.enable_mcp_apps, Some(true));
5641
5642 let (wire, _runtime) = cfg
5643 .into_wire(Some(SessionId::from("enable-mcp-apps")))
5644 .expect("enable_mcp_apps config has no duplicate handlers");
5645 assert!(wire.request_mcp_apps);
5646
5647 let json = serde_json::to_value(&wire).unwrap();
5648 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5649 }
5650
5651 #[test]
5652 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5653 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
5654 .with_enable_mcp_apps(true);
5655 assert_eq!(cfg.enable_mcp_apps, Some(true));
5656
5657 let (wire, _runtime) = cfg
5658 .into_wire()
5659 .expect("resume enable_mcp_apps config has no duplicate handlers");
5660 assert!(wire.request_mcp_apps);
5661
5662 let json = serde_json::to_value(&wire).unwrap();
5663 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5664 }
5665
5666 #[test]
5667 fn memory_configuration_constructors_and_serde() {
5668 assert!(MemoryConfiguration::enabled().enabled);
5669 assert!(!MemoryConfiguration::disabled().enabled);
5670 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
5671
5672 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
5673 assert_eq!(json, serde_json::json!({ "enabled": true }));
5674 }
5675
5676 #[test]
5677 fn session_config_with_memory_serializes() {
5678 let (wire, _runtime) = SessionConfig::default()
5679 .with_memory(MemoryConfiguration::enabled())
5680 .into_wire(Some(SessionId::from("memory-on")))
5681 .expect("no duplicate handlers");
5682 let json = serde_json::to_value(&wire).unwrap();
5683 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5684
5685 let (wire_off, _) = SessionConfig::default()
5686 .with_memory(MemoryConfiguration::disabled())
5687 .into_wire(Some(SessionId::from("memory-off")))
5688 .expect("no duplicate handlers");
5689 let json_off = serde_json::to_value(&wire_off).unwrap();
5690 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
5691
5692 let (empty_wire, _) = SessionConfig::default()
5694 .into_wire(Some(SessionId::from("memory-unset")))
5695 .expect("no duplicate handlers");
5696 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5697 assert!(empty_json.get("memory").is_none());
5698 }
5699
5700 #[test]
5701 fn resume_session_config_with_memory_serializes() {
5702 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
5703 .with_memory(MemoryConfiguration::enabled())
5704 .into_wire()
5705 .expect("no duplicate handlers");
5706 let json = serde_json::to_value(&wire).unwrap();
5707 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5708
5709 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
5711 .into_wire()
5712 .expect("no duplicate handlers");
5713 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5714 assert!(empty_json.get("memory").is_none());
5715 }
5716
5717 #[test]
5718 fn session_config_with_exp_assignments_serializes() {
5719 let assignments = serde_json::json!({
5720 "Parameters": { "copilot_exp_flag": "treatment" },
5721 "AssignmentContext": "ctx-123",
5722 });
5723 let (wire, _runtime) = SessionConfig::default()
5724 .with_exp_assignments(assignments.clone())
5725 .into_wire(Some(SessionId::from("exp-on")))
5726 .expect("no duplicate handlers");
5727 let json = serde_json::to_value(&wire).unwrap();
5728 assert_eq!(json["expAssignments"], assignments);
5729
5730 let (empty_wire, _) = SessionConfig::default()
5732 .into_wire(Some(SessionId::from("exp-unset")))
5733 .expect("no duplicate handlers");
5734 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5735 assert!(empty_json.get("expAssignments").is_none());
5736 }
5737
5738 #[test]
5739 fn resume_session_config_with_exp_assignments_serializes() {
5740 let assignments = serde_json::json!({
5741 "Parameters": { "copilot_exp_flag": "treatment" },
5742 "AssignmentContext": "ctx-456",
5743 });
5744 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
5745 .with_exp_assignments(assignments.clone())
5746 .into_wire()
5747 .expect("no duplicate handlers");
5748 let json = serde_json::to_value(&wire).unwrap();
5749 assert_eq!(json["expAssignments"], assignments);
5750
5751 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
5753 .into_wire()
5754 .expect("no duplicate handlers");
5755 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5756 assert!(empty_json.get("expAssignments").is_none());
5757 }
5758
5759 #[test]
5760 fn session_config_clone_preserves_exp_assignments() {
5761 let assignments = serde_json::json!({
5762 "Parameters": { "copilot_exp_flag": "treatment" },
5763 "AssignmentContext": "ctx-clone",
5764 });
5765 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
5766 let cloned = config.clone();
5767
5768 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5769
5770 let (wire, _runtime) = cloned
5771 .into_wire(Some(SessionId::from("exp-clone")))
5772 .expect("no duplicate handlers");
5773 let json = serde_json::to_value(&wire).unwrap();
5774 assert_eq!(json["expAssignments"], assignments);
5775 }
5776
5777 #[test]
5778 fn resume_session_config_clone_preserves_exp_assignments() {
5779 let assignments = serde_json::json!({
5780 "Parameters": { "copilot_exp_flag": "treatment" },
5781 "AssignmentContext": "ctx-clone-resume",
5782 });
5783 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
5784 .with_exp_assignments(assignments.clone());
5785 let cloned = config.clone();
5786
5787 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5788
5789 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
5790 let json = serde_json::to_value(&wire).unwrap();
5791 assert_eq!(json["expAssignments"], assignments);
5792 }
5793
5794 #[test]
5795 #[allow(clippy::field_reassign_with_default)]
5796 fn session_config_into_wire_serializes_bucket_b_fields() {
5797 use std::path::PathBuf;
5798
5799 use super::{CloudSessionOptions, CloudSessionRepository};
5800
5801 let mut cfg = SessionConfig::default();
5802 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5803 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5804 cfg.github_token = Some("ghs_secret".to_string());
5805 cfg.include_sub_agent_streaming_events = Some(false);
5806 cfg.enable_session_telemetry = Some(false);
5807 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
5808 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
5809 cfg.enable_on_demand_instruction_discovery = Some(false);
5810 cfg.cloud = Some(CloudSessionOptions::with_repository(
5811 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
5812 ));
5813
5814 let (wire, _runtime) = cfg
5815 .into_wire(Some(SessionId::from("custom-id")))
5816 .expect("no duplicate handlers");
5817 let wire_json = serde_json::to_value(&wire).unwrap();
5818 assert_eq!(wire_json["sessionId"], "custom-id");
5819 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5820 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5821 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5822 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
5823 assert_eq!(wire_json["enableSessionTelemetry"], false);
5824 assert_eq!(wire_json["reasoningSummary"], "concise");
5825 assert_eq!(wire_json["remoteSession"], "export");
5826 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5827 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
5828 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
5829 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
5830
5831 let (empty_wire, _) = SessionConfig::default()
5833 .into_wire(Some(SessionId::from("empty")))
5834 .expect("default has no duplicate handlers");
5835 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5836 assert!(empty_json.get("gitHubToken").is_none());
5837 assert!(empty_json.get("enableSessionTelemetry").is_none());
5838 assert!(empty_json.get("reasoningSummary").is_none());
5839 assert!(empty_json.get("remoteSession").is_none());
5840 assert!(
5841 empty_json
5842 .get("enableOnDemandInstructionDiscovery")
5843 .is_none()
5844 );
5845 assert!(empty_json.get("cloud").is_none());
5846 }
5847
5848 #[test]
5849 fn session_config_into_wire_serializes_named_providers_and_models() {
5850 let cfg = SessionConfig::default()
5851 .with_providers(vec![
5852 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
5853 .with_provider_type("openai")
5854 .with_wire_api("responses")
5855 .with_api_key("sk-test"),
5856 ])
5857 .with_models(vec![
5858 ProviderModelConfig::new("gpt-x", "my-openai")
5859 .with_wire_model("gpt-x-2025")
5860 .with_max_output_tokens(2048),
5861 ]);
5862
5863 let (wire, _) = cfg
5864 .into_wire(Some(SessionId::from("sess-providers")))
5865 .expect("no duplicate handlers");
5866 let wire_json = serde_json::to_value(&wire).unwrap();
5867 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
5868 assert_eq!(
5869 wire_json["providers"][0]["baseUrl"],
5870 "https://api.example.com/v1"
5871 );
5872 assert_eq!(wire_json["providers"][0]["type"], "openai");
5873 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
5874 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
5875 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
5876 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
5877 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
5878 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
5879
5880 let (empty_wire, _) = SessionConfig::default()
5881 .into_wire(Some(SessionId::from("empty")))
5882 .expect("default has no duplicate handlers");
5883 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5884 assert!(empty_json.get("providers").is_none());
5885 assert!(empty_json.get("models").is_none());
5886 }
5887
5888 #[test]
5889 fn resume_config_into_wire_serializes_named_providers_and_models() {
5890 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
5891 .with_providers(vec![
5892 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
5893 .with_provider_type("azure")
5894 .with_azure(AzureProviderOptions {
5895 api_version: Some("2024-10-21".to_string()),
5896 }),
5897 ])
5898 .with_models(vec![
5899 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
5900 ]);
5901
5902 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5903 let wire_json = serde_json::to_value(&wire).unwrap();
5904 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
5905 assert_eq!(wire_json["providers"][0]["type"], "azure");
5906 assert_eq!(
5907 wire_json["providers"][0]["azure"]["apiVersion"],
5908 "2024-10-21"
5909 );
5910 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
5911 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
5912 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
5913
5914 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
5915 .into_wire()
5916 .expect("default has no duplicate handlers");
5917 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5918 assert!(empty_json.get("providers").is_none());
5919 assert!(empty_json.get("models").is_none());
5920 }
5921
5922 #[test]
5923 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
5924 use std::path::PathBuf;
5925
5926 let cfg = SessionConfig {
5927 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
5928 large_output: Some(
5929 LargeToolOutputConfig::new()
5930 .with_enabled(true)
5931 .with_max_size_bytes(1024)
5932 .with_output_directory(PathBuf::from("/tmp/large-output")),
5933 ),
5934 ..Default::default()
5935 };
5936
5937 let (wire, _) = cfg
5938 .into_wire(Some(SessionId::from("sess-1")))
5939 .expect("no duplicate handlers");
5940 let wire_json = serde_json::to_value(&wire).unwrap();
5941 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
5942 assert_eq!(wire_json["largeOutput"]["enabled"], true);
5943 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
5944 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
5945
5946 let (empty_wire, _) = SessionConfig::default()
5947 .into_wire(Some(SessionId::from("empty")))
5948 .expect("default has no duplicate handlers");
5949 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5950 assert!(empty_json.get("pluginDirectories").is_none());
5951 assert!(empty_json.get("largeOutput").is_none());
5952 }
5953
5954 #[test]
5955 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
5956 use std::path::PathBuf;
5957
5958 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5959 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5960 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5961 cfg.github_token = Some("ghs_secret".to_string());
5962 cfg.include_sub_agent_streaming_events = Some(true);
5963 cfg.enable_session_telemetry = Some(false);
5964 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
5965 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
5966 cfg.enable_on_demand_instruction_discovery = Some(false);
5967
5968 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5969 let wire_json = serde_json::to_value(&wire).unwrap();
5970 assert_eq!(wire_json["sessionId"], "sess-1");
5971 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5972 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5973 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5974 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
5975 assert_eq!(wire_json["enableSessionTelemetry"], false);
5976 assert_eq!(wire_json["reasoningSummary"], "detailed");
5977 assert_eq!(wire_json["remoteSession"], "on");
5978 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5979
5980 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5982 .into_wire()
5983 .expect("default resume has no duplicate handlers");
5984 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5985 assert!(empty_json.get("reasoningSummary").is_none());
5986 assert!(empty_json.get("remoteSession").is_none());
5987 assert!(
5988 empty_json
5989 .get("enableOnDemandInstructionDiscovery")
5990 .is_none()
5991 );
5992 }
5993
5994 #[test]
5995 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
5996 use std::path::PathBuf;
5997
5998 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5999 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
6000 cfg.large_output = Some(
6001 LargeToolOutputConfig::new()
6002 .with_enabled(false)
6003 .with_max_size_bytes(2048)
6004 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
6005 );
6006
6007 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6008 let wire_json = serde_json::to_value(&wire).unwrap();
6009 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
6010 assert_eq!(wire_json["largeOutput"]["enabled"], false);
6011 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
6012 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
6013
6014 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6015 .into_wire()
6016 .expect("default resume has no duplicate handlers");
6017 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6018 assert!(empty_json.get("pluginDirectories").is_none());
6019 assert!(empty_json.get("largeOutput").is_none());
6020 }
6021
6022 #[test]
6023 fn session_config_builder_composes() {
6024 use indexmap::IndexMap;
6025
6026 let cfg = SessionConfig::default()
6027 .with_session_id(SessionId::from("sess-1"))
6028 .with_model("claude-sonnet-4")
6029 .with_client_name("test-app")
6030 .with_reasoning_effort("medium")
6031 .with_reasoning_summary(ReasoningSummary::Concise)
6032 .with_context_tier("long_context")
6033 .with_streaming(true)
6034 .with_tools([Tool::new("greet")])
6035 .with_available_tools(["bash", "view"])
6036 .with_excluded_tools(["dangerous"])
6037 .with_mcp_servers(IndexMap::new())
6038 .with_mcp_oauth_token_storage("persistent")
6039 .with_enable_config_discovery(true)
6040 .with_enable_on_demand_instruction_discovery(true)
6041 .with_skill_directories([PathBuf::from("/tmp/skills")])
6042 .with_disabled_skills(["broken-skill"])
6043 .with_agent("researcher")
6044 .with_config_directory(PathBuf::from("/tmp/config"))
6045 .with_working_directory(PathBuf::from("/tmp/work"))
6046 .with_github_token("ghp_test")
6047 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6048 .with_enable_session_telemetry(false)
6049 .with_include_sub_agent_streaming_events(false)
6050 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6051
6052 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
6053 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
6054 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6055 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
6056 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
6057 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
6058 assert_eq!(cfg.streaming, Some(true));
6059 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6060 assert_eq!(
6061 cfg.available_tools.as_deref(),
6062 Some(&["bash".to_string(), "view".to_string()][..])
6063 );
6064 assert_eq!(
6065 cfg.excluded_tools.as_deref(),
6066 Some(&["dangerous".to_string()][..])
6067 );
6068 assert!(cfg.mcp_servers.is_some());
6069 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6070 assert_eq!(cfg.enable_config_discovery, Some(true));
6071 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
6072 assert_eq!(
6073 cfg.skill_directories.as_deref(),
6074 Some(&[PathBuf::from("/tmp/skills")][..])
6075 );
6076 assert_eq!(
6077 cfg.disabled_skills.as_deref(),
6078 Some(&["broken-skill".to_string()][..])
6079 );
6080 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6081 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6082 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6083 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6084 assert_eq!(
6085 cfg.capi,
6086 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6087 );
6088 assert_eq!(cfg.enable_session_telemetry, Some(false));
6089 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
6090 assert_eq!(
6091 cfg.extension_info,
6092 Some(ExtensionInfo::new("github-app", "counter"))
6093 );
6094 }
6095
6096 #[test]
6097 fn resume_session_config_builder_composes() {
6098 use indexmap::IndexMap;
6099
6100 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
6101 .with_client_name("test-app")
6102 .with_reasoning_summary(ReasoningSummary::None)
6103 .with_context_tier("default")
6104 .with_streaming(true)
6105 .with_tools([Tool::new("greet")])
6106 .with_available_tools(["bash", "view"])
6107 .with_excluded_tools(["dangerous"])
6108 .with_mcp_servers(IndexMap::new())
6109 .with_mcp_oauth_token_storage("persistent")
6110 .with_enable_config_discovery(true)
6111 .with_enable_on_demand_instruction_discovery(false)
6112 .with_skill_directories([PathBuf::from("/tmp/skills")])
6113 .with_disabled_skills(["broken-skill"])
6114 .with_agent("researcher")
6115 .with_config_directory(PathBuf::from("/tmp/config"))
6116 .with_working_directory(PathBuf::from("/tmp/work"))
6117 .with_github_token("ghp_test")
6118 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6119 .with_enable_session_telemetry(false)
6120 .with_include_sub_agent_streaming_events(true)
6121 .with_suppress_resume_event(true)
6122 .with_continue_pending_work(true)
6123 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
6124
6125 assert_eq!(cfg.session_id.as_str(), "sess-2");
6126 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
6127 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
6128 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
6129 assert_eq!(cfg.streaming, Some(true));
6130 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
6131 assert_eq!(
6132 cfg.available_tools.as_deref(),
6133 Some(&["bash".to_string(), "view".to_string()][..])
6134 );
6135 assert_eq!(
6136 cfg.excluded_tools.as_deref(),
6137 Some(&["dangerous".to_string()][..])
6138 );
6139 assert!(cfg.mcp_servers.is_some());
6140 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
6141 assert_eq!(cfg.enable_config_discovery, Some(true));
6142 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
6143 assert_eq!(
6144 cfg.skill_directories.as_deref(),
6145 Some(&[PathBuf::from("/tmp/skills")][..])
6146 );
6147 assert_eq!(
6148 cfg.disabled_skills.as_deref(),
6149 Some(&["broken-skill".to_string()][..])
6150 );
6151 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
6152 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
6153 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
6154 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
6155 assert_eq!(
6156 cfg.capi,
6157 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6158 );
6159 assert_eq!(cfg.enable_session_telemetry, Some(false));
6160 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
6161 assert_eq!(cfg.suppress_resume_event, Some(true));
6162 assert_eq!(cfg.continue_pending_work, Some(true));
6163 assert_eq!(
6164 cfg.extension_info,
6165 Some(ExtensionInfo::new("github-app", "counter"))
6166 );
6167 }
6168
6169 #[test]
6173 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
6174 let cfg =
6175 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
6176 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6177 let json = serde_json::to_value(&wire).unwrap();
6178 assert_eq!(json["continuePendingWork"], true);
6179
6180 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6182 .into_wire()
6183 .expect("no duplicate handlers");
6184 let json = serde_json::to_value(&wire).unwrap();
6185 assert!(json.get("continuePendingWork").is_none());
6186 }
6187
6188 #[test]
6192 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
6193 let cfg =
6194 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
6195 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6196 let json = serde_json::to_value(&wire).unwrap();
6197 assert_eq!(json["disableResume"], true);
6198 assert!(json.get("suppressResumeEvent").is_none());
6199 }
6200
6201 #[test]
6204 fn session_config_serializes_instruction_directories_to_camel_case() {
6205 let cfg =
6206 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
6207 let (wire, _) = cfg
6208 .into_wire(Some(SessionId::from("instr-on")))
6209 .expect("no duplicate handlers");
6210 let json = serde_json::to_value(&wire).unwrap();
6211 assert_eq!(
6212 json["instructionDirectories"],
6213 serde_json::json!(["/tmp/instr"])
6214 );
6215
6216 let (wire, _) = SessionConfig::default()
6218 .into_wire(Some(SessionId::from("instr-off")))
6219 .expect("no duplicate handlers");
6220 let json = serde_json::to_value(&wire).unwrap();
6221 assert!(json.get("instructionDirectories").is_none());
6222 }
6223
6224 #[test]
6227 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
6228 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
6229 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
6230 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6231 let json = serde_json::to_value(&wire).unwrap();
6232 assert_eq!(
6233 json["instructionDirectories"],
6234 serde_json::json!(["/tmp/instr"])
6235 );
6236
6237 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6238 .into_wire()
6239 .expect("no duplicate handlers");
6240 let json = serde_json::to_value(&wire).unwrap();
6241 assert!(json.get("instructionDirectories").is_none());
6242 }
6243
6244 #[test]
6245 fn custom_agent_config_builder_composes() {
6246 use indexmap::IndexMap;
6247
6248 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
6249 .with_display_name("Research Assistant")
6250 .with_description("Investigates technical questions.")
6251 .with_tools(["bash", "view"])
6252 .with_mcp_servers(IndexMap::new())
6253 .with_infer(true)
6254 .with_skills(["rust-coding-skill"]);
6255
6256 assert_eq!(cfg.name, "researcher");
6257 assert_eq!(cfg.prompt, "You are a research assistant.");
6258 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
6259 assert_eq!(
6260 cfg.description.as_deref(),
6261 Some("Investigates technical questions.")
6262 );
6263 assert_eq!(
6264 cfg.tools.as_deref(),
6265 Some(&["bash".to_string(), "view".to_string()][..])
6266 );
6267 assert!(cfg.mcp_servers.is_some());
6268 assert_eq!(cfg.infer, Some(true));
6269 assert_eq!(
6270 cfg.skills.as_deref(),
6271 Some(&["rust-coding-skill".to_string()][..])
6272 );
6273 }
6274
6275 #[test]
6276 fn mcp_servers_serialize_in_insertion_order() {
6277 use indexmap::IndexMap;
6278
6279 let order = [
6285 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
6286 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
6287 ];
6288 let mut servers = IndexMap::new();
6289 for name in order {
6290 servers.insert(
6291 name.to_string(),
6292 McpServerConfig::Stdio(McpStdioServerConfig {
6293 command: "run".to_string(),
6294 ..Default::default()
6295 }),
6296 );
6297 }
6298
6299 let (wire, _runtime) = SessionConfig::default()
6300 .with_mcp_servers(servers)
6301 .into_wire(None)
6302 .expect("into_wire should succeed");
6303 let json = serde_json::to_string(&wire).expect("serialize wire");
6304
6305 let positions: Vec<usize> = order
6306 .iter()
6307 .map(|name| {
6308 json.find(&format!("\"{name}\""))
6309 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
6310 })
6311 .collect();
6312 let mut ascending = positions.clone();
6313 ascending.sort_unstable();
6314 assert_eq!(
6315 positions, ascending,
6316 "mcp server keys must serialize in insertion order: {json}"
6317 );
6318 }
6319
6320 #[test]
6321 fn infinite_session_config_builder_composes() {
6322 let cfg = InfiniteSessionConfig::new()
6323 .with_enabled(true)
6324 .with_background_compaction_threshold(0.75)
6325 .with_buffer_exhaustion_threshold(0.92);
6326
6327 assert_eq!(cfg.enabled, Some(true));
6328 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
6329 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
6330 }
6331
6332 #[test]
6333 fn provider_config_builder_composes() {
6334 use std::collections::HashMap;
6335
6336 let mut headers = HashMap::new();
6337 headers.insert("X-Custom".to_string(), "value".to_string());
6338
6339 let cfg = ProviderConfig::new("https://api.example.com")
6340 .with_provider_type("openai")
6341 .with_wire_api("completions")
6342 .with_transport("websockets")
6343 .with_api_key("sk-test")
6344 .with_bearer_token("bearer-test")
6345 .with_headers(headers)
6346 .with_model_id("gpt-4")
6347 .with_wire_model("azure-gpt-4-deployment")
6348 .with_max_prompt_tokens(8192)
6349 .with_max_output_tokens(2048);
6350
6351 assert_eq!(cfg.base_url, "https://api.example.com");
6352 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
6353 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
6354 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
6355 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
6356 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
6357 assert_eq!(
6358 cfg.headers
6359 .as_ref()
6360 .and_then(|h| h.get("X-Custom"))
6361 .map(String::as_str),
6362 Some("value"),
6363 );
6364 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
6365 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
6366 assert_eq!(cfg.max_prompt_tokens, Some(8192));
6367 assert_eq!(cfg.max_output_tokens, Some(2048));
6368
6369 let wire = serde_json::to_value(&cfg).unwrap();
6371 assert_eq!(wire["modelId"], "gpt-4");
6372 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
6373 assert_eq!(wire["maxPromptTokens"], 8192);
6374 assert_eq!(wire["maxOutputTokens"], 2048);
6375
6376 let unset = ProviderConfig::new("https://api.example.com");
6377 let wire_unset = serde_json::to_value(&unset).unwrap();
6378 assert!(wire_unset.get("modelId").is_none());
6379 assert!(wire_unset.get("wireModel").is_none());
6380 assert!(wire_unset.get("maxPromptTokens").is_none());
6381 assert!(wire_unset.get("maxOutputTokens").is_none());
6382 }
6383
6384 #[test]
6385 fn capi_session_options_builder_composes_and_serializes() {
6386 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
6387
6388 assert_eq!(cfg.enable_web_socket_responses, Some(false));
6389
6390 let wire = serde_json::to_value(&cfg).unwrap();
6391 assert_eq!(
6392 wire,
6393 serde_json::json!({ "enableWebSocketResponses": false })
6394 );
6395
6396 let unset = CapiSessionOptions::new();
6397 let wire_unset = serde_json::to_value(&unset).unwrap();
6398 assert!(wire_unset.get("enableWebSocketResponses").is_none());
6399 }
6400
6401 #[test]
6402 fn session_config_with_capi_serializes() {
6403 let (wire, _) = SessionConfig::default()
6404 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6405 .into_wire(Some(SessionId::from("capi-create")))
6406 .expect("no duplicate handlers");
6407 let json = serde_json::to_value(&wire).unwrap();
6408 assert_eq!(
6409 json["capi"],
6410 serde_json::json!({ "enableWebSocketResponses": false })
6411 );
6412
6413 let (empty_wire, _) = SessionConfig::default()
6414 .into_wire(Some(SessionId::from("capi-create-unset")))
6415 .expect("no duplicate handlers");
6416 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6417 assert!(empty_json.get("capi").is_none());
6418 }
6419
6420 #[test]
6421 fn resume_session_config_with_capi_serializes() {
6422 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
6423 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6424 .into_wire()
6425 .expect("no duplicate handlers");
6426 let json = serde_json::to_value(&wire).unwrap();
6427 assert_eq!(
6428 json["capi"],
6429 serde_json::json!({ "enableWebSocketResponses": false })
6430 );
6431
6432 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
6433 .into_wire()
6434 .expect("no duplicate handlers");
6435 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6436 assert!(empty_json.get("capi").is_none());
6437 }
6438
6439 #[test]
6440 fn system_message_config_builder_composes() {
6441 use std::collections::HashMap;
6442
6443 let cfg = SystemMessageConfig::new()
6444 .with_mode("replace")
6445 .with_content("Custom system message.")
6446 .with_sections(HashMap::new());
6447
6448 assert_eq!(cfg.mode.as_deref(), Some("replace"));
6449 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
6450 assert!(cfg.sections.is_some());
6451 }
6452
6453 #[test]
6454 fn delivery_mode_serializes_to_kebab_case_strings() {
6455 assert_eq!(
6456 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
6457 "\"enqueue\""
6458 );
6459 assert_eq!(
6460 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
6461 "\"immediate\""
6462 );
6463 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
6464 assert_eq!(parsed, DeliveryMode::Immediate);
6465 }
6466
6467 #[test]
6468 fn agent_mode_serializes_to_kebab_case_strings() {
6469 assert_eq!(
6470 serde_json::to_string(&AgentMode::Interactive).unwrap(),
6471 "\"interactive\""
6472 );
6473 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
6474 assert_eq!(
6475 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
6476 "\"autopilot\""
6477 );
6478 assert_eq!(
6479 serde_json::to_string(&AgentMode::Shell).unwrap(),
6480 "\"shell\""
6481 );
6482 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
6483 assert_eq!(parsed, AgentMode::Plan);
6484 }
6485
6486 #[test]
6487 fn connection_state_distinguishes_variants() {
6488 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
6491 }
6492
6493 #[test]
6499 fn session_event_round_trips_agent_id_on_envelope() {
6500 let wire = json!({
6501 "id": "evt-1",
6502 "timestamp": "2026-04-30T12:00:00Z",
6503 "parentId": null,
6504 "agentId": "sub-agent-42",
6505 "type": "assistant.message",
6506 "data": { "message": "hi" }
6507 });
6508
6509 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
6510 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6511
6512 let roundtripped = serde_json::to_value(&event).unwrap();
6514 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6515
6516 let main_agent_event: SessionEvent = serde_json::from_value(json!({
6518 "id": "evt-2",
6519 "timestamp": "2026-04-30T12:00:01Z",
6520 "parentId": null,
6521 "type": "session.idle",
6522 "data": {}
6523 }))
6524 .unwrap();
6525 assert!(main_agent_event.agent_id.is_none());
6526 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
6527 assert!(roundtripped.get("agentId").is_none());
6528 }
6529
6530 #[test]
6532 fn typed_session_event_round_trips_agent_id_on_envelope() {
6533 let wire = json!({
6534 "id": "evt-1",
6535 "timestamp": "2026-04-30T12:00:00Z",
6536 "parentId": null,
6537 "agentId": "sub-agent-42",
6538 "type": "session.idle",
6539 "data": {}
6540 });
6541
6542 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
6543 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6544
6545 let roundtripped = serde_json::to_value(&event).unwrap();
6546 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6547 }
6548
6549 #[test]
6550 fn connection_state_variants_compile() {
6551 let _ = ConnectionState::Disconnected;
6555 let _ = ConnectionState::Connecting;
6556 let _ = ConnectionState::Connected;
6557 let _ = ConnectionState::Error;
6558 }
6559
6560 #[test]
6561 fn deserializes_runtime_attachment_variants() {
6562 let attachments: Vec<Attachment> = serde_json::from_value(json!([
6563 {
6564 "type": "file",
6565 "path": "/tmp/file.rs",
6566 "displayName": "file.rs",
6567 "lineRange": { "start": 7, "end": 12 }
6568 },
6569 {
6570 "type": "directory",
6571 "path": "/tmp/project",
6572 "displayName": "project"
6573 },
6574 {
6575 "type": "selection",
6576 "filePath": "/tmp/lib.rs",
6577 "displayName": "lib.rs",
6578 "text": "fn main() {}",
6579 "selection": {
6580 "start": { "line": 1, "character": 2 },
6581 "end": { "line": 3, "character": 4 }
6582 }
6583 },
6584 {
6585 "type": "blob",
6586 "data": "Zm9v",
6587 "mimeType": "image/png",
6588 "displayName": "image.png"
6589 },
6590 {
6591 "type": "github_reference",
6592 "number": 42,
6593 "title": "Fix rendering",
6594 "referenceType": "issue",
6595 "state": "open",
6596 "url": "https://github.com/example/repo/issues/42"
6597 }
6598 ]))
6599 .expect("attachments should deserialize");
6600
6601 assert_eq!(attachments.len(), 5);
6602 assert!(matches!(
6603 &attachments[0],
6604 Attachment::File {
6605 path,
6606 display_name,
6607 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
6608 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
6609 ));
6610 assert!(matches!(
6611 &attachments[1],
6612 Attachment::Directory { path, display_name }
6613 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
6614 ));
6615 assert!(matches!(
6616 &attachments[2],
6617 Attachment::Selection {
6618 file_path,
6619 display_name,
6620 selection:
6621 AttachmentSelectionRange {
6622 start: AttachmentSelectionPosition { line: 1, character: 2 },
6623 end: AttachmentSelectionPosition { line: 3, character: 4 },
6624 },
6625 ..
6626 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
6627 ));
6628 assert!(matches!(
6629 &attachments[3],
6630 Attachment::Blob {
6631 data,
6632 mime_type,
6633 display_name,
6634 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
6635 ));
6636 assert!(matches!(
6637 &attachments[4],
6638 Attachment::GitHubReference {
6639 number: 42,
6640 title,
6641 reference_type: GitHubReferenceType::Issue,
6642 state,
6643 url,
6644 } if title == "Fix rendering"
6645 && state == "open"
6646 && url == "https://github.com/example/repo/issues/42"
6647 ));
6648 }
6649
6650 #[test]
6651 fn ensures_display_names_for_variants_that_support_them() {
6652 let mut attachments = vec![
6653 Attachment::File {
6654 path: PathBuf::from("/tmp/file.rs"),
6655 display_name: None,
6656 line_range: None,
6657 },
6658 Attachment::Selection {
6659 file_path: PathBuf::from("/tmp/src/lib.rs"),
6660 display_name: None,
6661 text: "fn main() {}".to_string(),
6662 selection: AttachmentSelectionRange {
6663 start: AttachmentSelectionPosition {
6664 line: 0,
6665 character: 0,
6666 },
6667 end: AttachmentSelectionPosition {
6668 line: 0,
6669 character: 10,
6670 },
6671 },
6672 },
6673 Attachment::Blob {
6674 data: "Zm9v".to_string(),
6675 mime_type: "image/png".to_string(),
6676 display_name: None,
6677 },
6678 Attachment::GitHubReference {
6679 number: 7,
6680 title: "Track regressions".to_string(),
6681 reference_type: GitHubReferenceType::Issue,
6682 state: "open".to_string(),
6683 url: "https://example.com/issues/7".to_string(),
6684 },
6685 ];
6686
6687 ensure_attachment_display_names(&mut attachments);
6688
6689 assert_eq!(attachments[0].display_name(), Some("file.rs"));
6690 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
6691 assert_eq!(attachments[2].display_name(), Some("attachment"));
6692 assert_eq!(attachments[3].display_name(), None);
6693 assert_eq!(
6694 attachments[3].label(),
6695 Some("Track regressions".to_string())
6696 );
6697 }
6698
6699 #[test]
6700 fn github_anchored_attachment_variants_round_trip() {
6701 let cases = vec![
6702 (
6703 "github_commit",
6704 json!({
6705 "type": "github_commit",
6706 "message": "Fix the thing",
6707 "oid": "abc123",
6708 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
6709 "url": "https://github.com/octocat/repo/commit/abc123"
6710 }),
6711 ),
6712 (
6713 "github_release",
6714 json!({
6715 "type": "github_release",
6716 "name": "v1.2.3",
6717 "repo": { "name": "repo", "owner": "octocat" },
6718 "tagName": "v1.2.3",
6719 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
6720 }),
6721 ),
6722 (
6723 "github_actions_job",
6724 json!({
6725 "type": "github_actions_job",
6726 "conclusion": "failure",
6727 "jobId": 99,
6728 "jobName": "build",
6729 "repo": { "name": "repo", "owner": "octocat" },
6730 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
6731 "workflowName": "CI"
6732 }),
6733 ),
6734 (
6735 "github_repository",
6736 json!({
6737 "type": "github_repository",
6738 "description": "An example repository",
6739 "ref": "main",
6740 "repo": { "name": "repo", "owner": "octocat" },
6741 "url": "https://github.com/octocat/repo"
6742 }),
6743 ),
6744 (
6745 "github_file_diff",
6746 json!({
6747 "type": "github_file_diff",
6748 "base": {
6749 "path": "src/lib.rs",
6750 "ref": "main",
6751 "repo": { "name": "repo", "owner": "octocat" }
6752 },
6753 "head": {
6754 "path": "src/lib.rs",
6755 "ref": "feature",
6756 "repo": { "name": "repo", "owner": "octocat" }
6757 },
6758 "url": "https://github.com/octocat/repo/compare/main...feature"
6759 }),
6760 ),
6761 (
6762 "github_tree_comparison",
6763 json!({
6764 "type": "github_tree_comparison",
6765 "base": {
6766 "repo": { "name": "repo", "owner": "octocat" },
6767 "revision": "main"
6768 },
6769 "head": {
6770 "repo": { "name": "repo", "owner": "octocat" },
6771 "revision": "feature"
6772 },
6773 "url": "https://github.com/octocat/repo/compare/main...feature"
6774 }),
6775 ),
6776 (
6777 "github_url",
6778 json!({
6779 "type": "github_url",
6780 "url": "https://github.com/octocat/repo/wiki"
6781 }),
6782 ),
6783 (
6784 "github_file",
6785 json!({
6786 "type": "github_file",
6787 "path": "src/main.rs",
6788 "ref": "main",
6789 "repo": { "name": "repo", "owner": "octocat" },
6790 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
6791 }),
6792 ),
6793 (
6794 "github_snippet",
6795 json!({
6796 "type": "github_snippet",
6797 "lineRange": { "start": 10, "end": 20 },
6798 "path": "src/main.rs",
6799 "ref": "main",
6800 "repo": { "name": "repo", "owner": "octocat" },
6801 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
6802 }),
6803 ),
6804 ];
6805
6806 for (expected_type, input) in cases {
6807 let attachment: Attachment = serde_json::from_value(input.clone())
6808 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
6809
6810 let serialized_string = serde_json::to_string(&attachment)
6815 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
6816
6817 assert_eq!(
6819 serialized_string.matches("\"type\":").count(),
6820 1,
6821 "{expected_type} must serialize a single `type` key"
6822 );
6823
6824 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
6825 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
6826 assert_eq!(
6827 serialized.get("type").and_then(|value| value.as_str()),
6828 Some(expected_type),
6829 "{expected_type} must serialize the correct discriminator"
6830 );
6831
6832 assert_eq!(
6834 serialized, input,
6835 "{expected_type} should round-trip without data loss"
6836 );
6837 let reparsed: Attachment = serde_json::from_value(serialized)
6838 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
6839 assert_eq!(
6840 reparsed, attachment,
6841 "{expected_type} should re-deserialize to the same value"
6842 );
6843 }
6844 }
6845}
6846
6847#[cfg(test)]
6848mod permission_builder_tests {
6849 use std::sync::Arc;
6850
6851 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
6852 use crate::permission;
6853 use crate::types::{
6854 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
6855 SessionId,
6856 };
6857
6858 fn data() -> PermissionRequestData {
6859 PermissionRequestData {
6860 extra: serde_json::json!({"tool": "shell"}),
6861 ..Default::default()
6862 }
6863 }
6864
6865 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6868 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6869 }
6870
6871 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6872 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6873 }
6874
6875 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
6876 handler
6877 .handle(SessionId::from("s1"), RequestId::new("1"), data())
6878 .await
6879 }
6880
6881 #[tokio::test]
6882 async fn approve_all_with_handler_present_approves() {
6883 let cfg = SessionConfig::default()
6884 .with_permission_handler(Arc::new(ApproveAllHandler))
6885 .approve_all_permissions();
6886 let h = resolve_create(cfg).expect("policy + handler yields handler");
6887 assert!(matches!(
6888 dispatch(&h).await,
6889 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6890 ));
6891 }
6892
6893 #[tokio::test]
6894 async fn approve_all_standalone_produces_handler() {
6895 let cfg = SessionConfig::default().approve_all_permissions();
6896 let h = resolve_create(cfg).expect("policy alone yields handler");
6897 assert!(matches!(
6898 dispatch(&h).await,
6899 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6900 ));
6901 }
6902
6903 #[tokio::test]
6906 async fn approve_all_is_order_independent() {
6907 let a = SessionConfig::default()
6908 .with_permission_handler(Arc::new(ApproveAllHandler))
6909 .approve_all_permissions();
6910 let b = SessionConfig::default()
6911 .approve_all_permissions()
6912 .with_permission_handler(Arc::new(ApproveAllHandler));
6913 let ha = resolve_create(a).unwrap();
6914 let hb = resolve_create(b).unwrap();
6915 assert!(matches!(
6916 dispatch(&ha).await,
6917 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6918 ));
6919 assert!(matches!(
6920 dispatch(&hb).await,
6921 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6922 ));
6923 }
6924
6925 #[tokio::test]
6926 async fn deny_all_is_order_independent() {
6927 let a = SessionConfig::default()
6928 .with_permission_handler(Arc::new(ApproveAllHandler))
6929 .deny_all_permissions();
6930 let b = SessionConfig::default()
6931 .deny_all_permissions()
6932 .with_permission_handler(Arc::new(ApproveAllHandler));
6933 let ha = resolve_create(a).unwrap();
6934 let hb = resolve_create(b).unwrap();
6935 assert!(matches!(
6936 dispatch(&ha).await,
6937 PermissionResult::Decision(PermissionDecision::Reject(_))
6938 ));
6939 assert!(matches!(
6940 dispatch(&hb).await,
6941 PermissionResult::Decision(PermissionDecision::Reject(_))
6942 ));
6943 }
6944
6945 #[tokio::test]
6946 async fn approve_permissions_if_consults_predicate() {
6947 let cfg = SessionConfig::default().approve_permissions_if(|d| {
6948 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6949 });
6950 let h = resolve_create(cfg).unwrap();
6951 assert!(matches!(
6952 dispatch(&h).await,
6953 PermissionResult::Decision(PermissionDecision::Reject(_))
6954 ));
6955 }
6956
6957 #[tokio::test]
6958 async fn approve_permissions_if_is_order_independent() {
6959 let predicate = |d: &PermissionRequestData| {
6960 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6961 };
6962 let a = SessionConfig::default()
6963 .with_permission_handler(Arc::new(ApproveAllHandler))
6964 .approve_permissions_if(predicate);
6965 let b = SessionConfig::default()
6966 .approve_permissions_if(predicate)
6967 .with_permission_handler(Arc::new(ApproveAllHandler));
6968 let ha = resolve_create(a).unwrap();
6969 let hb = resolve_create(b).unwrap();
6970 assert!(matches!(
6971 dispatch(&ha).await,
6972 PermissionResult::Decision(PermissionDecision::Reject(_))
6973 ));
6974 assert!(matches!(
6975 dispatch(&hb).await,
6976 PermissionResult::Decision(PermissionDecision::Reject(_))
6977 ));
6978 }
6979
6980 #[tokio::test]
6981 async fn resume_session_config_approve_all_works() {
6982 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
6983 .with_permission_handler(Arc::new(ApproveAllHandler))
6984 .approve_all_permissions();
6985 let h = resolve_resume(cfg).unwrap();
6986 assert!(matches!(
6987 dispatch(&h).await,
6988 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6989 ));
6990 }
6991
6992 #[tokio::test]
6993 async fn resume_session_config_approve_all_is_order_independent() {
6994 let a = ResumeSessionConfig::new(SessionId::from("s1"))
6995 .with_permission_handler(Arc::new(ApproveAllHandler))
6996 .approve_all_permissions();
6997 let b = ResumeSessionConfig::new(SessionId::from("s1"))
6998 .approve_all_permissions()
6999 .with_permission_handler(Arc::new(ApproveAllHandler));
7000 let ha = resolve_resume(a).unwrap();
7001 let hb = resolve_resume(b).unwrap();
7002 assert!(matches!(
7003 dispatch(&ha).await,
7004 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7005 ));
7006 assert!(matches!(
7007 dispatch(&hb).await,
7008 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
7009 ));
7010 }
7011}