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::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)]
769#[serde(rename_all = "camelCase")]
770#[non_exhaustive]
771pub struct InfiniteSessionConfig {
772 #[serde(default, skip_serializing_if = "Option::is_none")]
774 pub enabled: Option<bool>,
775 #[serde(default, skip_serializing_if = "Option::is_none")]
778 pub background_compaction_threshold: Option<f64>,
779 #[serde(default, skip_serializing_if = "Option::is_none")]
782 pub buffer_exhaustion_threshold: Option<f64>,
783}
784
785impl InfiniteSessionConfig {
786 pub fn new() -> Self {
789 Self::default()
790 }
791
792 pub fn with_enabled(mut self, enabled: bool) -> Self {
795 self.enabled = Some(enabled);
796 self
797 }
798
799 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
802 self.background_compaction_threshold = Some(threshold);
803 self
804 }
805
806 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
809 self.buffer_exhaustion_threshold = Some(threshold);
810 self
811 }
812}
813
814#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
825#[serde(rename_all = "camelCase")]
826#[non_exhaustive]
827pub struct MemoryConfiguration {
828 pub enabled: bool,
830}
831
832impl MemoryConfiguration {
833 pub fn enabled() -> Self {
835 Self { enabled: true }
836 }
837
838 pub fn disabled() -> Self {
840 Self { enabled: false }
841 }
842
843 pub fn with_enabled(mut self, enabled: bool) -> Self {
845 self.enabled = enabled;
846 self
847 }
848}
849
850#[derive(Debug, Clone, Serialize, Deserialize)]
852#[serde(rename_all = "camelCase")]
853#[non_exhaustive]
854pub struct CloudSessionRepository {
855 pub owner: String,
857 pub name: String,
859 #[serde(skip_serializing_if = "Option::is_none")]
861 pub branch: Option<String>,
862}
863
864impl CloudSessionRepository {
865 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
867 Self {
868 owner: owner.into(),
869 name: name.into(),
870 branch: None,
871 }
872 }
873
874 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
876 self.branch = Some(branch.into());
877 self
878 }
879}
880
881#[derive(Debug, Clone, Default, Serialize, Deserialize)]
883#[serde(rename_all = "camelCase")]
884#[non_exhaustive]
885pub struct CloudSessionOptions {
886 #[serde(skip_serializing_if = "Option::is_none")]
888 pub repository: Option<CloudSessionRepository>,
889}
890
891impl CloudSessionOptions {
892 pub fn with_repository(repository: CloudSessionRepository) -> Self {
894 Self {
895 repository: Some(repository),
896 }
897 }
898}
899
900#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
902#[serde(rename_all = "camelCase")]
903pub struct ExtensionInfo {
904 pub source: String,
906 pub name: String,
908}
909
910impl ExtensionInfo {
911 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
913 Self {
914 source: source.into(),
915 name: name.into(),
916 }
917 }
918}
919
920#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
931#[serde(rename_all = "camelCase")]
932pub struct CanvasProviderIdentity {
933 pub id: String,
935 #[serde(skip_serializing_if = "Option::is_none")]
937 pub name: Option<String>,
938}
939
940impl CanvasProviderIdentity {
941 pub fn new(id: impl Into<String>) -> Self {
943 Self {
944 id: id.into(),
945 name: None,
946 }
947 }
948
949 pub fn with_name(mut self, name: impl Into<String>) -> Self {
951 self.name = Some(name.into());
952 self
953 }
954}
955
956#[derive(Debug, Clone, Serialize, Deserialize)]
990#[serde(tag = "type", rename_all = "lowercase")]
991#[non_exhaustive]
992pub enum McpServerConfig {
993 #[serde(alias = "local")]
997 Stdio(McpStdioServerConfig),
998 Http(McpHttpServerConfig),
1000 Sse(McpHttpServerConfig),
1002}
1003
1004#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1008#[serde(rename_all = "camelCase")]
1009pub struct McpStdioServerConfig {
1010 #[serde(default, skip_serializing_if = "Option::is_none")]
1016 pub tools: Option<Vec<String>>,
1017 #[serde(default, skip_serializing_if = "Option::is_none")]
1019 pub timeout: Option<i64>,
1020 pub command: String,
1022 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1024 pub args: Vec<String>,
1025 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1028 pub env: HashMap<String, String>,
1029 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
1031 pub working_directory: Option<String>,
1032}
1033
1034#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1038#[serde(rename_all = "camelCase")]
1039pub struct McpHttpServerConfig {
1040 #[serde(default, skip_serializing_if = "Option::is_none")]
1046 pub tools: Option<Vec<String>>,
1047 #[serde(default, skip_serializing_if = "Option::is_none")]
1049 pub timeout: Option<i64>,
1050 pub url: String,
1052 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1054 pub headers: HashMap<String, String>,
1055}
1056
1057#[derive(Clone, Default, Serialize, Deserialize)]
1063#[serde(rename_all = "camelCase")]
1064#[non_exhaustive]
1065pub struct ProviderConfig {
1066 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1069 pub provider_type: Option<String>,
1070 #[serde(default, skip_serializing_if = "Option::is_none")]
1073 pub wire_api: Option<String>,
1074 #[serde(default, skip_serializing_if = "Option::is_none")]
1079 pub transport: Option<String>,
1080 pub base_url: String,
1082 #[serde(default, skip_serializing_if = "Option::is_none")]
1084 pub api_key: Option<String>,
1085 #[serde(default, skip_serializing_if = "Option::is_none")]
1089 pub bearer_token: Option<String>,
1090 #[serde(skip)]
1093 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1094 #[serde(default, skip_serializing_if = "Option::is_none")]
1095 pub(crate) has_bearer_token_provider: Option<bool>,
1096 #[serde(default, skip_serializing_if = "Option::is_none")]
1098 pub azure: Option<AzureProviderOptions>,
1099 #[serde(default, skip_serializing_if = "Option::is_none")]
1101 pub headers: Option<HashMap<String, String>>,
1102 #[serde(default, skip_serializing_if = "Option::is_none")]
1106 pub model_id: Option<String>,
1107 #[serde(default, skip_serializing_if = "Option::is_none")]
1114 pub wire_model: Option<String>,
1115 #[serde(default, skip_serializing_if = "Option::is_none")]
1120 pub max_prompt_tokens: Option<i64>,
1121 #[serde(default, skip_serializing_if = "Option::is_none")]
1124 pub max_output_tokens: Option<i64>,
1125}
1126
1127impl std::fmt::Debug for ProviderConfig {
1128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1129 f.debug_struct("ProviderConfig")
1130 .field("provider_type", &self.provider_type)
1131 .field("wire_api", &self.wire_api)
1132 .field("transport", &self.transport)
1133 .field("base_url", &self.base_url)
1134 .field("api_key", &self.api_key)
1135 .field("bearer_token", &self.bearer_token)
1136 .field(
1137 "bearer_token_provider",
1138 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1139 )
1140 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1141 .field("azure", &self.azure)
1142 .field("headers", &self.headers)
1143 .field("model_id", &self.model_id)
1144 .field("wire_model", &self.wire_model)
1145 .field("max_prompt_tokens", &self.max_prompt_tokens)
1146 .field("max_output_tokens", &self.max_output_tokens)
1147 .finish()
1148 }
1149}
1150
1151impl ProviderConfig {
1152 pub fn new(base_url: impl Into<String>) -> Self {
1155 Self {
1156 base_url: base_url.into(),
1157 ..Self::default()
1158 }
1159 }
1160
1161 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1163 self.provider_type = Some(provider_type.into());
1164 self
1165 }
1166
1167 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1169 self.wire_api = Some(wire_api.into());
1170 self
1171 }
1172
1173 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1176 self.transport = Some(transport.into());
1177 self
1178 }
1179
1180 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1182 self.api_key = Some(api_key.into());
1183 self
1184 }
1185
1186 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1189 self.bearer_token = Some(bearer_token.into());
1190 self
1191 }
1192
1193 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1199 self.bearer_token_provider = Some(provider);
1200 self
1201 }
1202
1203 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1205 self.azure = Some(azure);
1206 self
1207 }
1208
1209 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1211 self.headers = Some(headers);
1212 self
1213 }
1214
1215 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1218 self.model_id = Some(model_id.into());
1219 self
1220 }
1221
1222 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1227 self.wire_model = Some(wire_model.into());
1228 self
1229 }
1230
1231 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1235 self.max_prompt_tokens = Some(max);
1236 self
1237 }
1238
1239 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1242 self.max_output_tokens = Some(max);
1243 self
1244 }
1245}
1246
1247#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1260#[serde(rename_all = "camelCase")]
1261#[non_exhaustive]
1262pub struct CapiSessionOptions {
1263 #[serde(default, skip_serializing_if = "Option::is_none")]
1269 pub enable_web_socket_responses: Option<bool>,
1270}
1271
1272impl CapiSessionOptions {
1273 pub fn new() -> Self {
1275 Self::default()
1276 }
1277
1278 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1280 self.enable_web_socket_responses = Some(enable);
1281 self
1282 }
1283}
1284
1285#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1287#[serde(rename_all = "camelCase")]
1288pub struct AzureProviderOptions {
1289 #[serde(default, skip_serializing_if = "Option::is_none")]
1291 pub api_version: Option<String>,
1292}
1293
1294#[derive(Clone, Default, Serialize, Deserialize)]
1305#[serde(rename_all = "camelCase")]
1306#[non_exhaustive]
1307pub struct NamedProviderConfig {
1308 pub name: String,
1311 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1314 pub provider_type: Option<String>,
1315 #[serde(default, skip_serializing_if = "Option::is_none")]
1318 pub wire_api: Option<String>,
1319 pub base_url: String,
1321 #[serde(default, skip_serializing_if = "Option::is_none")]
1323 pub api_key: Option<String>,
1324 #[serde(default, skip_serializing_if = "Option::is_none")]
1327 pub bearer_token: Option<String>,
1328 #[serde(skip)]
1331 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1332 #[serde(default, skip_serializing_if = "Option::is_none")]
1333 pub(crate) has_bearer_token_provider: Option<bool>,
1334 #[serde(default, skip_serializing_if = "Option::is_none")]
1336 pub azure: Option<AzureProviderOptions>,
1337 #[serde(default, skip_serializing_if = "Option::is_none")]
1339 pub headers: Option<HashMap<String, String>>,
1340}
1341
1342impl std::fmt::Debug for NamedProviderConfig {
1343 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1344 f.debug_struct("NamedProviderConfig")
1345 .field("name", &self.name)
1346 .field("provider_type", &self.provider_type)
1347 .field("wire_api", &self.wire_api)
1348 .field("base_url", &self.base_url)
1349 .field("api_key", &self.api_key)
1350 .field("bearer_token", &self.bearer_token)
1351 .field(
1352 "bearer_token_provider",
1353 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1354 )
1355 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1356 .field("azure", &self.azure)
1357 .field("headers", &self.headers)
1358 .finish()
1359 }
1360}
1361
1362impl NamedProviderConfig {
1363 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1366 Self {
1367 name: name.into(),
1368 base_url: base_url.into(),
1369 ..Self::default()
1370 }
1371 }
1372
1373 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1375 self.provider_type = Some(provider_type.into());
1376 self
1377 }
1378
1379 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1381 self.wire_api = Some(wire_api.into());
1382 self
1383 }
1384
1385 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1387 self.api_key = Some(api_key.into());
1388 self
1389 }
1390
1391 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1394 self.bearer_token = Some(bearer_token.into());
1395 self
1396 }
1397
1398 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1404 self.bearer_token_provider = Some(provider);
1405 self
1406 }
1407
1408 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1410 self.azure = Some(azure);
1411 self
1412 }
1413
1414 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1416 self.headers = Some(headers);
1417 self
1418 }
1419}
1420
1421fn prepare_bearer_token_providers(
1422 provider: &mut Option<ProviderConfig>,
1423 providers: &mut Option<Vec<NamedProviderConfig>>,
1424) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1425 let mut bearer_token_providers = HashMap::new();
1426
1427 if let Some(provider) = provider.as_mut()
1428 && let Some(token_provider) = provider.bearer_token_provider.take()
1429 {
1430 provider.has_bearer_token_provider = Some(true);
1431 bearer_token_providers.insert("default".to_string(), token_provider);
1432 }
1433
1434 if let Some(providers) = providers.as_mut() {
1435 for provider in providers {
1436 if let Some(token_provider) = provider.bearer_token_provider.take() {
1437 provider.has_bearer_token_provider = Some(true);
1438 bearer_token_providers.insert(provider.name.clone(), token_provider);
1439 }
1440 }
1441 }
1442
1443 bearer_token_providers
1444}
1445
1446#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1454#[serde(rename_all = "camelCase")]
1455#[non_exhaustive]
1456pub struct ProviderModelConfig {
1457 pub id: String,
1460 pub provider: String,
1462 #[serde(default, skip_serializing_if = "Option::is_none")]
1465 pub wire_model: Option<String>,
1466 #[serde(default, skip_serializing_if = "Option::is_none")]
1469 pub model_id: Option<String>,
1470 #[serde(default, skip_serializing_if = "Option::is_none")]
1472 pub name: Option<String>,
1473 #[serde(default, skip_serializing_if = "Option::is_none")]
1475 pub max_prompt_tokens: Option<i64>,
1476 #[serde(default, skip_serializing_if = "Option::is_none")]
1478 pub max_context_window_tokens: Option<i64>,
1479 #[serde(default, skip_serializing_if = "Option::is_none")]
1481 pub max_output_tokens: Option<i64>,
1482 #[serde(default, skip_serializing_if = "Option::is_none")]
1485 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1486}
1487
1488impl ProviderModelConfig {
1489 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1492 Self {
1493 id: id.into(),
1494 provider: provider.into(),
1495 ..Self::default()
1496 }
1497 }
1498
1499 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1501 self.wire_model = Some(wire_model.into());
1502 self
1503 }
1504
1505 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1508 self.model_id = Some(model_id.into());
1509 self
1510 }
1511
1512 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1514 self.name = Some(name.into());
1515 self
1516 }
1517
1518 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1520 self.max_prompt_tokens = Some(max);
1521 self
1522 }
1523
1524 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1526 self.max_context_window_tokens = Some(max);
1527 self
1528 }
1529
1530 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1532 self.max_output_tokens = Some(max);
1533 self
1534 }
1535
1536 pub fn with_capabilities(
1538 mut self,
1539 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1540 ) -> Self {
1541 self.capabilities = Some(capabilities);
1542 self
1543 }
1544}
1545
1546#[derive(Clone)]
1598#[non_exhaustive]
1599pub struct SessionConfig {
1600 pub session_id: Option<SessionId>,
1602 pub model: Option<String>,
1604 pub client_name: Option<String>,
1606 pub reasoning_effort: Option<String>,
1608 pub reasoning_summary: Option<ReasoningSummary>,
1612 pub context_tier: Option<String>,
1615 pub streaming: Option<bool>,
1617 pub system_message: Option<SystemMessageConfig>,
1619 pub tools: Option<Vec<Tool>>,
1621 pub canvases: Option<Vec<CanvasDeclaration>>,
1623 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1628 pub request_canvas_renderer: Option<bool>,
1630 pub request_extensions: Option<bool>,
1632 pub extension_sdk_path: Option<String>,
1636 pub extension_info: Option<ExtensionInfo>,
1638 pub canvas_provider: Option<CanvasProviderIdentity>,
1641 pub available_tools: Option<Vec<String>>,
1643 pub excluded_tools: Option<Vec<String>>,
1645 pub excluded_builtin_agents: Option<Vec<String>>,
1651 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
1653 pub mcp_oauth_token_storage: Option<String>,
1662 pub enable_config_discovery: Option<bool>,
1664 pub skip_embedding_retrieval: Option<bool>,
1666 pub embedding_cache_storage: Option<String>,
1669 pub organization_custom_instructions: Option<String>,
1671 pub enable_on_demand_instruction_discovery: Option<bool>,
1673 pub enable_file_hooks: Option<bool>,
1675 pub enable_host_git_operations: Option<bool>,
1677 pub enable_session_store: Option<bool>,
1679 pub enable_skills: Option<bool>,
1681 pub enable_mcp_apps: Option<bool>,
1708 pub skill_directories: Option<Vec<PathBuf>>,
1710 pub instruction_directories: Option<Vec<PathBuf>>,
1713 pub plugin_directories: Option<Vec<PathBuf>>,
1715 pub large_output: Option<LargeToolOutputConfig>,
1717 pub disabled_skills: Option<Vec<String>>,
1720 pub hooks: Option<bool>,
1724 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1726 pub default_agent: Option<DefaultAgentConfig>,
1730 pub agent: Option<String>,
1733 pub infinite_sessions: Option<InfiniteSessionConfig>,
1736 pub provider: Option<ProviderConfig>,
1740 pub capi: Option<CapiSessionOptions>,
1746 pub providers: Option<Vec<NamedProviderConfig>>,
1753 pub models: Option<Vec<ProviderModelConfig>>,
1759 pub enable_session_telemetry: Option<bool>,
1767 pub enable_citations: Option<bool>,
1769 pub session_limits: Option<SessionLimitsConfig>,
1771 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1774 pub memory: Option<MemoryConfiguration>,
1776 pub config_directory: Option<PathBuf>,
1779 pub working_directory: Option<PathBuf>,
1782 pub github_token: Option<String>,
1788 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
1794 pub cloud: Option<CloudSessionOptions>,
1797 pub include_sub_agent_streaming_events: Option<bool>,
1801 pub commands: Option<Vec<CommandDefinition>>,
1805 #[doc(hidden)]
1812 pub exp_assignments: Option<Value>,
1813 pub enable_managed_settings: Option<bool>,
1820 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1825 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
1829 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
1832 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
1835 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
1839 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
1842 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
1845 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1849 pub(crate) permission_policy: Option<crate::permission::Policy>,
1853 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
1858 pub skip_custom_instructions: Option<bool>,
1862 pub custom_agents_local_only: Option<bool>,
1866 pub coauthor_enabled: Option<bool>,
1870 pub manage_schedule_enabled: Option<bool>,
1874}
1875
1876impl std::fmt::Debug for SessionConfig {
1877 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1878 f.debug_struct("SessionConfig")
1879 .field("session_id", &self.session_id)
1880 .field("model", &self.model)
1881 .field("client_name", &self.client_name)
1882 .field("reasoning_effort", &self.reasoning_effort)
1883 .field("reasoning_summary", &self.reasoning_summary)
1884 .field("context_tier", &self.context_tier)
1885 .field("streaming", &self.streaming)
1886 .field("system_message", &self.system_message)
1887 .field("tools", &self.tools)
1888 .field("canvases", &self.canvases)
1889 .field(
1890 "canvas_handler",
1891 &self.canvas_handler.as_ref().map(|_| "<set>"),
1892 )
1893 .field("request_canvas_renderer", &self.request_canvas_renderer)
1894 .field("request_extensions", &self.request_extensions)
1895 .field("extension_sdk_path", &self.extension_sdk_path)
1896 .field("extension_info", &self.extension_info)
1897 .field("canvas_provider", &self.canvas_provider)
1898 .field("available_tools", &self.available_tools)
1899 .field("excluded_tools", &self.excluded_tools)
1900 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
1901 .field("mcp_servers", &self.mcp_servers)
1902 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
1903 .field("embedding_cache_storage", &self.embedding_cache_storage)
1904 .field("enable_config_discovery", &self.enable_config_discovery)
1905 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
1906 .field(
1907 "organization_custom_instructions",
1908 &self
1909 .organization_custom_instructions
1910 .as_ref()
1911 .map(|_| "<redacted>"),
1912 )
1913 .field(
1914 "enable_on_demand_instruction_discovery",
1915 &self.enable_on_demand_instruction_discovery,
1916 )
1917 .field("enable_file_hooks", &self.enable_file_hooks)
1918 .field(
1919 "enable_host_git_operations",
1920 &self.enable_host_git_operations,
1921 )
1922 .field("enable_session_store", &self.enable_session_store)
1923 .field("enable_skills", &self.enable_skills)
1924 .field("enable_mcp_apps", &self.enable_mcp_apps)
1925 .field("skill_directories", &self.skill_directories)
1926 .field("instruction_directories", &self.instruction_directories)
1927 .field("plugin_directories", &self.plugin_directories)
1928 .field("large_output", &self.large_output)
1929 .field("disabled_skills", &self.disabled_skills)
1930 .field("hooks", &self.hooks)
1931 .field("custom_agents", &self.custom_agents)
1932 .field("default_agent", &self.default_agent)
1933 .field("agent", &self.agent)
1934 .field("infinite_sessions", &self.infinite_sessions)
1935 .field("provider", &self.provider)
1936 .field("capi", &self.capi)
1937 .field("enable_session_telemetry", &self.enable_session_telemetry)
1938 .field("enable_citations", &self.enable_citations)
1939 .field("session_limits", &self.session_limits)
1940 .field("model_capabilities", &self.model_capabilities)
1941 .field("memory", &self.memory)
1942 .field("config_directory", &self.config_directory)
1943 .field("working_directory", &self.working_directory)
1944 .field(
1945 "github_token",
1946 &self.github_token.as_ref().map(|_| "<redacted>"),
1947 )
1948 .field("remote_session", &self.remote_session)
1949 .field("cloud", &self.cloud)
1950 .field(
1951 "include_sub_agent_streaming_events",
1952 &self.include_sub_agent_streaming_events,
1953 )
1954 .field("commands", &self.commands)
1955 .field("exp_assignments", &self.exp_assignments)
1956 .field("enable_managed_settings", &self.enable_managed_settings)
1957 .field(
1958 "session_fs_provider",
1959 &self.session_fs_provider.as_ref().map(|_| "<set>"),
1960 )
1961 .field(
1962 "permission_handler",
1963 &self.permission_handler.as_ref().map(|_| "<set>"),
1964 )
1965 .field(
1966 "elicitation_handler",
1967 &self.elicitation_handler.as_ref().map(|_| "<set>"),
1968 )
1969 .field(
1970 "mcp_auth_handler",
1971 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
1972 )
1973 .field(
1974 "user_input_handler",
1975 &self.user_input_handler.as_ref().map(|_| "<set>"),
1976 )
1977 .field(
1978 "exit_plan_mode_handler",
1979 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
1980 )
1981 .field(
1982 "auto_mode_switch_handler",
1983 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
1984 )
1985 .field(
1986 "hooks_handler",
1987 &self.hooks_handler.as_ref().map(|_| "<set>"),
1988 )
1989 .field(
1990 "system_message_transform",
1991 &self.system_message_transform.as_ref().map(|_| "<set>"),
1992 )
1993 .finish()
1994 }
1995}
1996
1997impl Default for SessionConfig {
1998 fn default() -> Self {
2004 Self {
2005 session_id: None,
2006 model: None,
2007 client_name: None,
2008 reasoning_effort: None,
2009 reasoning_summary: None,
2010 context_tier: None,
2011 streaming: None,
2012 system_message: None,
2013 tools: None,
2014 canvases: None,
2015 canvas_handler: None,
2016 request_canvas_renderer: None,
2017 request_extensions: None,
2018 extension_sdk_path: None,
2019 extension_info: None,
2020 canvas_provider: None,
2021 available_tools: None,
2022 excluded_tools: None,
2023 excluded_builtin_agents: None,
2024 mcp_servers: None,
2025 mcp_oauth_token_storage: None,
2026 enable_config_discovery: None,
2027 skip_embedding_retrieval: None,
2028 organization_custom_instructions: None,
2029 enable_on_demand_instruction_discovery: None,
2030 enable_file_hooks: None,
2031 enable_host_git_operations: None,
2032 enable_session_store: None,
2033 enable_skills: None,
2034 embedding_cache_storage: None,
2035 enable_mcp_apps: None,
2036 skill_directories: None,
2037 instruction_directories: None,
2038 plugin_directories: None,
2039 large_output: None,
2040 disabled_skills: None,
2041 hooks: None,
2042 custom_agents: None,
2043 default_agent: None,
2044 agent: None,
2045 infinite_sessions: None,
2046 provider: None,
2047 capi: None,
2048 providers: None,
2049 models: None,
2050 enable_session_telemetry: None,
2051 enable_citations: None,
2052 session_limits: None,
2053 model_capabilities: None,
2054 memory: None,
2055 config_directory: None,
2056 working_directory: None,
2057 github_token: None,
2058 remote_session: None,
2059 cloud: None,
2060 include_sub_agent_streaming_events: None,
2061 commands: None,
2062 exp_assignments: None,
2063 enable_managed_settings: None,
2064 session_fs_provider: None,
2065 permission_handler: None,
2066 elicitation_handler: None,
2067 mcp_auth_handler: None,
2068 user_input_handler: None,
2069 exit_plan_mode_handler: None,
2070 auto_mode_switch_handler: None,
2071 hooks_handler: None,
2072 permission_policy: None,
2073 system_message_transform: None,
2074 skip_custom_instructions: None,
2075 custom_agents_local_only: None,
2076 coauthor_enabled: None,
2077 manage_schedule_enabled: None,
2078 }
2079 }
2080}
2081
2082pub(crate) struct SessionConfigRuntime {
2088 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2089 pub permission_policy: Option<crate::permission::Policy>,
2090 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2091 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2092 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2093 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2094 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2095 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2096 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2097 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2098 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2099 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2100 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2101 pub commands: Option<Vec<CommandDefinition>>,
2102}
2103
2104impl SessionConfig {
2105 pub(crate) fn into_wire(
2117 mut self,
2118 session_id: Option<SessionId>,
2119 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2120 let permission_active =
2121 self.permission_handler.is_some() || self.permission_policy.is_some();
2122 let request_user_input = self.user_input_handler.is_some();
2123 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2124 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2125 let request_elicitation = self.elicitation_handler.is_some();
2126 let hooks_flag = self.hooks_handler.is_some();
2127
2128 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2129 if let Some(tools) = self.tools.as_mut() {
2130 for tool in tools.iter_mut() {
2131 if let Some(handler) = tool.handler.take()
2132 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2133 {
2134 return Err(crate::Error::with_message(
2135 crate::ErrorKind::InvalidConfig,
2136 format!("duplicate tool handler registered for name {:?}", tool.name),
2137 ));
2138 }
2139 }
2140 }
2141
2142 let wire_commands = self.commands.as_ref().map(|cmds| {
2143 cmds.iter()
2144 .map(|c| crate::wire::CommandWireDefinition {
2145 name: c.name.clone(),
2146 description: c.description.clone(),
2147 })
2148 .collect()
2149 });
2150 let wire_canvases = self.canvases.clone();
2151 let canvas_handler = self.canvas_handler.clone();
2152 let bearer_token_providers =
2153 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2154
2155 let wire = crate::wire::SessionCreateWire {
2156 session_id,
2157 model: self.model,
2158 client_name: self.client_name,
2159 reasoning_effort: self.reasoning_effort,
2160 reasoning_summary: self.reasoning_summary,
2161 context_tier: self.context_tier,
2162 streaming: self.streaming,
2163 system_message: self.system_message,
2164 tools: self.tools,
2165 canvases: wire_canvases,
2166 request_canvas_renderer: self.request_canvas_renderer,
2167 request_extensions: self.request_extensions,
2168 extension_sdk_path: self.extension_sdk_path,
2169 extension_info: self.extension_info,
2170 canvas_provider: self.canvas_provider,
2171 available_tools: self.available_tools,
2172 excluded_tools: self.excluded_tools,
2173 excluded_builtin_agents: self.excluded_builtin_agents,
2174 tool_filter_precedence: "excluded",
2175 mcp_servers: self.mcp_servers,
2176 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2177 embedding_cache_storage: self.embedding_cache_storage,
2178 env_value_mode: "direct",
2179 enable_config_discovery: self.enable_config_discovery,
2180 skip_embedding_retrieval: self.skip_embedding_retrieval,
2181 organization_custom_instructions: self.organization_custom_instructions,
2182 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2183 enable_file_hooks: self.enable_file_hooks,
2184 enable_host_git_operations: self.enable_host_git_operations,
2185 enable_session_store: self.enable_session_store,
2186 enable_skills: self.enable_skills,
2187 request_user_input,
2188 request_permission: permission_active,
2189 request_exit_plan_mode,
2190 request_auto_mode_switch,
2191 request_elicitation,
2192 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2193 hooks: hooks_flag,
2194 skill_directories: self.skill_directories,
2195 instruction_directories: self.instruction_directories,
2196 plugin_directories: self.plugin_directories,
2197 large_output: self.large_output,
2198 disabled_skills: self.disabled_skills,
2199 custom_agents: self.custom_agents,
2200 default_agent: self.default_agent,
2201 agent: self.agent,
2202 infinite_sessions: self.infinite_sessions,
2203 provider: self.provider,
2204 capi: self.capi,
2205 providers: self.providers,
2206 models: self.models,
2207 enable_session_telemetry: self.enable_session_telemetry,
2208 enable_citations: self.enable_citations,
2209 session_limits: self.session_limits,
2210 model_capabilities: self.model_capabilities,
2211 memory: self.memory,
2212 config_dir: self.config_directory,
2213 working_directory: self.working_directory,
2214 github_token: self.github_token,
2215 remote_session: self.remote_session,
2216 cloud: self.cloud,
2217 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2218 enable_github_telemetry_forwarding: None,
2219 commands: wire_commands,
2220 exp_assignments: self.exp_assignments,
2221 enable_managed_settings: self.enable_managed_settings,
2222 };
2223
2224 let runtime = SessionConfigRuntime {
2225 permission_handler: self.permission_handler,
2226 permission_policy: self.permission_policy,
2227 elicitation_handler: self.elicitation_handler,
2228 mcp_auth_handler: self.mcp_auth_handler,
2229 user_input_handler: self.user_input_handler,
2230 exit_plan_mode_handler: self.exit_plan_mode_handler,
2231 auto_mode_switch_handler: self.auto_mode_switch_handler,
2232 hooks_handler: self.hooks_handler,
2233 system_message_transform: self.system_message_transform,
2234 tool_handlers,
2235 canvas_handler,
2236 session_fs_provider: self.session_fs_provider,
2237 bearer_token_providers,
2238 commands: self.commands,
2239 };
2240
2241 Ok((wire, runtime))
2242 }
2243
2244 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2248 self.permission_handler = Some(handler);
2249 self
2250 }
2251
2252 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2255 self.elicitation_handler = Some(handler);
2256 self
2257 }
2258
2259 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2261 self.mcp_auth_handler = Some(handler);
2262 self
2263 }
2264
2265 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2268 self.user_input_handler = Some(handler);
2269 self
2270 }
2271
2272 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2274 self.exit_plan_mode_handler = Some(handler);
2275 self
2276 }
2277
2278 pub fn with_auto_mode_switch_handler(
2280 mut self,
2281 handler: Arc<dyn AutoModeSwitchHandler>,
2282 ) -> Self {
2283 self.auto_mode_switch_handler = Some(handler);
2284 self
2285 }
2286
2287 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2292 self.commands = Some(commands);
2293 self
2294 }
2295
2296 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2300 self.session_fs_provider = Some(provider);
2301 self
2302 }
2303
2304 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2307 self.hooks_handler = Some(hooks);
2308 self
2309 }
2310
2311 pub fn with_system_message_transform(
2315 mut self,
2316 transform: Arc<dyn SystemMessageTransform>,
2317 ) -> Self {
2318 self.system_message_transform = Some(transform);
2319 self
2320 }
2321
2322 pub fn approve_all_permissions(mut self) -> Self {
2328 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2329 self
2330 }
2331
2332 pub fn deny_all_permissions(mut self) -> Self {
2335 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2336 self
2337 }
2338
2339 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2344 where
2345 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2346 {
2347 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2348 self
2349 }
2350
2351 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2353 self.session_id = Some(id.into());
2354 self
2355 }
2356
2357 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2359 self.model = Some(model.into());
2360 self
2361 }
2362
2363 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2365 self.client_name = Some(name.into());
2366 self
2367 }
2368
2369 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2371 self.reasoning_effort = Some(effort.into());
2372 self
2373 }
2374
2375 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2377 self.reasoning_summary = Some(summary);
2378 self
2379 }
2380
2381 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2383 self.context_tier = Some(tier.into());
2384 self
2385 }
2386
2387 pub fn with_streaming(mut self, streaming: bool) -> Self {
2389 self.streaming = Some(streaming);
2390 self
2391 }
2392
2393 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2395 self.system_message = Some(system_message);
2396 self
2397 }
2398
2399 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2401 self.tools = Some(tools.into_iter().collect());
2402 self
2403 }
2404
2405 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2410 self.canvases = Some(canvases.into_iter().collect());
2411 self
2412 }
2413
2414 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2416 self.canvas_handler = Some(handler);
2417 self
2418 }
2419
2420 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2422 self.request_canvas_renderer = Some(request);
2423 self
2424 }
2425
2426 pub fn with_request_extensions(mut self, request: bool) -> Self {
2428 self.request_extensions = Some(request);
2429 self
2430 }
2431
2432 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2436 self.extension_sdk_path = Some(path.into());
2437 self
2438 }
2439
2440 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2442 self.extension_info = Some(extension_info);
2443 self
2444 }
2445
2446 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
2449 self.canvas_provider = Some(canvas_provider);
2450 self
2451 }
2452
2453 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2455 where
2456 I: IntoIterator<Item = S>,
2457 S: Into<String>,
2458 {
2459 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2460 self
2461 }
2462
2463 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2465 where
2466 I: IntoIterator<Item = S>,
2467 S: Into<String>,
2468 {
2469 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2470 self
2471 }
2472
2473 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2475 where
2476 I: IntoIterator<Item = S>,
2477 S: Into<String>,
2478 {
2479 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2480 self
2481 }
2482
2483 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2485 self.mcp_servers = Some(servers);
2486 self
2487 }
2488
2489 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2497 self.mcp_oauth_token_storage = Some(mode.into());
2498 self
2499 }
2500
2501 pub fn with_embedding_cache_storage(
2503 mut self,
2504 embedding_cache_storage: impl Into<String>,
2505 ) -> Self {
2506 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2507 self
2508 }
2509
2510 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2512 self.enable_config_discovery = Some(enable);
2513 self
2514 }
2515
2516 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2518 self.skip_embedding_retrieval = Some(value);
2519 self
2520 }
2521
2522 pub fn with_organization_custom_instructions(
2524 mut self,
2525 instructions: impl Into<String>,
2526 ) -> Self {
2527 self.organization_custom_instructions = Some(instructions.into());
2528 self
2529 }
2530
2531 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2533 self.enable_on_demand_instruction_discovery = Some(value);
2534 self
2535 }
2536
2537 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2539 self.enable_file_hooks = Some(value);
2540 self
2541 }
2542
2543 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2545 self.enable_host_git_operations = Some(value);
2546 self
2547 }
2548
2549 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2551 self.enable_session_store = Some(value);
2552 self
2553 }
2554
2555 pub fn with_enable_skills(mut self, value: bool) -> Self {
2557 self.enable_skills = Some(value);
2558 self
2559 }
2560
2561 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2567 self.enable_mcp_apps = Some(enable);
2568 self
2569 }
2570
2571 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2573 where
2574 I: IntoIterator<Item = P>,
2575 P: Into<PathBuf>,
2576 {
2577 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2578 self
2579 }
2580
2581 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2585 where
2586 I: IntoIterator<Item = P>,
2587 P: Into<PathBuf>,
2588 {
2589 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2590 self
2591 }
2592
2593 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2595 where
2596 I: IntoIterator<Item = P>,
2597 P: Into<PathBuf>,
2598 {
2599 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2600 self
2601 }
2602
2603 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2605 self.large_output = Some(config);
2606 self
2607 }
2608
2609 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2611 where
2612 I: IntoIterator<Item = S>,
2613 S: Into<String>,
2614 {
2615 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2616 self
2617 }
2618
2619 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2621 mut self,
2622 agents: I,
2623 ) -> Self {
2624 self.custom_agents = Some(agents.into_iter().collect());
2625 self
2626 }
2627
2628 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2630 self.default_agent = Some(agent);
2631 self
2632 }
2633
2634 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2637 self.agent = Some(name.into());
2638 self
2639 }
2640
2641 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2644 self.infinite_sessions = Some(config);
2645 self
2646 }
2647
2648 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2650 self.provider = Some(provider);
2651 self
2652 }
2653
2654 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
2656 self.capi = Some(capi);
2657 self
2658 }
2659
2660 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
2666 self.providers = Some(providers);
2667 self
2668 }
2669
2670 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
2676 self.models = Some(models);
2677 self
2678 }
2679
2680 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2684 self.enable_session_telemetry = Some(enable);
2685 self
2686 }
2687
2688 pub fn with_enable_citations(mut self, enable: bool) -> Self {
2690 self.enable_citations = Some(enable);
2691 self
2692 }
2693
2694 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
2696 self.session_limits = Some(limits);
2697 self
2698 }
2699
2700 pub fn with_model_capabilities(
2702 mut self,
2703 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2704 ) -> Self {
2705 self.model_capabilities = Some(capabilities);
2706 self
2707 }
2708
2709 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
2711 self.memory = Some(memory);
2712 self
2713 }
2714
2715 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2717 self.config_directory = Some(dir.into());
2718 self
2719 }
2720
2721 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2724 self.working_directory = Some(dir.into());
2725 self
2726 }
2727
2728 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2733 self.github_token = Some(token.into());
2734 self
2735 }
2736
2737 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2740 self.include_sub_agent_streaming_events = Some(include);
2741 self
2742 }
2743
2744 pub fn with_remote_session(
2746 mut self,
2747 mode: crate::generated::api_types::RemoteSessionMode,
2748 ) -> Self {
2749 self.remote_session = Some(mode);
2750 self
2751 }
2752
2753 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
2755 self.cloud = Some(cloud);
2756 self
2757 }
2758
2759 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
2761 self.skip_custom_instructions = Some(value);
2762 self
2763 }
2764
2765 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
2767 self.custom_agents_local_only = Some(value);
2768 self
2769 }
2770
2771 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
2773 self.coauthor_enabled = Some(value);
2774 self
2775 }
2776
2777 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
2779 self.manage_schedule_enabled = Some(value);
2780 self
2781 }
2782
2783 #[doc(hidden)]
2791 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
2792 self.exp_assignments = Some(assignments);
2793 self
2794 }
2795
2796 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
2802 self.enable_managed_settings = Some(enabled);
2803 self
2804 }
2805}
2806#[derive(Clone)]
2813#[non_exhaustive]
2814pub struct ResumeSessionConfig {
2815 pub session_id: SessionId,
2817 pub model: Option<String>,
2820 pub client_name: Option<String>,
2822 pub reasoning_effort: Option<String>,
2824 pub reasoning_summary: Option<ReasoningSummary>,
2828 pub context_tier: Option<String>,
2831 pub streaming: Option<bool>,
2833 pub system_message: Option<SystemMessageConfig>,
2836 pub tools: Option<Vec<Tool>>,
2838 pub canvases: Option<Vec<CanvasDeclaration>>,
2840 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2843 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
2845 pub request_canvas_renderer: Option<bool>,
2847 pub request_extensions: Option<bool>,
2849 pub extension_sdk_path: Option<String>,
2853 pub extension_info: Option<ExtensionInfo>,
2855 pub canvas_provider: Option<CanvasProviderIdentity>,
2858 pub available_tools: Option<Vec<String>>,
2860 pub excluded_tools: Option<Vec<String>>,
2862 pub excluded_builtin_agents: Option<Vec<String>>,
2868 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
2870 pub mcp_oauth_token_storage: Option<String>,
2873 pub enable_config_discovery: Option<bool>,
2875 pub skip_embedding_retrieval: Option<bool>,
2877 pub embedding_cache_storage: Option<String>,
2879 pub organization_custom_instructions: Option<String>,
2881 pub enable_on_demand_instruction_discovery: Option<bool>,
2883 pub enable_file_hooks: Option<bool>,
2885 pub enable_host_git_operations: Option<bool>,
2887 pub enable_session_store: Option<bool>,
2889 pub enable_skills: Option<bool>,
2891 pub enable_mcp_apps: Option<bool>,
2897 pub skill_directories: Option<Vec<PathBuf>>,
2899 pub instruction_directories: Option<Vec<PathBuf>>,
2902 pub plugin_directories: Option<Vec<PathBuf>>,
2904 pub large_output: Option<LargeToolOutputConfig>,
2906 pub disabled_skills: Option<Vec<String>>,
2908 pub hooks: Option<bool>,
2910 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2912 pub default_agent: Option<DefaultAgentConfig>,
2914 pub agent: Option<String>,
2916 pub infinite_sessions: Option<InfiniteSessionConfig>,
2918 pub provider: Option<ProviderConfig>,
2920 pub capi: Option<CapiSessionOptions>,
2926 pub providers: Option<Vec<NamedProviderConfig>>,
2932 pub models: Option<Vec<ProviderModelConfig>>,
2938 pub enable_session_telemetry: Option<bool>,
2946 pub enable_citations: Option<bool>,
2948 pub session_limits: Option<SessionLimitsConfig>,
2950 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2952 pub memory: Option<MemoryConfiguration>,
2954 pub config_directory: Option<PathBuf>,
2956 pub working_directory: Option<PathBuf>,
2958 pub github_token: Option<String>,
2961 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2964 pub include_sub_agent_streaming_events: Option<bool>,
2966 pub commands: Option<Vec<CommandDefinition>>,
2970 #[doc(hidden)]
2975 pub exp_assignments: Option<Value>,
2976 pub enable_managed_settings: Option<bool>,
2982 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2987 pub suppress_resume_event: Option<bool>,
2990 pub continue_pending_work: Option<bool>,
2998 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
3001 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
3004 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
3006 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
3009 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
3012 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
3015 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
3017 pub(crate) permission_policy: Option<crate::permission::Policy>,
3019 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
3021 pub skip_custom_instructions: Option<bool>,
3023 pub custom_agents_local_only: Option<bool>,
3025 pub coauthor_enabled: Option<bool>,
3027 pub manage_schedule_enabled: Option<bool>,
3029}
3030
3031impl std::fmt::Debug for ResumeSessionConfig {
3032 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3033 f.debug_struct("ResumeSessionConfig")
3034 .field("session_id", &self.session_id)
3035 .field("model", &self.model)
3036 .field("client_name", &self.client_name)
3037 .field("reasoning_effort", &self.reasoning_effort)
3038 .field("reasoning_summary", &self.reasoning_summary)
3039 .field("context_tier", &self.context_tier)
3040 .field("streaming", &self.streaming)
3041 .field("system_message", &self.system_message)
3042 .field("tools", &self.tools)
3043 .field("canvases", &self.canvases)
3044 .field(
3045 "canvas_handler",
3046 &self.canvas_handler.as_ref().map(|_| "<set>"),
3047 )
3048 .field("open_canvases", &self.open_canvases)
3049 .field("request_canvas_renderer", &self.request_canvas_renderer)
3050 .field("request_extensions", &self.request_extensions)
3051 .field("extension_sdk_path", &self.extension_sdk_path)
3052 .field("extension_info", &self.extension_info)
3053 .field("canvas_provider", &self.canvas_provider)
3054 .field("available_tools", &self.available_tools)
3055 .field("excluded_tools", &self.excluded_tools)
3056 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3057 .field("mcp_servers", &self.mcp_servers)
3058 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3059 .field("embedding_cache_storage", &self.embedding_cache_storage)
3060 .field("enable_config_discovery", &self.enable_config_discovery)
3061 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3062 .field(
3063 "organization_custom_instructions",
3064 &self
3065 .organization_custom_instructions
3066 .as_ref()
3067 .map(|_| "<redacted>"),
3068 )
3069 .field(
3070 "enable_on_demand_instruction_discovery",
3071 &self.enable_on_demand_instruction_discovery,
3072 )
3073 .field("enable_file_hooks", &self.enable_file_hooks)
3074 .field(
3075 "enable_host_git_operations",
3076 &self.enable_host_git_operations,
3077 )
3078 .field("enable_session_store", &self.enable_session_store)
3079 .field("enable_skills", &self.enable_skills)
3080 .field("enable_mcp_apps", &self.enable_mcp_apps)
3081 .field("skill_directories", &self.skill_directories)
3082 .field("instruction_directories", &self.instruction_directories)
3083 .field("plugin_directories", &self.plugin_directories)
3084 .field("large_output", &self.large_output)
3085 .field("disabled_skills", &self.disabled_skills)
3086 .field("hooks", &self.hooks)
3087 .field("custom_agents", &self.custom_agents)
3088 .field("default_agent", &self.default_agent)
3089 .field("agent", &self.agent)
3090 .field("infinite_sessions", &self.infinite_sessions)
3091 .field("provider", &self.provider)
3092 .field("capi", &self.capi)
3093 .field("enable_session_telemetry", &self.enable_session_telemetry)
3094 .field("enable_citations", &self.enable_citations)
3095 .field("session_limits", &self.session_limits)
3096 .field("model_capabilities", &self.model_capabilities)
3097 .field("memory", &self.memory)
3098 .field("config_directory", &self.config_directory)
3099 .field("working_directory", &self.working_directory)
3100 .field(
3101 "github_token",
3102 &self.github_token.as_ref().map(|_| "<redacted>"),
3103 )
3104 .field("remote_session", &self.remote_session)
3105 .field(
3106 "include_sub_agent_streaming_events",
3107 &self.include_sub_agent_streaming_events,
3108 )
3109 .field("commands", &self.commands)
3110 .field("exp_assignments", &self.exp_assignments)
3111 .field("enable_managed_settings", &self.enable_managed_settings)
3112 .field(
3113 "session_fs_provider",
3114 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3115 )
3116 .field(
3117 "permission_handler",
3118 &self.permission_handler.as_ref().map(|_| "<set>"),
3119 )
3120 .field(
3121 "elicitation_handler",
3122 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3123 )
3124 .field(
3125 "user_input_handler",
3126 &self.user_input_handler.as_ref().map(|_| "<set>"),
3127 )
3128 .field(
3129 "exit_plan_mode_handler",
3130 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3131 )
3132 .field(
3133 "auto_mode_switch_handler",
3134 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3135 )
3136 .field(
3137 "hooks_handler",
3138 &self.hooks_handler.as_ref().map(|_| "<set>"),
3139 )
3140 .field(
3141 "system_message_transform",
3142 &self.system_message_transform.as_ref().map(|_| "<set>"),
3143 )
3144 .field("suppress_resume_event", &self.suppress_resume_event)
3145 .field("continue_pending_work", &self.continue_pending_work)
3146 .finish()
3147 }
3148}
3149
3150impl ResumeSessionConfig {
3151 pub(crate) fn into_wire(
3159 mut self,
3160 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3161 let permission_active =
3162 self.permission_handler.is_some() || self.permission_policy.is_some();
3163 let request_user_input = self.user_input_handler.is_some();
3164 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3165 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3166 let request_elicitation = self.elicitation_handler.is_some();
3167 let hooks_flag = self.hooks_handler.is_some();
3168
3169 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3170 if let Some(tools) = self.tools.as_mut() {
3171 for tool in tools.iter_mut() {
3172 if let Some(handler) = tool.handler.take()
3173 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3174 {
3175 return Err(crate::Error::with_message(
3176 crate::ErrorKind::InvalidConfig,
3177 format!("duplicate tool handler registered for name {:?}", tool.name),
3178 ));
3179 }
3180 }
3181 }
3182
3183 let wire_commands = self.commands.as_ref().map(|cmds| {
3184 cmds.iter()
3185 .map(|c| crate::wire::CommandWireDefinition {
3186 name: c.name.clone(),
3187 description: c.description.clone(),
3188 })
3189 .collect()
3190 });
3191 let wire_canvases = self.canvases.clone();
3192 let canvas_handler = self.canvas_handler.clone();
3193 let bearer_token_providers =
3194 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3195
3196 let wire = crate::wire::SessionResumeWire {
3197 session_id: self.session_id,
3198 model: self.model,
3199 client_name: self.client_name,
3200 reasoning_effort: self.reasoning_effort,
3201 reasoning_summary: self.reasoning_summary,
3202 context_tier: self.context_tier,
3203 streaming: self.streaming,
3204 system_message: self.system_message,
3205 tools: self.tools,
3206 canvases: wire_canvases,
3207 open_canvases: self.open_canvases,
3208 request_canvas_renderer: self.request_canvas_renderer,
3209 request_extensions: self.request_extensions,
3210 extension_sdk_path: self.extension_sdk_path,
3211 extension_info: self.extension_info,
3212 canvas_provider: self.canvas_provider,
3213 available_tools: self.available_tools,
3214 excluded_tools: self.excluded_tools,
3215 excluded_builtin_agents: self.excluded_builtin_agents,
3216 tool_filter_precedence: "excluded",
3217 mcp_servers: self.mcp_servers,
3218 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3219 embedding_cache_storage: self.embedding_cache_storage,
3220 env_value_mode: "direct",
3221 enable_config_discovery: self.enable_config_discovery,
3222 skip_embedding_retrieval: self.skip_embedding_retrieval,
3223 organization_custom_instructions: self.organization_custom_instructions,
3224 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3225 enable_file_hooks: self.enable_file_hooks,
3226 enable_host_git_operations: self.enable_host_git_operations,
3227 enable_session_store: self.enable_session_store,
3228 enable_skills: self.enable_skills,
3229 request_user_input,
3230 request_permission: permission_active,
3231 request_exit_plan_mode,
3232 request_auto_mode_switch,
3233 request_elicitation,
3234 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3235 hooks: hooks_flag,
3236 skill_directories: self.skill_directories,
3237 instruction_directories: self.instruction_directories,
3238 plugin_directories: self.plugin_directories,
3239 large_output: self.large_output,
3240 disabled_skills: self.disabled_skills,
3241 custom_agents: self.custom_agents,
3242 default_agent: self.default_agent,
3243 agent: self.agent,
3244 infinite_sessions: self.infinite_sessions,
3245 provider: self.provider,
3246 capi: self.capi,
3247 providers: self.providers,
3248 models: self.models,
3249 enable_session_telemetry: self.enable_session_telemetry,
3250 enable_citations: self.enable_citations,
3251 session_limits: self.session_limits,
3252 model_capabilities: self.model_capabilities,
3253 memory: self.memory,
3254 config_dir: self.config_directory,
3255 working_directory: self.working_directory,
3256 github_token: self.github_token,
3257 remote_session: self.remote_session,
3258 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3259 enable_github_telemetry_forwarding: None,
3260 commands: wire_commands,
3261 exp_assignments: self.exp_assignments,
3262 enable_managed_settings: self.enable_managed_settings,
3263 suppress_resume_event: self.suppress_resume_event,
3264 continue_pending_work: self.continue_pending_work,
3265 };
3266
3267 let runtime = SessionConfigRuntime {
3268 permission_handler: self.permission_handler,
3269 permission_policy: self.permission_policy,
3270 elicitation_handler: self.elicitation_handler,
3271 mcp_auth_handler: self.mcp_auth_handler,
3272 user_input_handler: self.user_input_handler,
3273 exit_plan_mode_handler: self.exit_plan_mode_handler,
3274 auto_mode_switch_handler: self.auto_mode_switch_handler,
3275 hooks_handler: self.hooks_handler,
3276 system_message_transform: self.system_message_transform,
3277 tool_handlers,
3278 canvas_handler,
3279 session_fs_provider: self.session_fs_provider,
3280 bearer_token_providers,
3281 commands: self.commands,
3282 };
3283
3284 Ok((wire, runtime))
3285 }
3286
3287 pub fn new(session_id: SessionId) -> Self {
3292 Self {
3293 session_id,
3294 model: None,
3295 client_name: None,
3296 reasoning_effort: None,
3297 reasoning_summary: None,
3298 context_tier: None,
3299 streaming: None,
3300 system_message: None,
3301 tools: None,
3302 canvases: None,
3303 canvas_handler: None,
3304 open_canvases: None,
3305 request_canvas_renderer: None,
3306 request_extensions: None,
3307 extension_sdk_path: None,
3308 extension_info: None,
3309 canvas_provider: None,
3310 available_tools: None,
3311 excluded_tools: None,
3312 excluded_builtin_agents: None,
3313 mcp_servers: None,
3314 mcp_oauth_token_storage: None,
3315 enable_config_discovery: None,
3316 skip_embedding_retrieval: None,
3317 organization_custom_instructions: None,
3318 enable_on_demand_instruction_discovery: None,
3319 enable_file_hooks: None,
3320 enable_host_git_operations: None,
3321 enable_session_store: None,
3322 enable_skills: None,
3323 embedding_cache_storage: None,
3324 enable_mcp_apps: None,
3325 skill_directories: None,
3326 instruction_directories: None,
3327 plugin_directories: None,
3328 large_output: None,
3329 disabled_skills: None,
3330 hooks: None,
3331 custom_agents: None,
3332 default_agent: None,
3333 agent: None,
3334 infinite_sessions: None,
3335 provider: None,
3336 capi: None,
3337 providers: None,
3338 models: None,
3339 enable_session_telemetry: None,
3340 enable_citations: None,
3341 session_limits: None,
3342 model_capabilities: None,
3343 memory: None,
3344 config_directory: None,
3345 working_directory: None,
3346 github_token: None,
3347 remote_session: None,
3348 include_sub_agent_streaming_events: None,
3349 commands: None,
3350 exp_assignments: None,
3351 enable_managed_settings: None,
3352 session_fs_provider: None,
3353 suppress_resume_event: None,
3354 continue_pending_work: None,
3355 permission_handler: None,
3356 elicitation_handler: None,
3357 mcp_auth_handler: None,
3358 user_input_handler: None,
3359 exit_plan_mode_handler: None,
3360 auto_mode_switch_handler: None,
3361 hooks_handler: None,
3362 permission_policy: None,
3363 system_message_transform: None,
3364 skip_custom_instructions: None,
3365 custom_agents_local_only: None,
3366 coauthor_enabled: None,
3367 manage_schedule_enabled: None,
3368 }
3369 }
3370
3371 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3373 self.permission_handler = Some(handler);
3374 self
3375 }
3376
3377 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3379 self.elicitation_handler = Some(handler);
3380 self
3381 }
3382
3383 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3385 self.mcp_auth_handler = Some(handler);
3386 self
3387 }
3388
3389 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3391 self.user_input_handler = Some(handler);
3392 self
3393 }
3394
3395 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3397 self.exit_plan_mode_handler = Some(handler);
3398 self
3399 }
3400
3401 pub fn with_auto_mode_switch_handler(
3403 mut self,
3404 handler: Arc<dyn AutoModeSwitchHandler>,
3405 ) -> Self {
3406 self.auto_mode_switch_handler = Some(handler);
3407 self
3408 }
3409
3410 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3413 self.hooks_handler = Some(hooks);
3414 self
3415 }
3416
3417 pub fn with_system_message_transform(
3419 mut self,
3420 transform: Arc<dyn SystemMessageTransform>,
3421 ) -> Self {
3422 self.system_message_transform = Some(transform);
3423 self
3424 }
3425
3426 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3430 self.commands = Some(commands);
3431 self
3432 }
3433
3434 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3437 self.session_fs_provider = Some(provider);
3438 self
3439 }
3440
3441 pub fn approve_all_permissions(mut self) -> Self {
3444 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3445 self
3446 }
3447
3448 pub fn deny_all_permissions(mut self) -> Self {
3451 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3452 self
3453 }
3454
3455 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3458 where
3459 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3460 {
3461 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3462 self
3463 }
3464
3465 pub fn with_model(mut self, model: impl Into<String>) -> Self {
3467 self.model = Some(model.into());
3468 self
3469 }
3470
3471 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3473 self.client_name = Some(name.into());
3474 self
3475 }
3476
3477 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3479 self.reasoning_effort = Some(effort.into());
3480 self
3481 }
3482
3483 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3485 self.reasoning_summary = Some(summary);
3486 self
3487 }
3488
3489 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3492 self.context_tier = Some(tier.into());
3493 self
3494 }
3495
3496 pub fn with_streaming(mut self, streaming: bool) -> Self {
3498 self.streaming = Some(streaming);
3499 self
3500 }
3501
3502 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3505 self.system_message = Some(system_message);
3506 self
3507 }
3508
3509 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3511 self.tools = Some(tools.into_iter().collect());
3512 self
3513 }
3514
3515 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3517 self.canvases = Some(canvases.into_iter().collect());
3518 self
3519 }
3520
3521 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
3523 self.canvas_handler = Some(handler);
3524 self
3525 }
3526
3527 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
3529 mut self,
3530 open_canvases: I,
3531 ) -> Self {
3532 self.open_canvases = Some(open_canvases.into_iter().collect());
3533 self
3534 }
3535
3536 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
3538 self.request_canvas_renderer = Some(request);
3539 self
3540 }
3541
3542 pub fn with_request_extensions(mut self, request: bool) -> Self {
3544 self.request_extensions = Some(request);
3545 self
3546 }
3547
3548 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
3552 self.extension_sdk_path = Some(path.into());
3553 self
3554 }
3555
3556 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
3558 self.extension_info = Some(extension_info);
3559 self
3560 }
3561
3562 pub fn with_canvas_provider(mut self, canvas_provider: CanvasProviderIdentity) -> Self {
3565 self.canvas_provider = Some(canvas_provider);
3566 self
3567 }
3568
3569 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
3571 where
3572 I: IntoIterator<Item = S>,
3573 S: Into<String>,
3574 {
3575 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
3576 self
3577 }
3578
3579 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
3581 where
3582 I: IntoIterator<Item = S>,
3583 S: Into<String>,
3584 {
3585 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
3586 self
3587 }
3588
3589 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
3591 where
3592 I: IntoIterator<Item = S>,
3593 S: Into<String>,
3594 {
3595 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
3596 self
3597 }
3598
3599 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
3601 self.mcp_servers = Some(servers);
3602 self
3603 }
3604
3605 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3608 self.mcp_oauth_token_storage = Some(mode.into());
3609 self
3610 }
3611
3612 pub fn with_embedding_cache_storage(
3614 mut self,
3615 embedding_cache_storage: impl Into<String>,
3616 ) -> Self {
3617 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3618 self
3619 }
3620
3621 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3623 self.enable_config_discovery = Some(enable);
3624 self
3625 }
3626
3627 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3629 self.skip_embedding_retrieval = Some(value);
3630 self
3631 }
3632
3633 pub fn with_organization_custom_instructions(
3635 mut self,
3636 instructions: impl Into<String>,
3637 ) -> Self {
3638 self.organization_custom_instructions = Some(instructions.into());
3639 self
3640 }
3641
3642 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3644 self.enable_on_demand_instruction_discovery = Some(value);
3645 self
3646 }
3647
3648 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3650 self.enable_file_hooks = Some(value);
3651 self
3652 }
3653
3654 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3656 self.enable_host_git_operations = Some(value);
3657 self
3658 }
3659
3660 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3662 self.enable_session_store = Some(value);
3663 self
3664 }
3665
3666 pub fn with_enable_skills(mut self, value: bool) -> Self {
3668 self.enable_skills = Some(value);
3669 self
3670 }
3671
3672 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3678 self.enable_mcp_apps = Some(enable);
3679 self
3680 }
3681
3682 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3684 where
3685 I: IntoIterator<Item = P>,
3686 P: Into<PathBuf>,
3687 {
3688 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3689 self
3690 }
3691
3692 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3696 where
3697 I: IntoIterator<Item = P>,
3698 P: Into<PathBuf>,
3699 {
3700 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3701 self
3702 }
3703
3704 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3706 where
3707 I: IntoIterator<Item = P>,
3708 P: Into<PathBuf>,
3709 {
3710 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3711 self
3712 }
3713
3714 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3716 self.large_output = Some(config);
3717 self
3718 }
3719
3720 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3722 where
3723 I: IntoIterator<Item = S>,
3724 S: Into<String>,
3725 {
3726 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3727 self
3728 }
3729
3730 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3732 mut self,
3733 agents: I,
3734 ) -> Self {
3735 self.custom_agents = Some(agents.into_iter().collect());
3736 self
3737 }
3738
3739 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3741 self.default_agent = Some(agent);
3742 self
3743 }
3744
3745 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3747 self.agent = Some(name.into());
3748 self
3749 }
3750
3751 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3753 self.infinite_sessions = Some(config);
3754 self
3755 }
3756
3757 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3759 self.provider = Some(provider);
3760 self
3761 }
3762
3763 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3765 self.capi = Some(capi);
3766 self
3767 }
3768
3769 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3775 self.providers = Some(providers);
3776 self
3777 }
3778
3779 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3785 self.models = Some(models);
3786 self
3787 }
3788
3789 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3793 self.enable_session_telemetry = Some(enable);
3794 self
3795 }
3796
3797 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3799 self.enable_citations = Some(enable);
3800 self
3801 }
3802
3803 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3805 self.session_limits = Some(limits);
3806 self
3807 }
3808
3809 pub fn with_model_capabilities(
3811 mut self,
3812 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3813 ) -> Self {
3814 self.model_capabilities = Some(capabilities);
3815 self
3816 }
3817
3818 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3820 self.memory = Some(memory);
3821 self
3822 }
3823
3824 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3826 self.config_directory = Some(dir.into());
3827 self
3828 }
3829
3830 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3832 self.working_directory = Some(dir.into());
3833 self
3834 }
3835
3836 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3840 self.github_token = Some(token.into());
3841 self
3842 }
3843
3844 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3846 self.include_sub_agent_streaming_events = Some(include);
3847 self
3848 }
3849
3850 pub fn with_remote_session(
3852 mut self,
3853 mode: crate::generated::api_types::RemoteSessionMode,
3854 ) -> Self {
3855 self.remote_session = Some(mode);
3856 self
3857 }
3858
3859 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
3862 self.suppress_resume_event = Some(suppress);
3863 self
3864 }
3865
3866 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
3872 self.continue_pending_work = Some(continue_pending);
3873 self
3874 }
3875
3876 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3878 self.skip_custom_instructions = Some(value);
3879 self
3880 }
3881
3882 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3884 self.custom_agents_local_only = Some(value);
3885 self
3886 }
3887
3888 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3890 self.coauthor_enabled = Some(value);
3891 self
3892 }
3893
3894 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3896 self.manage_schedule_enabled = Some(value);
3897 self
3898 }
3899
3900 #[doc(hidden)]
3904 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
3905 self.exp_assignments = Some(assignments);
3906 self
3907 }
3908
3909 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3912 self.enable_managed_settings = Some(enabled);
3913 self
3914 }
3915}
3916
3917#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3923#[serde(rename_all = "camelCase")]
3924#[non_exhaustive]
3925pub struct SystemMessageConfig {
3926 #[serde(skip_serializing_if = "Option::is_none")]
3928 pub mode: Option<String>,
3929 #[serde(skip_serializing_if = "Option::is_none")]
3931 pub content: Option<String>,
3932 #[serde(skip_serializing_if = "Option::is_none")]
3934 pub sections: Option<HashMap<String, SectionOverride>>,
3935}
3936
3937impl SystemMessageConfig {
3938 pub fn new() -> Self {
3941 Self::default()
3942 }
3943
3944 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
3947 self.mode = Some(mode.into());
3948 self
3949 }
3950
3951 pub fn with_content(mut self, content: impl Into<String>) -> Self {
3954 self.content = Some(content.into());
3955 self
3956 }
3957
3958 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
3960 self.sections = Some(sections);
3961 self
3962 }
3963}
3964
3965#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3971#[serde(rename_all = "camelCase")]
3972pub struct SectionOverride {
3973 #[serde(skip_serializing_if = "Option::is_none")]
3976 pub action: Option<String>,
3977 #[serde(skip_serializing_if = "Option::is_none")]
3979 pub content: Option<String>,
3980}
3981
3982#[derive(Debug, Clone, Serialize, Deserialize)]
3984#[serde(rename_all = "camelCase")]
3985pub struct CreateSessionResult {
3986 pub session_id: SessionId,
3988 #[serde(skip_serializing_if = "Option::is_none")]
3990 pub workspace_path: Option<PathBuf>,
3991 #[serde(default, alias = "remote_url")]
3993 pub remote_url: Option<String>,
3994 #[serde(skip_serializing_if = "Option::is_none")]
3996 pub capabilities: Option<SessionCapabilities>,
3997}
3998
3999#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4001#[serde(rename_all = "camelCase")]
4002pub(crate) struct ResumeSessionResult {
4003 #[serde(default)]
4005 pub session_id: Option<SessionId>,
4006 #[serde(default, skip_serializing_if = "Option::is_none")]
4008 pub workspace_path: Option<PathBuf>,
4009 #[serde(default, alias = "remote_url")]
4011 pub remote_url: Option<String>,
4012 #[serde(default, skip_serializing_if = "Option::is_none")]
4014 pub capabilities: Option<SessionCapabilities>,
4015 #[serde(
4017 default,
4018 alias = "openCanvasInstances",
4019 skip_serializing_if = "Option::is_none"
4020 )]
4021 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
4022}
4023
4024#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4026#[serde(rename_all = "lowercase")]
4027pub enum LogLevel {
4028 #[default]
4030 Info,
4031 Warning,
4033 Error,
4035}
4036
4037#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4042#[serde(rename_all = "camelCase")]
4043pub struct LogOptions {
4044 #[serde(skip_serializing_if = "Option::is_none")]
4046 pub level: Option<LogLevel>,
4047 #[serde(skip_serializing_if = "Option::is_none")]
4050 pub ephemeral: Option<bool>,
4051}
4052
4053impl LogOptions {
4054 pub fn with_level(mut self, level: LogLevel) -> Self {
4056 self.level = Some(level);
4057 self
4058 }
4059
4060 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4062 self.ephemeral = Some(ephemeral);
4063 self
4064 }
4065}
4066
4067#[derive(Debug, Clone, Default)]
4071pub struct SetModelOptions {
4072 pub reasoning_effort: Option<String>,
4075 pub reasoning_summary: Option<ReasoningSummary>,
4079 pub context_tier: Option<ContextTier>,
4082 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4086}
4087
4088impl SetModelOptions {
4089 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4091 self.reasoning_effort = Some(effort.into());
4092 self
4093 }
4094
4095 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4097 self.reasoning_summary = Some(summary);
4098 self
4099 }
4100
4101 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4103 self.context_tier = Some(tier);
4104 self
4105 }
4106
4107 pub fn with_model_capabilities(
4109 mut self,
4110 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4111 ) -> Self {
4112 self.model_capabilities = Some(caps);
4113 self
4114 }
4115}
4116
4117#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4124#[serde(rename_all = "camelCase")]
4125pub struct PingResponse {
4126 #[serde(default)]
4128 pub message: String,
4129 #[serde(default)]
4131 pub timestamp: String,
4132 #[serde(skip_serializing_if = "Option::is_none")]
4134 pub protocol_version: Option<u32>,
4135}
4136
4137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4139#[serde(rename_all = "camelCase")]
4140pub struct AttachmentLineRange {
4141 pub start: u32,
4143 pub end: u32,
4145}
4146
4147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4149#[serde(rename_all = "camelCase")]
4150pub struct AttachmentSelectionPosition {
4151 pub line: u32,
4153 pub character: u32,
4155}
4156
4157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4159#[serde(rename_all = "camelCase")]
4160pub struct AttachmentSelectionRange {
4161 pub start: AttachmentSelectionPosition,
4163 pub end: AttachmentSelectionPosition,
4165}
4166
4167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4169#[serde(rename_all = "snake_case")]
4170#[non_exhaustive]
4171pub enum GitHubReferenceType {
4172 Issue,
4174 Pr,
4176 Discussion,
4178}
4179
4180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4186#[serde(rename_all = "camelCase")]
4187pub struct GitHubRepoPointer {
4188 #[serde(skip_serializing_if = "Option::is_none")]
4190 pub id: Option<i64>,
4191 pub name: String,
4193 pub owner: String,
4195}
4196
4197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4199#[serde(rename_all = "camelCase")]
4200pub struct GitHubFileDiffSide {
4201 pub path: String,
4203 pub r#ref: String,
4205 pub repo: GitHubRepoPointer,
4207}
4208
4209#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4211#[serde(rename_all = "camelCase")]
4212pub struct GitHubTreeComparisonSide {
4213 pub repo: GitHubRepoPointer,
4215 pub revision: String,
4217}
4218
4219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4221#[serde(rename_all = "camelCase")]
4222pub struct GitHubSnippetLineRange {
4223 pub start: i64,
4225 pub end: i64,
4227}
4228
4229#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4231#[serde(
4232 tag = "type",
4233 rename_all = "camelCase",
4234 rename_all_fields = "camelCase"
4235)]
4236#[non_exhaustive]
4237pub enum Attachment {
4238 File {
4240 path: PathBuf,
4242 #[serde(skip_serializing_if = "Option::is_none")]
4244 display_name: Option<String>,
4245 #[serde(skip_serializing_if = "Option::is_none")]
4247 line_range: Option<AttachmentLineRange>,
4248 },
4249 Directory {
4251 path: PathBuf,
4253 #[serde(skip_serializing_if = "Option::is_none")]
4255 display_name: Option<String>,
4256 },
4257 Selection {
4259 file_path: PathBuf,
4261 text: String,
4263 #[serde(skip_serializing_if = "Option::is_none")]
4265 display_name: Option<String>,
4266 selection: AttachmentSelectionRange,
4268 },
4269 Blob {
4271 data: String,
4273 mime_type: String,
4275 #[serde(skip_serializing_if = "Option::is_none")]
4277 display_name: Option<String>,
4278 },
4279 #[serde(rename = "github_reference")]
4281 GitHubReference {
4282 number: u64,
4284 title: String,
4286 reference_type: GitHubReferenceType,
4288 state: String,
4290 url: String,
4292 },
4293 #[serde(rename = "github_commit")]
4295 GitHubCommit {
4296 message: String,
4298 oid: String,
4300 repo: GitHubRepoPointer,
4302 url: String,
4304 },
4305 #[serde(rename = "github_release")]
4307 GitHubRelease {
4308 name: String,
4310 repo: GitHubRepoPointer,
4312 tag_name: String,
4314 url: String,
4316 },
4317 #[serde(rename = "github_actions_job")]
4319 GitHubActionsJob {
4320 #[serde(skip_serializing_if = "Option::is_none")]
4323 conclusion: Option<String>,
4324 job_id: i64,
4326 job_name: String,
4328 repo: GitHubRepoPointer,
4330 url: String,
4332 workflow_name: String,
4334 },
4335 #[serde(rename = "github_repository")]
4337 GitHubRepository {
4338 #[serde(skip_serializing_if = "Option::is_none")]
4340 description: Option<String>,
4341 #[serde(skip_serializing_if = "Option::is_none")]
4344 r#ref: Option<String>,
4345 repo: GitHubRepoPointer,
4347 url: String,
4349 },
4350 #[serde(rename = "github_file_diff")]
4352 GitHubFileDiff {
4353 #[serde(skip_serializing_if = "Option::is_none")]
4355 base: Option<GitHubFileDiffSide>,
4356 #[serde(skip_serializing_if = "Option::is_none")]
4358 head: Option<GitHubFileDiffSide>,
4359 url: String,
4361 },
4362 #[serde(rename = "github_tree_comparison")]
4364 GitHubTreeComparison {
4365 base: GitHubTreeComparisonSide,
4367 head: GitHubTreeComparisonSide,
4369 url: String,
4371 },
4372 #[serde(rename = "github_url")]
4374 GitHubUrl {
4375 url: String,
4377 },
4378 #[serde(rename = "github_file")]
4380 GitHubFile {
4381 path: String,
4383 r#ref: String,
4385 repo: GitHubRepoPointer,
4387 url: String,
4389 },
4390 #[serde(rename = "github_snippet")]
4392 GitHubSnippet {
4393 line_range: GitHubSnippetLineRange,
4395 path: String,
4397 r#ref: String,
4399 repo: GitHubRepoPointer,
4401 url: String,
4403 },
4404}
4405
4406impl Attachment {
4407 pub fn display_name(&self) -> Option<&str> {
4409 match self {
4410 Self::File { display_name, .. }
4411 | Self::Directory { display_name, .. }
4412 | Self::Selection { display_name, .. }
4413 | Self::Blob { display_name, .. } => display_name.as_deref(),
4414 Self::GitHubReference { .. }
4415 | Self::GitHubCommit { .. }
4416 | Self::GitHubRelease { .. }
4417 | Self::GitHubActionsJob { .. }
4418 | Self::GitHubRepository { .. }
4419 | Self::GitHubFileDiff { .. }
4420 | Self::GitHubTreeComparison { .. }
4421 | Self::GitHubUrl { .. }
4422 | Self::GitHubFile { .. }
4423 | Self::GitHubSnippet { .. } => None,
4424 }
4425 }
4426
4427 pub fn label(&self) -> Option<String> {
4429 if let Some(display_name) = self
4430 .display_name()
4431 .map(str::trim)
4432 .filter(|name| !name.is_empty())
4433 {
4434 return Some(display_name.to_string());
4435 }
4436
4437 match self {
4438 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4439 format!("#{}", number)
4440 } else {
4441 title.trim().to_string()
4442 }),
4443 _ => self.derived_display_name(),
4444 }
4445 }
4446
4447 pub fn ensure_display_name(&mut self) {
4449 if self
4450 .display_name()
4451 .map(str::trim)
4452 .is_some_and(|name| !name.is_empty())
4453 {
4454 return;
4455 }
4456
4457 let Some(derived_display_name) = self.derived_display_name() else {
4458 return;
4459 };
4460
4461 match self {
4462 Self::File { display_name, .. }
4463 | Self::Directory { display_name, .. }
4464 | Self::Selection { display_name, .. }
4465 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4466 Self::GitHubReference { .. }
4467 | Self::GitHubCommit { .. }
4468 | Self::GitHubRelease { .. }
4469 | Self::GitHubActionsJob { .. }
4470 | Self::GitHubRepository { .. }
4471 | Self::GitHubFileDiff { .. }
4472 | Self::GitHubTreeComparison { .. }
4473 | Self::GitHubUrl { .. }
4474 | Self::GitHubFile { .. }
4475 | Self::GitHubSnippet { .. } => {}
4476 }
4477 }
4478
4479 fn derived_display_name(&self) -> Option<String> {
4480 match self {
4481 Self::File { path, .. } | Self::Directory { path, .. } => {
4482 Some(attachment_name_from_path(path))
4483 }
4484 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4485 Self::Blob { .. } => Some("attachment".to_string()),
4486 Self::GitHubReference { .. }
4487 | Self::GitHubCommit { .. }
4488 | Self::GitHubRelease { .. }
4489 | Self::GitHubActionsJob { .. }
4490 | Self::GitHubRepository { .. }
4491 | Self::GitHubFileDiff { .. }
4492 | Self::GitHubTreeComparison { .. }
4493 | Self::GitHubUrl { .. }
4494 | Self::GitHubFile { .. }
4495 | Self::GitHubSnippet { .. } => None,
4496 }
4497 }
4498}
4499
4500fn attachment_name_from_path(path: &Path) -> String {
4501 path.file_name()
4502 .map(|name| name.to_string_lossy().into_owned())
4503 .filter(|name| !name.is_empty())
4504 .unwrap_or_else(|| {
4505 let full = path.to_string_lossy();
4506 if full.is_empty() {
4507 "attachment".to_string()
4508 } else {
4509 full.into_owned()
4510 }
4511 })
4512}
4513
4514pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
4516 for attachment in attachments {
4517 attachment.ensure_display_name();
4518 }
4519}
4520
4521#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4526#[serde(rename_all = "lowercase")]
4527#[non_exhaustive]
4528pub enum DeliveryMode {
4529 Enqueue,
4531 Immediate,
4533}
4534
4535#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4540#[serde(rename_all = "lowercase")]
4541#[non_exhaustive]
4542pub enum AgentMode {
4543 Interactive,
4545 Plan,
4547 Autopilot,
4549 Shell,
4551}
4552
4553#[derive(Debug, Clone)]
4582#[non_exhaustive]
4583pub struct MessageOptions {
4584 pub prompt: String,
4586 pub mode: Option<DeliveryMode>,
4592 pub agent_mode: Option<AgentMode>,
4596 pub attachments: Option<Vec<Attachment>>,
4598 pub wait_timeout: Option<Duration>,
4601 pub request_headers: Option<HashMap<String, String>>,
4605 pub traceparent: Option<String>,
4612 pub tracestate: Option<String>,
4616 pub display_prompt: Option<String>,
4618}
4619
4620impl MessageOptions {
4621 pub fn new(prompt: impl Into<String>) -> Self {
4623 Self {
4624 prompt: prompt.into(),
4625 mode: None,
4626 agent_mode: None,
4627 attachments: None,
4628 wait_timeout: None,
4629 request_headers: None,
4630 traceparent: None,
4631 tracestate: None,
4632 display_prompt: None,
4633 }
4634 }
4635
4636 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
4642 self.mode = Some(mode);
4643 self
4644 }
4645
4646 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
4650 self.agent_mode = Some(agent_mode);
4651 self
4652 }
4653
4654 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
4656 self.attachments = Some(attachments);
4657 self
4658 }
4659
4660 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
4662 self.wait_timeout = Some(timeout);
4663 self
4664 }
4665
4666 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
4668 self.request_headers = Some(headers);
4669 self
4670 }
4671
4672 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
4677 self.traceparent = ctx.traceparent;
4678 self.tracestate = ctx.tracestate;
4679 self
4680 }
4681
4682 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
4684 self.traceparent = Some(traceparent.into());
4685 self
4686 }
4687
4688 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
4690 self.tracestate = Some(tracestate.into());
4691 self
4692 }
4693
4694 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
4696 self.display_prompt = Some(display_prompt.into());
4697 self
4698 }
4699}
4700
4701impl From<&str> for MessageOptions {
4702 fn from(prompt: &str) -> Self {
4703 Self::new(prompt)
4704 }
4705}
4706
4707impl From<String> for MessageOptions {
4708 fn from(prompt: String) -> Self {
4709 Self::new(prompt)
4710 }
4711}
4712
4713impl From<&String> for MessageOptions {
4714 fn from(prompt: &String) -> Self {
4715 Self::new(prompt.clone())
4716 }
4717}
4718
4719#[derive(Debug, Clone, Serialize, Deserialize)]
4721#[serde(rename_all = "camelCase")]
4722#[non_exhaustive]
4723pub struct GetStatusResponse {
4724 pub version: String,
4726 pub protocol_version: u32,
4728}
4729
4730#[derive(Debug, Clone, Serialize, Deserialize)]
4732#[serde(rename_all = "camelCase")]
4733#[non_exhaustive]
4734pub struct GetAuthStatusResponse {
4735 pub is_authenticated: bool,
4737 #[serde(skip_serializing_if = "Option::is_none")]
4740 pub auth_type: Option<String>,
4741 #[serde(skip_serializing_if = "Option::is_none")]
4743 pub host: Option<String>,
4744 #[serde(skip_serializing_if = "Option::is_none")]
4746 pub login: Option<String>,
4747 #[serde(skip_serializing_if = "Option::is_none")]
4749 pub status_message: Option<String>,
4750}
4751
4752#[derive(Debug, Clone, Serialize, Deserialize)]
4756#[serde(rename_all = "camelCase")]
4757pub struct SessionEventNotification {
4758 pub session_id: SessionId,
4760 pub event: SessionEvent,
4762}
4763
4764#[derive(Debug, Clone, Serialize, Deserialize)]
4771#[serde(rename_all = "camelCase")]
4772pub struct SessionEvent {
4773 pub id: String,
4775 pub timestamp: String,
4777 pub parent_id: Option<String>,
4779 #[serde(skip_serializing_if = "Option::is_none")]
4781 pub ephemeral: Option<bool>,
4782 #[serde(skip_serializing_if = "Option::is_none")]
4785 pub agent_id: Option<String>,
4786 #[serde(skip_serializing_if = "Option::is_none")]
4788 pub debug_cli_received_at_ms: Option<i64>,
4789 #[serde(skip_serializing_if = "Option::is_none")]
4791 pub debug_ws_forwarded_at_ms: Option<i64>,
4792 #[serde(rename = "type")]
4794 pub event_type: String,
4795 pub data: Value,
4797}
4798
4799impl SessionEvent {
4800 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
4805 use serde::de::IntoDeserializer;
4806 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
4807 self.event_type.as_str().into_deserializer();
4808 crate::generated::SessionEventType::deserialize(deserializer)
4809 .unwrap_or(crate::generated::SessionEventType::Unknown)
4810 }
4811
4812 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
4818 serde_json::from_value(self.data.clone()).ok()
4819 }
4820
4821 pub fn is_transient_error(&self) -> bool {
4825 self.event_type == "session.error"
4826 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
4827 }
4828}
4829
4830#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4835#[serde(rename_all = "camelCase")]
4836#[non_exhaustive]
4837pub struct ToolInvocation {
4838 pub session_id: SessionId,
4840 pub tool_call_id: String,
4842 pub tool_name: String,
4844 pub arguments: Value,
4846 #[serde(default, skip_serializing_if = "Option::is_none")]
4851 pub traceparent: Option<String>,
4852 #[serde(default, skip_serializing_if = "Option::is_none")]
4855 pub tracestate: Option<String>,
4856}
4857
4858impl ToolInvocation {
4859 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
4880 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
4881 }
4882
4883 pub fn trace_context(&self) -> TraceContext {
4886 TraceContext {
4887 traceparent: self.traceparent.clone(),
4888 tracestate: self.tracestate.clone(),
4889 }
4890 }
4891}
4892
4893#[derive(Debug, Clone, Serialize, Deserialize)]
4895#[serde(rename_all = "camelCase")]
4896pub struct ToolBinaryResult {
4897 pub data: String,
4899 pub mime_type: String,
4901 pub r#type: String,
4903 #[serde(default, skip_serializing_if = "Option::is_none")]
4905 pub description: Option<String>,
4906}
4907
4908#[derive(Debug, Clone, Serialize, Deserialize)]
4910#[serde(rename_all = "camelCase")]
4911pub struct ToolResultExpanded {
4912 pub text_result_for_llm: String,
4914 pub result_type: String,
4916 #[serde(default, skip_serializing_if = "Option::is_none")]
4918 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
4919 #[serde(skip_serializing_if = "Option::is_none")]
4921 pub session_log: Option<String>,
4922 #[serde(skip_serializing_if = "Option::is_none")]
4924 pub error: Option<String>,
4925 #[serde(default, skip_serializing_if = "Option::is_none")]
4927 pub tool_telemetry: Option<HashMap<String, Value>>,
4928}
4929
4930#[derive(Debug, Clone, Serialize, Deserialize)]
4932#[serde(untagged)]
4933#[non_exhaustive]
4934pub enum ToolResult {
4935 Text(String),
4937 Expanded(ToolResultExpanded),
4939}
4940
4941#[derive(Debug, Clone, Serialize, Deserialize)]
4943#[serde(rename_all = "camelCase")]
4944pub struct ToolResultResponse {
4945 pub result: ToolResult,
4947}
4948
4949#[derive(Debug, Clone, Serialize, Deserialize)]
4951#[serde(rename_all = "camelCase")]
4952pub struct SessionMetadata {
4953 pub session_id: SessionId,
4955 pub start_time: String,
4957 pub modified_time: String,
4959 #[serde(skip_serializing_if = "Option::is_none")]
4961 pub summary: Option<String>,
4962 pub is_remote: bool,
4964}
4965
4966#[derive(Debug, Clone, Serialize, Deserialize)]
4968#[serde(rename_all = "camelCase")]
4969pub struct ListSessionsResponse {
4970 pub sessions: Vec<SessionMetadata>,
4972}
4973
4974#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4978#[serde(rename_all = "camelCase")]
4979pub struct SessionListFilter {
4980 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
4982 pub working_directory: Option<String>,
4983 #[serde(default, skip_serializing_if = "Option::is_none")]
4985 pub git_root: Option<String>,
4986 #[serde(default, skip_serializing_if = "Option::is_none")]
4988 pub repository: Option<String>,
4989 #[serde(default, skip_serializing_if = "Option::is_none")]
4991 pub branch: Option<String>,
4992}
4993
4994#[derive(Debug, Clone, Serialize, Deserialize)]
4996#[serde(rename_all = "camelCase")]
4997pub struct GetSessionMetadataResponse {
4998 #[serde(skip_serializing_if = "Option::is_none")]
5000 pub session: Option<SessionMetadata>,
5001}
5002
5003#[derive(Debug, Clone, Serialize, Deserialize)]
5005#[serde(rename_all = "camelCase")]
5006pub struct GetLastSessionIdResponse {
5007 #[serde(skip_serializing_if = "Option::is_none")]
5009 pub session_id: Option<SessionId>,
5010}
5011
5012#[derive(Debug, Clone, Serialize, Deserialize)]
5014#[serde(rename_all = "camelCase")]
5015pub struct GetForegroundSessionResponse {
5016 #[serde(skip_serializing_if = "Option::is_none")]
5018 pub session_id: Option<SessionId>,
5019}
5020
5021#[derive(Debug, Clone, Serialize, Deserialize)]
5023#[serde(rename_all = "camelCase")]
5024pub struct GetMessagesResponse {
5025 pub events: Vec<SessionEvent>,
5027}
5028
5029#[derive(Debug, Clone, Serialize, Deserialize)]
5031#[serde(rename_all = "camelCase")]
5032pub struct ElicitationResult {
5033 pub action: String,
5035 #[serde(skip_serializing_if = "Option::is_none")]
5037 pub content: Option<Value>,
5038}
5039
5040#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5046#[serde(rename_all = "camelCase")]
5047#[non_exhaustive]
5048pub enum ElicitationMode {
5049 Form,
5051 Url,
5053 #[serde(other)]
5055 Unknown,
5056}
5057
5058#[derive(Debug, Clone, Serialize, Deserialize)]
5065#[serde(rename_all = "camelCase")]
5066pub struct ElicitationRequest {
5067 pub message: String,
5069 #[serde(skip_serializing_if = "Option::is_none")]
5071 pub requested_schema: Option<Value>,
5072 #[serde(skip_serializing_if = "Option::is_none")]
5074 pub mode: Option<ElicitationMode>,
5075 #[serde(skip_serializing_if = "Option::is_none")]
5077 pub elicitation_source: Option<String>,
5078 #[serde(skip_serializing_if = "Option::is_none")]
5080 pub url: Option<String>,
5081}
5082
5083#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5088#[serde(rename_all = "camelCase")]
5089pub struct SessionCapabilities {
5090 #[serde(skip_serializing_if = "Option::is_none")]
5092 pub ui: Option<UiCapabilities>,
5093}
5094
5095#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5097#[serde(rename_all = "camelCase")]
5098pub struct UiCapabilities {
5099 #[serde(skip_serializing_if = "Option::is_none")]
5101 pub elicitation: Option<bool>,
5102 #[serde(skip_serializing_if = "Option::is_none")]
5113 pub mcp_apps: Option<bool>,
5114 #[serde(skip_serializing_if = "Option::is_none")]
5116 pub canvases: Option<bool>,
5117}
5118
5119#[derive(Debug, Clone, Default)]
5121pub struct UiInputOptions<'a> {
5122 pub title: Option<&'a str>,
5124 pub description: Option<&'a str>,
5126 pub min_length: Option<u64>,
5128 pub max_length: Option<u64>,
5130 pub format: Option<InputFormat>,
5132 pub default: Option<&'a str>,
5134}
5135
5136#[derive(Debug, Clone, Copy)]
5138#[non_exhaustive]
5139pub enum InputFormat {
5140 Email,
5142 Uri,
5144 Date,
5146 DateTime,
5148}
5149
5150impl InputFormat {
5151 pub fn as_str(&self) -> &'static str {
5153 match self {
5154 Self::Email => "email",
5155 Self::Uri => "uri",
5156 Self::Date => "date",
5157 Self::DateTime => "date-time",
5158 }
5159 }
5160}
5161
5162pub use crate::generated::api_types::{
5167 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5168 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5169 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5170 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
5171};
5172
5173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5179#[serde(rename_all = "kebab-case")]
5180#[non_exhaustive]
5181pub enum PermissionRequestKind {
5182 Shell,
5184 Write,
5186 Read,
5188 Url,
5190 Mcp,
5192 CustomTool,
5194 Memory,
5196 Hook,
5198 #[serde(other)]
5201 Unknown,
5202}
5203
5204#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5210#[serde(rename_all = "camelCase")]
5211pub struct PermissionRequestData {
5212 #[serde(default, skip_serializing_if = "Option::is_none")]
5216 pub kind: Option<PermissionRequestKind>,
5217 #[serde(default, skip_serializing_if = "Option::is_none")]
5220 pub tool_call_id: Option<String>,
5221 #[serde(flatten)]
5224 pub extra: Value,
5225}
5226
5227#[derive(Debug, Clone, Serialize, Deserialize)]
5229#[serde(rename_all = "camelCase")]
5230pub struct ExitPlanModeData {
5231 #[serde(default)]
5233 pub summary: String,
5234 #[serde(default, skip_serializing_if = "Option::is_none")]
5236 pub plan_content: Option<String>,
5237 #[serde(default)]
5239 pub actions: Vec<String>,
5240 #[serde(default = "default_recommended_action")]
5242 pub recommended_action: String,
5243}
5244
5245fn default_recommended_action() -> String {
5246 "autopilot".to_string()
5247}
5248
5249impl Default for ExitPlanModeData {
5250 fn default() -> Self {
5251 Self {
5252 summary: String::new(),
5253 plan_content: None,
5254 actions: Vec::new(),
5255 recommended_action: default_recommended_action(),
5256 }
5257 }
5258}
5259
5260#[cfg(test)]
5261mod tests {
5262 use std::path::PathBuf;
5263
5264 use serde_json::json;
5265
5266 use super::{
5267 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5268 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5269 CustomAgentConfig, DeliveryMode, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig,
5270 LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration,
5271 NamedProviderConfig, ProviderConfig, ProviderModelConfig, ReasoningSummary,
5272 ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, SystemMessageConfig, Tool,
5273 ToolBinaryResult, ToolResult, ToolResultExpanded, ToolResultResponse,
5274 ensure_attachment_display_names,
5275 };
5276 use crate::generated::session_events::TypedSessionEvent;
5277
5278 #[test]
5279 fn tool_builder_composes() {
5280 let tool = Tool::new("greet")
5281 .with_description("Say hello")
5282 .with_namespaced_name("hello/greet")
5283 .with_instructions("Pass the user's name")
5284 .with_parameters(json!({
5285 "type": "object",
5286 "properties": { "name": { "type": "string" } },
5287 "required": ["name"]
5288 }))
5289 .with_overrides_built_in_tool(true)
5290 .with_skip_permission(true);
5291 assert_eq!(tool.name, "greet");
5292 assert_eq!(tool.description, "Say hello");
5293 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5294 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5295 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5296 assert!(tool.overrides_built_in_tool);
5297 assert!(tool.skip_permission);
5298 }
5299
5300 #[test]
5301 fn tool_defer_serialization() {
5302 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5303 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5304 let value = serde_json::to_value(&tool).unwrap();
5305 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5306
5307 let plain = Tool::new("plain");
5308 let value = serde_json::to_value(&plain).unwrap();
5309 assert!(value.get("defer").is_none());
5310 }
5311
5312 #[test]
5313 fn custom_agent_config_builder_with_model() {
5314 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5315 .with_model("claude-haiku-4.5")
5316 .with_display_name("My Agent");
5317 assert_eq!(agent.name, "my-agent");
5318 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5319 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5320 }
5321
5322 #[test]
5323 fn custom_agent_config_serializes_model() {
5324 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5325 let wire = serde_json::to_value(&agent).unwrap();
5326 assert_eq!(wire["model"], "claude-haiku-4.5");
5327 assert_eq!(wire["name"], "model-agent");
5328 }
5329
5330 #[test]
5331 fn custom_agent_config_omits_model_when_none() {
5332 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5333 let wire = serde_json::to_value(&agent).unwrap();
5334 assert!(wire.get("model").is_none());
5335 }
5336
5337 #[test]
5338 #[should_panic(expected = "tool parameter schema must be a JSON object")]
5339 fn tool_with_parameters_panics_on_non_object_value() {
5340 let _ = Tool::new("noop").with_parameters(json!(null));
5341 }
5342
5343 #[test]
5344 fn tool_result_expanded_serializes_binary_results_for_llm() {
5345 let response = ToolResultResponse {
5346 result: ToolResult::Expanded(ToolResultExpanded {
5347 text_result_for_llm: "rendered chart".to_string(),
5348 result_type: "success".to_string(),
5349 binary_results_for_llm: Some(vec![ToolBinaryResult {
5350 data: "aW1n".to_string(),
5351 mime_type: "image/png".to_string(),
5352 r#type: "image".to_string(),
5353 description: Some("chart preview".to_string()),
5354 }]),
5355 session_log: None,
5356 error: None,
5357 tool_telemetry: None,
5358 }),
5359 };
5360
5361 let wire = serde_json::to_value(&response).unwrap();
5362
5363 assert_eq!(
5364 wire,
5365 json!({
5366 "result": {
5367 "textResultForLlm": "rendered chart",
5368 "resultType": "success",
5369 "binaryResultsForLlm": [
5370 {
5371 "data": "aW1n",
5372 "mimeType": "image/png",
5373 "type": "image",
5374 "description": "chart preview"
5375 }
5376 ]
5377 }
5378 })
5379 );
5380 }
5381
5382 #[test]
5383 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
5384 let response = ToolResultResponse {
5385 result: ToolResult::Expanded(ToolResultExpanded {
5386 text_result_for_llm: "ok".to_string(),
5387 result_type: "success".to_string(),
5388 binary_results_for_llm: None,
5389 session_log: None,
5390 error: None,
5391 tool_telemetry: None,
5392 }),
5393 };
5394
5395 let wire = serde_json::to_value(&response).unwrap();
5396
5397 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5398 assert!(wire["result"].get("binaryResultsForLlm").is_none());
5399 }
5400
5401 #[test]
5402 fn session_config_default_wire_flags_off_without_handlers() {
5403 let cfg = SessionConfig::default();
5404 assert_eq!(cfg.mcp_oauth_token_storage, None);
5405 let (wire, _runtime) = cfg
5409 .into_wire(Some(SessionId::from("default-flags")))
5410 .expect("default config has no duplicate handlers");
5411 assert!(!wire.request_user_input);
5412 assert!(!wire.request_permission);
5413 assert!(!wire.request_elicitation);
5414 assert!(!wire.request_exit_plan_mode);
5415 assert!(!wire.request_auto_mode_switch);
5416 assert!(!wire.hooks);
5417 assert!(!wire.request_mcp_apps);
5418 }
5419
5420 #[test]
5421 fn resume_session_config_new_wire_flags_off_without_handlers() {
5422 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
5423 assert_eq!(cfg.mcp_oauth_token_storage, None);
5424 let (wire, _runtime) = cfg
5425 .into_wire()
5426 .expect("default resume config has no duplicate handlers");
5427 assert!(!wire.request_user_input);
5428 assert!(!wire.request_permission);
5429 assert!(!wire.request_elicitation);
5430 assert!(!wire.request_exit_plan_mode);
5431 assert!(!wire.request_auto_mode_switch);
5432 assert!(!wire.hooks);
5433 assert!(!wire.request_mcp_apps);
5434 }
5435
5436 #[test]
5437 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5438 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
5439 assert_eq!(cfg.enable_mcp_apps, Some(true));
5440
5441 let (wire, _runtime) = cfg
5442 .into_wire(Some(SessionId::from("enable-mcp-apps")))
5443 .expect("enable_mcp_apps config has no duplicate handlers");
5444 assert!(wire.request_mcp_apps);
5445
5446 let json = serde_json::to_value(&wire).unwrap();
5447 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5448 }
5449
5450 #[test]
5451 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5452 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
5453 .with_enable_mcp_apps(true);
5454 assert_eq!(cfg.enable_mcp_apps, Some(true));
5455
5456 let (wire, _runtime) = cfg
5457 .into_wire()
5458 .expect("resume enable_mcp_apps config has no duplicate handlers");
5459 assert!(wire.request_mcp_apps);
5460
5461 let json = serde_json::to_value(&wire).unwrap();
5462 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5463 }
5464
5465 #[test]
5466 fn memory_configuration_constructors_and_serde() {
5467 assert!(MemoryConfiguration::enabled().enabled);
5468 assert!(!MemoryConfiguration::disabled().enabled);
5469 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
5470
5471 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
5472 assert_eq!(json, serde_json::json!({ "enabled": true }));
5473 }
5474
5475 #[test]
5476 fn session_config_with_memory_serializes() {
5477 let (wire, _runtime) = SessionConfig::default()
5478 .with_memory(MemoryConfiguration::enabled())
5479 .into_wire(Some(SessionId::from("memory-on")))
5480 .expect("no duplicate handlers");
5481 let json = serde_json::to_value(&wire).unwrap();
5482 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5483
5484 let (wire_off, _) = SessionConfig::default()
5485 .with_memory(MemoryConfiguration::disabled())
5486 .into_wire(Some(SessionId::from("memory-off")))
5487 .expect("no duplicate handlers");
5488 let json_off = serde_json::to_value(&wire_off).unwrap();
5489 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
5490
5491 let (empty_wire, _) = SessionConfig::default()
5493 .into_wire(Some(SessionId::from("memory-unset")))
5494 .expect("no duplicate handlers");
5495 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5496 assert!(empty_json.get("memory").is_none());
5497 }
5498
5499 #[test]
5500 fn resume_session_config_with_memory_serializes() {
5501 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
5502 .with_memory(MemoryConfiguration::enabled())
5503 .into_wire()
5504 .expect("no duplicate handlers");
5505 let json = serde_json::to_value(&wire).unwrap();
5506 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5507
5508 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
5510 .into_wire()
5511 .expect("no duplicate handlers");
5512 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5513 assert!(empty_json.get("memory").is_none());
5514 }
5515
5516 #[test]
5517 fn session_config_with_exp_assignments_serializes() {
5518 let assignments = serde_json::json!({
5519 "Parameters": { "copilot_exp_flag": "treatment" },
5520 "AssignmentContext": "ctx-123",
5521 });
5522 let (wire, _runtime) = SessionConfig::default()
5523 .with_exp_assignments(assignments.clone())
5524 .into_wire(Some(SessionId::from("exp-on")))
5525 .expect("no duplicate handlers");
5526 let json = serde_json::to_value(&wire).unwrap();
5527 assert_eq!(json["expAssignments"], assignments);
5528
5529 let (empty_wire, _) = SessionConfig::default()
5531 .into_wire(Some(SessionId::from("exp-unset")))
5532 .expect("no duplicate handlers");
5533 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5534 assert!(empty_json.get("expAssignments").is_none());
5535 }
5536
5537 #[test]
5538 fn resume_session_config_with_exp_assignments_serializes() {
5539 let assignments = serde_json::json!({
5540 "Parameters": { "copilot_exp_flag": "treatment" },
5541 "AssignmentContext": "ctx-456",
5542 });
5543 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
5544 .with_exp_assignments(assignments.clone())
5545 .into_wire()
5546 .expect("no duplicate handlers");
5547 let json = serde_json::to_value(&wire).unwrap();
5548 assert_eq!(json["expAssignments"], assignments);
5549
5550 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
5552 .into_wire()
5553 .expect("no duplicate handlers");
5554 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5555 assert!(empty_json.get("expAssignments").is_none());
5556 }
5557
5558 #[test]
5559 fn session_config_clone_preserves_exp_assignments() {
5560 let assignments = serde_json::json!({
5561 "Parameters": { "copilot_exp_flag": "treatment" },
5562 "AssignmentContext": "ctx-clone",
5563 });
5564 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
5565 let cloned = config.clone();
5566
5567 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5568
5569 let (wire, _runtime) = cloned
5570 .into_wire(Some(SessionId::from("exp-clone")))
5571 .expect("no duplicate handlers");
5572 let json = serde_json::to_value(&wire).unwrap();
5573 assert_eq!(json["expAssignments"], assignments);
5574 }
5575
5576 #[test]
5577 fn resume_session_config_clone_preserves_exp_assignments() {
5578 let assignments = serde_json::json!({
5579 "Parameters": { "copilot_exp_flag": "treatment" },
5580 "AssignmentContext": "ctx-clone-resume",
5581 });
5582 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
5583 .with_exp_assignments(assignments.clone());
5584 let cloned = config.clone();
5585
5586 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5587
5588 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
5589 let json = serde_json::to_value(&wire).unwrap();
5590 assert_eq!(json["expAssignments"], assignments);
5591 }
5592
5593 #[test]
5594 #[allow(clippy::field_reassign_with_default)]
5595 fn session_config_into_wire_serializes_bucket_b_fields() {
5596 use std::path::PathBuf;
5597
5598 use super::{CloudSessionOptions, CloudSessionRepository};
5599
5600 let mut cfg = SessionConfig::default();
5601 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5602 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5603 cfg.github_token = Some("ghs_secret".to_string());
5604 cfg.include_sub_agent_streaming_events = Some(false);
5605 cfg.enable_session_telemetry = Some(false);
5606 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
5607 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
5608 cfg.enable_on_demand_instruction_discovery = Some(false);
5609 cfg.cloud = Some(CloudSessionOptions::with_repository(
5610 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
5611 ));
5612
5613 let (wire, _runtime) = cfg
5614 .into_wire(Some(SessionId::from("custom-id")))
5615 .expect("no duplicate handlers");
5616 let wire_json = serde_json::to_value(&wire).unwrap();
5617 assert_eq!(wire_json["sessionId"], "custom-id");
5618 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5619 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5620 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5621 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
5622 assert_eq!(wire_json["enableSessionTelemetry"], false);
5623 assert_eq!(wire_json["reasoningSummary"], "concise");
5624 assert_eq!(wire_json["remoteSession"], "export");
5625 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5626 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
5627 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
5628 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
5629
5630 let (empty_wire, _) = SessionConfig::default()
5632 .into_wire(Some(SessionId::from("empty")))
5633 .expect("default has no duplicate handlers");
5634 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5635 assert!(empty_json.get("gitHubToken").is_none());
5636 assert!(empty_json.get("enableSessionTelemetry").is_none());
5637 assert!(empty_json.get("reasoningSummary").is_none());
5638 assert!(empty_json.get("remoteSession").is_none());
5639 assert!(
5640 empty_json
5641 .get("enableOnDemandInstructionDiscovery")
5642 .is_none()
5643 );
5644 assert!(empty_json.get("cloud").is_none());
5645 }
5646
5647 #[test]
5648 fn session_config_into_wire_serializes_named_providers_and_models() {
5649 let cfg = SessionConfig::default()
5650 .with_providers(vec![
5651 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
5652 .with_provider_type("openai")
5653 .with_wire_api("responses")
5654 .with_api_key("sk-test"),
5655 ])
5656 .with_models(vec![
5657 ProviderModelConfig::new("gpt-x", "my-openai")
5658 .with_wire_model("gpt-x-2025")
5659 .with_max_output_tokens(2048),
5660 ]);
5661
5662 let (wire, _) = cfg
5663 .into_wire(Some(SessionId::from("sess-providers")))
5664 .expect("no duplicate handlers");
5665 let wire_json = serde_json::to_value(&wire).unwrap();
5666 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
5667 assert_eq!(
5668 wire_json["providers"][0]["baseUrl"],
5669 "https://api.example.com/v1"
5670 );
5671 assert_eq!(wire_json["providers"][0]["type"], "openai");
5672 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
5673 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
5674 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
5675 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
5676 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
5677 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
5678
5679 let (empty_wire, _) = SessionConfig::default()
5680 .into_wire(Some(SessionId::from("empty")))
5681 .expect("default has no duplicate handlers");
5682 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5683 assert!(empty_json.get("providers").is_none());
5684 assert!(empty_json.get("models").is_none());
5685 }
5686
5687 #[test]
5688 fn resume_config_into_wire_serializes_named_providers_and_models() {
5689 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
5690 .with_providers(vec![
5691 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
5692 .with_provider_type("azure")
5693 .with_azure(AzureProviderOptions {
5694 api_version: Some("2024-10-21".to_string()),
5695 }),
5696 ])
5697 .with_models(vec![
5698 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
5699 ]);
5700
5701 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5702 let wire_json = serde_json::to_value(&wire).unwrap();
5703 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
5704 assert_eq!(wire_json["providers"][0]["type"], "azure");
5705 assert_eq!(
5706 wire_json["providers"][0]["azure"]["apiVersion"],
5707 "2024-10-21"
5708 );
5709 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
5710 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
5711 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
5712
5713 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
5714 .into_wire()
5715 .expect("default has no duplicate handlers");
5716 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5717 assert!(empty_json.get("providers").is_none());
5718 assert!(empty_json.get("models").is_none());
5719 }
5720
5721 #[test]
5722 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
5723 use std::path::PathBuf;
5724
5725 let cfg = SessionConfig {
5726 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
5727 large_output: Some(
5728 LargeToolOutputConfig::new()
5729 .with_enabled(true)
5730 .with_max_size_bytes(1024)
5731 .with_output_directory(PathBuf::from("/tmp/large-output")),
5732 ),
5733 ..Default::default()
5734 };
5735
5736 let (wire, _) = cfg
5737 .into_wire(Some(SessionId::from("sess-1")))
5738 .expect("no duplicate handlers");
5739 let wire_json = serde_json::to_value(&wire).unwrap();
5740 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
5741 assert_eq!(wire_json["largeOutput"]["enabled"], true);
5742 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
5743 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
5744
5745 let (empty_wire, _) = SessionConfig::default()
5746 .into_wire(Some(SessionId::from("empty")))
5747 .expect("default has no duplicate handlers");
5748 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5749 assert!(empty_json.get("pluginDirectories").is_none());
5750 assert!(empty_json.get("largeOutput").is_none());
5751 }
5752
5753 #[test]
5754 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
5755 use std::path::PathBuf;
5756
5757 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5758 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5759 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5760 cfg.github_token = Some("ghs_secret".to_string());
5761 cfg.include_sub_agent_streaming_events = Some(true);
5762 cfg.enable_session_telemetry = Some(false);
5763 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
5764 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
5765 cfg.enable_on_demand_instruction_discovery = Some(false);
5766
5767 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5768 let wire_json = serde_json::to_value(&wire).unwrap();
5769 assert_eq!(wire_json["sessionId"], "sess-1");
5770 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5771 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5772 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5773 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
5774 assert_eq!(wire_json["enableSessionTelemetry"], false);
5775 assert_eq!(wire_json["reasoningSummary"], "detailed");
5776 assert_eq!(wire_json["remoteSession"], "on");
5777 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5778
5779 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5781 .into_wire()
5782 .expect("default resume has no duplicate handlers");
5783 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5784 assert!(empty_json.get("reasoningSummary").is_none());
5785 assert!(empty_json.get("remoteSession").is_none());
5786 assert!(
5787 empty_json
5788 .get("enableOnDemandInstructionDiscovery")
5789 .is_none()
5790 );
5791 }
5792
5793 #[test]
5794 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
5795 use std::path::PathBuf;
5796
5797 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5798 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
5799 cfg.large_output = Some(
5800 LargeToolOutputConfig::new()
5801 .with_enabled(false)
5802 .with_max_size_bytes(2048)
5803 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
5804 );
5805
5806 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5807 let wire_json = serde_json::to_value(&wire).unwrap();
5808 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
5809 assert_eq!(wire_json["largeOutput"]["enabled"], false);
5810 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
5811 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
5812
5813 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5814 .into_wire()
5815 .expect("default resume has no duplicate handlers");
5816 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5817 assert!(empty_json.get("pluginDirectories").is_none());
5818 assert!(empty_json.get("largeOutput").is_none());
5819 }
5820
5821 #[test]
5822 fn session_config_builder_composes() {
5823 use indexmap::IndexMap;
5824
5825 let cfg = SessionConfig::default()
5826 .with_session_id(SessionId::from("sess-1"))
5827 .with_model("claude-sonnet-4")
5828 .with_client_name("test-app")
5829 .with_reasoning_effort("medium")
5830 .with_reasoning_summary(ReasoningSummary::Concise)
5831 .with_context_tier("long_context")
5832 .with_streaming(true)
5833 .with_tools([Tool::new("greet")])
5834 .with_available_tools(["bash", "view"])
5835 .with_excluded_tools(["dangerous"])
5836 .with_mcp_servers(IndexMap::new())
5837 .with_mcp_oauth_token_storage("persistent")
5838 .with_enable_config_discovery(true)
5839 .with_enable_on_demand_instruction_discovery(true)
5840 .with_skill_directories([PathBuf::from("/tmp/skills")])
5841 .with_disabled_skills(["broken-skill"])
5842 .with_agent("researcher")
5843 .with_config_directory(PathBuf::from("/tmp/config"))
5844 .with_working_directory(PathBuf::from("/tmp/work"))
5845 .with_github_token("ghp_test")
5846 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5847 .with_enable_session_telemetry(false)
5848 .with_include_sub_agent_streaming_events(false)
5849 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5850
5851 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
5852 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
5853 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5854 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
5855 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
5856 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
5857 assert_eq!(cfg.streaming, Some(true));
5858 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5859 assert_eq!(
5860 cfg.available_tools.as_deref(),
5861 Some(&["bash".to_string(), "view".to_string()][..])
5862 );
5863 assert_eq!(
5864 cfg.excluded_tools.as_deref(),
5865 Some(&["dangerous".to_string()][..])
5866 );
5867 assert!(cfg.mcp_servers.is_some());
5868 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5869 assert_eq!(cfg.enable_config_discovery, Some(true));
5870 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
5871 assert_eq!(
5872 cfg.skill_directories.as_deref(),
5873 Some(&[PathBuf::from("/tmp/skills")][..])
5874 );
5875 assert_eq!(
5876 cfg.disabled_skills.as_deref(),
5877 Some(&["broken-skill".to_string()][..])
5878 );
5879 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5880 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5881 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5882 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5883 assert_eq!(
5884 cfg.capi,
5885 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5886 );
5887 assert_eq!(cfg.enable_session_telemetry, Some(false));
5888 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
5889 assert_eq!(
5890 cfg.extension_info,
5891 Some(ExtensionInfo::new("github-app", "counter"))
5892 );
5893 }
5894
5895 #[test]
5896 fn resume_session_config_builder_composes() {
5897 use indexmap::IndexMap;
5898
5899 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
5900 .with_client_name("test-app")
5901 .with_reasoning_summary(ReasoningSummary::None)
5902 .with_context_tier("default")
5903 .with_streaming(true)
5904 .with_tools([Tool::new("greet")])
5905 .with_available_tools(["bash", "view"])
5906 .with_excluded_tools(["dangerous"])
5907 .with_mcp_servers(IndexMap::new())
5908 .with_mcp_oauth_token_storage("persistent")
5909 .with_enable_config_discovery(true)
5910 .with_enable_on_demand_instruction_discovery(false)
5911 .with_skill_directories([PathBuf::from("/tmp/skills")])
5912 .with_disabled_skills(["broken-skill"])
5913 .with_agent("researcher")
5914 .with_config_directory(PathBuf::from("/tmp/config"))
5915 .with_working_directory(PathBuf::from("/tmp/work"))
5916 .with_github_token("ghp_test")
5917 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5918 .with_enable_session_telemetry(false)
5919 .with_include_sub_agent_streaming_events(true)
5920 .with_suppress_resume_event(true)
5921 .with_continue_pending_work(true)
5922 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5923
5924 assert_eq!(cfg.session_id.as_str(), "sess-2");
5925 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5926 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
5927 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
5928 assert_eq!(cfg.streaming, Some(true));
5929 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5930 assert_eq!(
5931 cfg.available_tools.as_deref(),
5932 Some(&["bash".to_string(), "view".to_string()][..])
5933 );
5934 assert_eq!(
5935 cfg.excluded_tools.as_deref(),
5936 Some(&["dangerous".to_string()][..])
5937 );
5938 assert!(cfg.mcp_servers.is_some());
5939 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5940 assert_eq!(cfg.enable_config_discovery, Some(true));
5941 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
5942 assert_eq!(
5943 cfg.skill_directories.as_deref(),
5944 Some(&[PathBuf::from("/tmp/skills")][..])
5945 );
5946 assert_eq!(
5947 cfg.disabled_skills.as_deref(),
5948 Some(&["broken-skill".to_string()][..])
5949 );
5950 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5951 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5952 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5953 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5954 assert_eq!(
5955 cfg.capi,
5956 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5957 );
5958 assert_eq!(cfg.enable_session_telemetry, Some(false));
5959 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
5960 assert_eq!(cfg.suppress_resume_event, Some(true));
5961 assert_eq!(cfg.continue_pending_work, Some(true));
5962 assert_eq!(
5963 cfg.extension_info,
5964 Some(ExtensionInfo::new("github-app", "counter"))
5965 );
5966 }
5967
5968 #[test]
5972 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
5973 let cfg =
5974 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
5975 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5976 let json = serde_json::to_value(&wire).unwrap();
5977 assert_eq!(json["continuePendingWork"], true);
5978
5979 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5981 .into_wire()
5982 .expect("no duplicate handlers");
5983 let json = serde_json::to_value(&wire).unwrap();
5984 assert!(json.get("continuePendingWork").is_none());
5985 }
5986
5987 #[test]
5991 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
5992 let cfg =
5993 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
5994 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5995 let json = serde_json::to_value(&wire).unwrap();
5996 assert_eq!(json["disableResume"], true);
5997 assert!(json.get("suppressResumeEvent").is_none());
5998 }
5999
6000 #[test]
6003 fn session_config_serializes_instruction_directories_to_camel_case() {
6004 let cfg =
6005 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
6006 let (wire, _) = cfg
6007 .into_wire(Some(SessionId::from("instr-on")))
6008 .expect("no duplicate handlers");
6009 let json = serde_json::to_value(&wire).unwrap();
6010 assert_eq!(
6011 json["instructionDirectories"],
6012 serde_json::json!(["/tmp/instr"])
6013 );
6014
6015 let (wire, _) = SessionConfig::default()
6017 .into_wire(Some(SessionId::from("instr-off")))
6018 .expect("no duplicate handlers");
6019 let json = serde_json::to_value(&wire).unwrap();
6020 assert!(json.get("instructionDirectories").is_none());
6021 }
6022
6023 #[test]
6026 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
6027 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
6028 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
6029 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
6030 let json = serde_json::to_value(&wire).unwrap();
6031 assert_eq!(
6032 json["instructionDirectories"],
6033 serde_json::json!(["/tmp/instr"])
6034 );
6035
6036 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
6037 .into_wire()
6038 .expect("no duplicate handlers");
6039 let json = serde_json::to_value(&wire).unwrap();
6040 assert!(json.get("instructionDirectories").is_none());
6041 }
6042
6043 #[test]
6044 fn custom_agent_config_builder_composes() {
6045 use indexmap::IndexMap;
6046
6047 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
6048 .with_display_name("Research Assistant")
6049 .with_description("Investigates technical questions.")
6050 .with_tools(["bash", "view"])
6051 .with_mcp_servers(IndexMap::new())
6052 .with_infer(true)
6053 .with_skills(["rust-coding-skill"]);
6054
6055 assert_eq!(cfg.name, "researcher");
6056 assert_eq!(cfg.prompt, "You are a research assistant.");
6057 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
6058 assert_eq!(
6059 cfg.description.as_deref(),
6060 Some("Investigates technical questions.")
6061 );
6062 assert_eq!(
6063 cfg.tools.as_deref(),
6064 Some(&["bash".to_string(), "view".to_string()][..])
6065 );
6066 assert!(cfg.mcp_servers.is_some());
6067 assert_eq!(cfg.infer, Some(true));
6068 assert_eq!(
6069 cfg.skills.as_deref(),
6070 Some(&["rust-coding-skill".to_string()][..])
6071 );
6072 }
6073
6074 #[test]
6075 fn mcp_servers_serialize_in_insertion_order() {
6076 use indexmap::IndexMap;
6077
6078 let order = [
6084 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
6085 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
6086 ];
6087 let mut servers = IndexMap::new();
6088 for name in order {
6089 servers.insert(
6090 name.to_string(),
6091 McpServerConfig::Stdio(McpStdioServerConfig {
6092 command: "run".to_string(),
6093 ..Default::default()
6094 }),
6095 );
6096 }
6097
6098 let (wire, _runtime) = SessionConfig::default()
6099 .with_mcp_servers(servers)
6100 .into_wire(None)
6101 .expect("into_wire should succeed");
6102 let json = serde_json::to_string(&wire).expect("serialize wire");
6103
6104 let positions: Vec<usize> = order
6105 .iter()
6106 .map(|name| {
6107 json.find(&format!("\"{name}\""))
6108 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
6109 })
6110 .collect();
6111 let mut ascending = positions.clone();
6112 ascending.sort_unstable();
6113 assert_eq!(
6114 positions, ascending,
6115 "mcp server keys must serialize in insertion order: {json}"
6116 );
6117 }
6118
6119 #[test]
6120 fn infinite_session_config_builder_composes() {
6121 let cfg = InfiniteSessionConfig::new()
6122 .with_enabled(true)
6123 .with_background_compaction_threshold(0.75)
6124 .with_buffer_exhaustion_threshold(0.92);
6125
6126 assert_eq!(cfg.enabled, Some(true));
6127 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
6128 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
6129 }
6130
6131 #[test]
6132 fn provider_config_builder_composes() {
6133 use std::collections::HashMap;
6134
6135 let mut headers = HashMap::new();
6136 headers.insert("X-Custom".to_string(), "value".to_string());
6137
6138 let cfg = ProviderConfig::new("https://api.example.com")
6139 .with_provider_type("openai")
6140 .with_wire_api("completions")
6141 .with_transport("websockets")
6142 .with_api_key("sk-test")
6143 .with_bearer_token("bearer-test")
6144 .with_headers(headers)
6145 .with_model_id("gpt-4")
6146 .with_wire_model("azure-gpt-4-deployment")
6147 .with_max_prompt_tokens(8192)
6148 .with_max_output_tokens(2048);
6149
6150 assert_eq!(cfg.base_url, "https://api.example.com");
6151 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
6152 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
6153 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
6154 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
6155 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
6156 assert_eq!(
6157 cfg.headers
6158 .as_ref()
6159 .and_then(|h| h.get("X-Custom"))
6160 .map(String::as_str),
6161 Some("value"),
6162 );
6163 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
6164 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
6165 assert_eq!(cfg.max_prompt_tokens, Some(8192));
6166 assert_eq!(cfg.max_output_tokens, Some(2048));
6167
6168 let wire = serde_json::to_value(&cfg).unwrap();
6170 assert_eq!(wire["modelId"], "gpt-4");
6171 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
6172 assert_eq!(wire["maxPromptTokens"], 8192);
6173 assert_eq!(wire["maxOutputTokens"], 2048);
6174
6175 let unset = ProviderConfig::new("https://api.example.com");
6176 let wire_unset = serde_json::to_value(&unset).unwrap();
6177 assert!(wire_unset.get("modelId").is_none());
6178 assert!(wire_unset.get("wireModel").is_none());
6179 assert!(wire_unset.get("maxPromptTokens").is_none());
6180 assert!(wire_unset.get("maxOutputTokens").is_none());
6181 }
6182
6183 #[test]
6184 fn capi_session_options_builder_composes_and_serializes() {
6185 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
6186
6187 assert_eq!(cfg.enable_web_socket_responses, Some(false));
6188
6189 let wire = serde_json::to_value(&cfg).unwrap();
6190 assert_eq!(
6191 wire,
6192 serde_json::json!({ "enableWebSocketResponses": false })
6193 );
6194
6195 let unset = CapiSessionOptions::new();
6196 let wire_unset = serde_json::to_value(&unset).unwrap();
6197 assert!(wire_unset.get("enableWebSocketResponses").is_none());
6198 }
6199
6200 #[test]
6201 fn session_config_with_capi_serializes() {
6202 let (wire, _) = SessionConfig::default()
6203 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6204 .into_wire(Some(SessionId::from("capi-create")))
6205 .expect("no duplicate handlers");
6206 let json = serde_json::to_value(&wire).unwrap();
6207 assert_eq!(
6208 json["capi"],
6209 serde_json::json!({ "enableWebSocketResponses": false })
6210 );
6211
6212 let (empty_wire, _) = SessionConfig::default()
6213 .into_wire(Some(SessionId::from("capi-create-unset")))
6214 .expect("no duplicate handlers");
6215 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6216 assert!(empty_json.get("capi").is_none());
6217 }
6218
6219 #[test]
6220 fn resume_session_config_with_capi_serializes() {
6221 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
6222 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6223 .into_wire()
6224 .expect("no duplicate handlers");
6225 let json = serde_json::to_value(&wire).unwrap();
6226 assert_eq!(
6227 json["capi"],
6228 serde_json::json!({ "enableWebSocketResponses": false })
6229 );
6230
6231 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
6232 .into_wire()
6233 .expect("no duplicate handlers");
6234 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6235 assert!(empty_json.get("capi").is_none());
6236 }
6237
6238 #[test]
6239 fn system_message_config_builder_composes() {
6240 use std::collections::HashMap;
6241
6242 let cfg = SystemMessageConfig::new()
6243 .with_mode("replace")
6244 .with_content("Custom system message.")
6245 .with_sections(HashMap::new());
6246
6247 assert_eq!(cfg.mode.as_deref(), Some("replace"));
6248 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
6249 assert!(cfg.sections.is_some());
6250 }
6251
6252 #[test]
6253 fn delivery_mode_serializes_to_kebab_case_strings() {
6254 assert_eq!(
6255 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
6256 "\"enqueue\""
6257 );
6258 assert_eq!(
6259 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
6260 "\"immediate\""
6261 );
6262 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
6263 assert_eq!(parsed, DeliveryMode::Immediate);
6264 }
6265
6266 #[test]
6267 fn agent_mode_serializes_to_kebab_case_strings() {
6268 assert_eq!(
6269 serde_json::to_string(&AgentMode::Interactive).unwrap(),
6270 "\"interactive\""
6271 );
6272 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
6273 assert_eq!(
6274 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
6275 "\"autopilot\""
6276 );
6277 assert_eq!(
6278 serde_json::to_string(&AgentMode::Shell).unwrap(),
6279 "\"shell\""
6280 );
6281 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
6282 assert_eq!(parsed, AgentMode::Plan);
6283 }
6284
6285 #[test]
6286 fn connection_state_distinguishes_variants() {
6287 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
6290 }
6291
6292 #[test]
6298 fn session_event_round_trips_agent_id_on_envelope() {
6299 let wire = json!({
6300 "id": "evt-1",
6301 "timestamp": "2026-04-30T12:00:00Z",
6302 "parentId": null,
6303 "agentId": "sub-agent-42",
6304 "type": "assistant.message",
6305 "data": { "message": "hi" }
6306 });
6307
6308 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
6309 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6310
6311 let roundtripped = serde_json::to_value(&event).unwrap();
6313 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6314
6315 let main_agent_event: SessionEvent = serde_json::from_value(json!({
6317 "id": "evt-2",
6318 "timestamp": "2026-04-30T12:00:01Z",
6319 "parentId": null,
6320 "type": "session.idle",
6321 "data": {}
6322 }))
6323 .unwrap();
6324 assert!(main_agent_event.agent_id.is_none());
6325 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
6326 assert!(roundtripped.get("agentId").is_none());
6327 }
6328
6329 #[test]
6331 fn typed_session_event_round_trips_agent_id_on_envelope() {
6332 let wire = json!({
6333 "id": "evt-1",
6334 "timestamp": "2026-04-30T12:00:00Z",
6335 "parentId": null,
6336 "agentId": "sub-agent-42",
6337 "type": "session.idle",
6338 "data": {}
6339 });
6340
6341 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
6342 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6343
6344 let roundtripped = serde_json::to_value(&event).unwrap();
6345 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6346 }
6347
6348 #[test]
6349 fn connection_state_variants_compile() {
6350 let _ = ConnectionState::Disconnected;
6354 let _ = ConnectionState::Connecting;
6355 let _ = ConnectionState::Connected;
6356 let _ = ConnectionState::Error;
6357 }
6358
6359 #[test]
6360 fn deserializes_runtime_attachment_variants() {
6361 let attachments: Vec<Attachment> = serde_json::from_value(json!([
6362 {
6363 "type": "file",
6364 "path": "/tmp/file.rs",
6365 "displayName": "file.rs",
6366 "lineRange": { "start": 7, "end": 12 }
6367 },
6368 {
6369 "type": "directory",
6370 "path": "/tmp/project",
6371 "displayName": "project"
6372 },
6373 {
6374 "type": "selection",
6375 "filePath": "/tmp/lib.rs",
6376 "displayName": "lib.rs",
6377 "text": "fn main() {}",
6378 "selection": {
6379 "start": { "line": 1, "character": 2 },
6380 "end": { "line": 3, "character": 4 }
6381 }
6382 },
6383 {
6384 "type": "blob",
6385 "data": "Zm9v",
6386 "mimeType": "image/png",
6387 "displayName": "image.png"
6388 },
6389 {
6390 "type": "github_reference",
6391 "number": 42,
6392 "title": "Fix rendering",
6393 "referenceType": "issue",
6394 "state": "open",
6395 "url": "https://github.com/example/repo/issues/42"
6396 }
6397 ]))
6398 .expect("attachments should deserialize");
6399
6400 assert_eq!(attachments.len(), 5);
6401 assert!(matches!(
6402 &attachments[0],
6403 Attachment::File {
6404 path,
6405 display_name,
6406 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
6407 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
6408 ));
6409 assert!(matches!(
6410 &attachments[1],
6411 Attachment::Directory { path, display_name }
6412 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
6413 ));
6414 assert!(matches!(
6415 &attachments[2],
6416 Attachment::Selection {
6417 file_path,
6418 display_name,
6419 selection:
6420 AttachmentSelectionRange {
6421 start: AttachmentSelectionPosition { line: 1, character: 2 },
6422 end: AttachmentSelectionPosition { line: 3, character: 4 },
6423 },
6424 ..
6425 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
6426 ));
6427 assert!(matches!(
6428 &attachments[3],
6429 Attachment::Blob {
6430 data,
6431 mime_type,
6432 display_name,
6433 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
6434 ));
6435 assert!(matches!(
6436 &attachments[4],
6437 Attachment::GitHubReference {
6438 number: 42,
6439 title,
6440 reference_type: GitHubReferenceType::Issue,
6441 state,
6442 url,
6443 } if title == "Fix rendering"
6444 && state == "open"
6445 && url == "https://github.com/example/repo/issues/42"
6446 ));
6447 }
6448
6449 #[test]
6450 fn ensures_display_names_for_variants_that_support_them() {
6451 let mut attachments = vec![
6452 Attachment::File {
6453 path: PathBuf::from("/tmp/file.rs"),
6454 display_name: None,
6455 line_range: None,
6456 },
6457 Attachment::Selection {
6458 file_path: PathBuf::from("/tmp/src/lib.rs"),
6459 display_name: None,
6460 text: "fn main() {}".to_string(),
6461 selection: AttachmentSelectionRange {
6462 start: AttachmentSelectionPosition {
6463 line: 0,
6464 character: 0,
6465 },
6466 end: AttachmentSelectionPosition {
6467 line: 0,
6468 character: 10,
6469 },
6470 },
6471 },
6472 Attachment::Blob {
6473 data: "Zm9v".to_string(),
6474 mime_type: "image/png".to_string(),
6475 display_name: None,
6476 },
6477 Attachment::GitHubReference {
6478 number: 7,
6479 title: "Track regressions".to_string(),
6480 reference_type: GitHubReferenceType::Issue,
6481 state: "open".to_string(),
6482 url: "https://example.com/issues/7".to_string(),
6483 },
6484 ];
6485
6486 ensure_attachment_display_names(&mut attachments);
6487
6488 assert_eq!(attachments[0].display_name(), Some("file.rs"));
6489 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
6490 assert_eq!(attachments[2].display_name(), Some("attachment"));
6491 assert_eq!(attachments[3].display_name(), None);
6492 assert_eq!(
6493 attachments[3].label(),
6494 Some("Track regressions".to_string())
6495 );
6496 }
6497
6498 #[test]
6499 fn github_anchored_attachment_variants_round_trip() {
6500 let cases = vec![
6501 (
6502 "github_commit",
6503 json!({
6504 "type": "github_commit",
6505 "message": "Fix the thing",
6506 "oid": "abc123",
6507 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
6508 "url": "https://github.com/octocat/repo/commit/abc123"
6509 }),
6510 ),
6511 (
6512 "github_release",
6513 json!({
6514 "type": "github_release",
6515 "name": "v1.2.3",
6516 "repo": { "name": "repo", "owner": "octocat" },
6517 "tagName": "v1.2.3",
6518 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
6519 }),
6520 ),
6521 (
6522 "github_actions_job",
6523 json!({
6524 "type": "github_actions_job",
6525 "conclusion": "failure",
6526 "jobId": 99,
6527 "jobName": "build",
6528 "repo": { "name": "repo", "owner": "octocat" },
6529 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
6530 "workflowName": "CI"
6531 }),
6532 ),
6533 (
6534 "github_repository",
6535 json!({
6536 "type": "github_repository",
6537 "description": "An example repository",
6538 "ref": "main",
6539 "repo": { "name": "repo", "owner": "octocat" },
6540 "url": "https://github.com/octocat/repo"
6541 }),
6542 ),
6543 (
6544 "github_file_diff",
6545 json!({
6546 "type": "github_file_diff",
6547 "base": {
6548 "path": "src/lib.rs",
6549 "ref": "main",
6550 "repo": { "name": "repo", "owner": "octocat" }
6551 },
6552 "head": {
6553 "path": "src/lib.rs",
6554 "ref": "feature",
6555 "repo": { "name": "repo", "owner": "octocat" }
6556 },
6557 "url": "https://github.com/octocat/repo/compare/main...feature"
6558 }),
6559 ),
6560 (
6561 "github_tree_comparison",
6562 json!({
6563 "type": "github_tree_comparison",
6564 "base": {
6565 "repo": { "name": "repo", "owner": "octocat" },
6566 "revision": "main"
6567 },
6568 "head": {
6569 "repo": { "name": "repo", "owner": "octocat" },
6570 "revision": "feature"
6571 },
6572 "url": "https://github.com/octocat/repo/compare/main...feature"
6573 }),
6574 ),
6575 (
6576 "github_url",
6577 json!({
6578 "type": "github_url",
6579 "url": "https://github.com/octocat/repo/wiki"
6580 }),
6581 ),
6582 (
6583 "github_file",
6584 json!({
6585 "type": "github_file",
6586 "path": "src/main.rs",
6587 "ref": "main",
6588 "repo": { "name": "repo", "owner": "octocat" },
6589 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
6590 }),
6591 ),
6592 (
6593 "github_snippet",
6594 json!({
6595 "type": "github_snippet",
6596 "lineRange": { "start": 10, "end": 20 },
6597 "path": "src/main.rs",
6598 "ref": "main",
6599 "repo": { "name": "repo", "owner": "octocat" },
6600 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
6601 }),
6602 ),
6603 ];
6604
6605 for (expected_type, input) in cases {
6606 let attachment: Attachment = serde_json::from_value(input.clone())
6607 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
6608
6609 let serialized_string = serde_json::to_string(&attachment)
6614 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
6615
6616 assert_eq!(
6618 serialized_string.matches("\"type\":").count(),
6619 1,
6620 "{expected_type} must serialize a single `type` key"
6621 );
6622
6623 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
6624 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
6625 assert_eq!(
6626 serialized.get("type").and_then(|value| value.as_str()),
6627 Some(expected_type),
6628 "{expected_type} must serialize the correct discriminator"
6629 );
6630
6631 assert_eq!(
6633 serialized, input,
6634 "{expected_type} should round-trip without data loss"
6635 );
6636 let reparsed: Attachment = serde_json::from_value(serialized)
6637 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
6638 assert_eq!(
6639 reparsed, attachment,
6640 "{expected_type} should re-deserialize to the same value"
6641 );
6642 }
6643 }
6644}
6645
6646#[cfg(test)]
6647mod permission_builder_tests {
6648 use std::sync::Arc;
6649
6650 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
6651 use crate::permission;
6652 use crate::types::{
6653 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
6654 SessionId,
6655 };
6656
6657 fn data() -> PermissionRequestData {
6658 PermissionRequestData {
6659 extra: serde_json::json!({"tool": "shell"}),
6660 ..Default::default()
6661 }
6662 }
6663
6664 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6667 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6668 }
6669
6670 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6671 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6672 }
6673
6674 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
6675 handler
6676 .handle(SessionId::from("s1"), RequestId::new("1"), data())
6677 .await
6678 }
6679
6680 #[tokio::test]
6681 async fn approve_all_with_handler_present_approves() {
6682 let cfg = SessionConfig::default()
6683 .with_permission_handler(Arc::new(ApproveAllHandler))
6684 .approve_all_permissions();
6685 let h = resolve_create(cfg).expect("policy + handler yields handler");
6686 assert!(matches!(
6687 dispatch(&h).await,
6688 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6689 ));
6690 }
6691
6692 #[tokio::test]
6693 async fn approve_all_standalone_produces_handler() {
6694 let cfg = SessionConfig::default().approve_all_permissions();
6695 let h = resolve_create(cfg).expect("policy alone yields handler");
6696 assert!(matches!(
6697 dispatch(&h).await,
6698 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6699 ));
6700 }
6701
6702 #[tokio::test]
6705 async fn approve_all_is_order_independent() {
6706 let a = SessionConfig::default()
6707 .with_permission_handler(Arc::new(ApproveAllHandler))
6708 .approve_all_permissions();
6709 let b = SessionConfig::default()
6710 .approve_all_permissions()
6711 .with_permission_handler(Arc::new(ApproveAllHandler));
6712 let ha = resolve_create(a).unwrap();
6713 let hb = resolve_create(b).unwrap();
6714 assert!(matches!(
6715 dispatch(&ha).await,
6716 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6717 ));
6718 assert!(matches!(
6719 dispatch(&hb).await,
6720 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6721 ));
6722 }
6723
6724 #[tokio::test]
6725 async fn deny_all_is_order_independent() {
6726 let a = SessionConfig::default()
6727 .with_permission_handler(Arc::new(ApproveAllHandler))
6728 .deny_all_permissions();
6729 let b = SessionConfig::default()
6730 .deny_all_permissions()
6731 .with_permission_handler(Arc::new(ApproveAllHandler));
6732 let ha = resolve_create(a).unwrap();
6733 let hb = resolve_create(b).unwrap();
6734 assert!(matches!(
6735 dispatch(&ha).await,
6736 PermissionResult::Decision(PermissionDecision::Reject(_))
6737 ));
6738 assert!(matches!(
6739 dispatch(&hb).await,
6740 PermissionResult::Decision(PermissionDecision::Reject(_))
6741 ));
6742 }
6743
6744 #[tokio::test]
6745 async fn approve_permissions_if_consults_predicate() {
6746 let cfg = SessionConfig::default().approve_permissions_if(|d| {
6747 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6748 });
6749 let h = resolve_create(cfg).unwrap();
6750 assert!(matches!(
6751 dispatch(&h).await,
6752 PermissionResult::Decision(PermissionDecision::Reject(_))
6753 ));
6754 }
6755
6756 #[tokio::test]
6757 async fn approve_permissions_if_is_order_independent() {
6758 let predicate = |d: &PermissionRequestData| {
6759 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6760 };
6761 let a = SessionConfig::default()
6762 .with_permission_handler(Arc::new(ApproveAllHandler))
6763 .approve_permissions_if(predicate);
6764 let b = SessionConfig::default()
6765 .approve_permissions_if(predicate)
6766 .with_permission_handler(Arc::new(ApproveAllHandler));
6767 let ha = resolve_create(a).unwrap();
6768 let hb = resolve_create(b).unwrap();
6769 assert!(matches!(
6770 dispatch(&ha).await,
6771 PermissionResult::Decision(PermissionDecision::Reject(_))
6772 ));
6773 assert!(matches!(
6774 dispatch(&hb).await,
6775 PermissionResult::Decision(PermissionDecision::Reject(_))
6776 ));
6777 }
6778
6779 #[tokio::test]
6780 async fn resume_session_config_approve_all_works() {
6781 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
6782 .with_permission_handler(Arc::new(ApproveAllHandler))
6783 .approve_all_permissions();
6784 let h = resolve_resume(cfg).unwrap();
6785 assert!(matches!(
6786 dispatch(&h).await,
6787 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6788 ));
6789 }
6790
6791 #[tokio::test]
6792 async fn resume_session_config_approve_all_is_order_independent() {
6793 let a = ResumeSessionConfig::new(SessionId::from("s1"))
6794 .with_permission_handler(Arc::new(ApproveAllHandler))
6795 .approve_all_permissions();
6796 let b = ResumeSessionConfig::new(SessionId::from("s1"))
6797 .approve_all_permissions()
6798 .with_permission_handler(Arc::new(ApproveAllHandler));
6799 let ha = resolve_resume(a).unwrap();
6800 let hb = resolve_resume(b).unwrap();
6801 assert!(matches!(
6802 dispatch(&ha).await,
6803 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6804 ));
6805 assert!(matches!(
6806 dispatch(&hb).await,
6807 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6808 ));
6809 }
6810}