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)]
954#[serde(tag = "type", rename_all = "lowercase")]
955#[non_exhaustive]
956pub enum McpServerConfig {
957 #[serde(alias = "local")]
961 Stdio(McpStdioServerConfig),
962 Http(McpHttpServerConfig),
964 Sse(McpHttpServerConfig),
966}
967
968#[derive(Debug, Clone, Default, Serialize, Deserialize)]
972#[serde(rename_all = "camelCase")]
973pub struct McpStdioServerConfig {
974 #[serde(default, skip_serializing_if = "Option::is_none")]
980 pub tools: Option<Vec<String>>,
981 #[serde(default, skip_serializing_if = "Option::is_none")]
983 pub timeout: Option<i64>,
984 pub command: String,
986 #[serde(default, skip_serializing_if = "Vec::is_empty")]
988 pub args: Vec<String>,
989 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
992 pub env: HashMap<String, String>,
993 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
995 pub working_directory: Option<String>,
996}
997
998#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1002#[serde(rename_all = "camelCase")]
1003pub struct McpHttpServerConfig {
1004 #[serde(default, skip_serializing_if = "Option::is_none")]
1010 pub tools: Option<Vec<String>>,
1011 #[serde(default, skip_serializing_if = "Option::is_none")]
1013 pub timeout: Option<i64>,
1014 pub url: String,
1016 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1018 pub headers: HashMap<String, String>,
1019}
1020
1021#[derive(Clone, Default, Serialize, Deserialize)]
1027#[serde(rename_all = "camelCase")]
1028#[non_exhaustive]
1029pub struct ProviderConfig {
1030 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1033 pub provider_type: Option<String>,
1034 #[serde(default, skip_serializing_if = "Option::is_none")]
1037 pub wire_api: Option<String>,
1038 #[serde(default, skip_serializing_if = "Option::is_none")]
1043 pub transport: Option<String>,
1044 pub base_url: String,
1046 #[serde(default, skip_serializing_if = "Option::is_none")]
1048 pub api_key: Option<String>,
1049 #[serde(default, skip_serializing_if = "Option::is_none")]
1053 pub bearer_token: Option<String>,
1054 #[serde(skip)]
1057 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1058 #[serde(default, skip_serializing_if = "Option::is_none")]
1059 pub(crate) has_bearer_token_provider: Option<bool>,
1060 #[serde(default, skip_serializing_if = "Option::is_none")]
1062 pub azure: Option<AzureProviderOptions>,
1063 #[serde(default, skip_serializing_if = "Option::is_none")]
1065 pub headers: Option<HashMap<String, String>>,
1066 #[serde(default, skip_serializing_if = "Option::is_none")]
1070 pub model_id: Option<String>,
1071 #[serde(default, skip_serializing_if = "Option::is_none")]
1078 pub wire_model: Option<String>,
1079 #[serde(default, skip_serializing_if = "Option::is_none")]
1084 pub max_prompt_tokens: Option<i64>,
1085 #[serde(default, skip_serializing_if = "Option::is_none")]
1088 pub max_output_tokens: Option<i64>,
1089}
1090
1091impl std::fmt::Debug for ProviderConfig {
1092 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1093 f.debug_struct("ProviderConfig")
1094 .field("provider_type", &self.provider_type)
1095 .field("wire_api", &self.wire_api)
1096 .field("transport", &self.transport)
1097 .field("base_url", &self.base_url)
1098 .field("api_key", &self.api_key)
1099 .field("bearer_token", &self.bearer_token)
1100 .field(
1101 "bearer_token_provider",
1102 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1103 )
1104 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1105 .field("azure", &self.azure)
1106 .field("headers", &self.headers)
1107 .field("model_id", &self.model_id)
1108 .field("wire_model", &self.wire_model)
1109 .field("max_prompt_tokens", &self.max_prompt_tokens)
1110 .field("max_output_tokens", &self.max_output_tokens)
1111 .finish()
1112 }
1113}
1114
1115impl ProviderConfig {
1116 pub fn new(base_url: impl Into<String>) -> Self {
1119 Self {
1120 base_url: base_url.into(),
1121 ..Self::default()
1122 }
1123 }
1124
1125 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1127 self.provider_type = Some(provider_type.into());
1128 self
1129 }
1130
1131 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1133 self.wire_api = Some(wire_api.into());
1134 self
1135 }
1136
1137 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1140 self.transport = Some(transport.into());
1141 self
1142 }
1143
1144 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1146 self.api_key = Some(api_key.into());
1147 self
1148 }
1149
1150 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1153 self.bearer_token = Some(bearer_token.into());
1154 self
1155 }
1156
1157 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1163 self.bearer_token_provider = Some(provider);
1164 self
1165 }
1166
1167 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1169 self.azure = Some(azure);
1170 self
1171 }
1172
1173 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1175 self.headers = Some(headers);
1176 self
1177 }
1178
1179 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1182 self.model_id = Some(model_id.into());
1183 self
1184 }
1185
1186 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1191 self.wire_model = Some(wire_model.into());
1192 self
1193 }
1194
1195 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1199 self.max_prompt_tokens = Some(max);
1200 self
1201 }
1202
1203 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1206 self.max_output_tokens = Some(max);
1207 self
1208 }
1209}
1210
1211#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1224#[serde(rename_all = "camelCase")]
1225#[non_exhaustive]
1226pub struct CapiSessionOptions {
1227 #[serde(default, skip_serializing_if = "Option::is_none")]
1233 pub enable_web_socket_responses: Option<bool>,
1234}
1235
1236impl CapiSessionOptions {
1237 pub fn new() -> Self {
1239 Self::default()
1240 }
1241
1242 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1244 self.enable_web_socket_responses = Some(enable);
1245 self
1246 }
1247}
1248
1249#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1251#[serde(rename_all = "camelCase")]
1252pub struct AzureProviderOptions {
1253 #[serde(default, skip_serializing_if = "Option::is_none")]
1255 pub api_version: Option<String>,
1256}
1257
1258#[derive(Clone, Default, Serialize, Deserialize)]
1269#[serde(rename_all = "camelCase")]
1270#[non_exhaustive]
1271pub struct NamedProviderConfig {
1272 pub name: String,
1275 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1278 pub provider_type: Option<String>,
1279 #[serde(default, skip_serializing_if = "Option::is_none")]
1282 pub wire_api: Option<String>,
1283 pub base_url: String,
1285 #[serde(default, skip_serializing_if = "Option::is_none")]
1287 pub api_key: Option<String>,
1288 #[serde(default, skip_serializing_if = "Option::is_none")]
1291 pub bearer_token: Option<String>,
1292 #[serde(skip)]
1295 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1296 #[serde(default, skip_serializing_if = "Option::is_none")]
1297 pub(crate) has_bearer_token_provider: Option<bool>,
1298 #[serde(default, skip_serializing_if = "Option::is_none")]
1300 pub azure: Option<AzureProviderOptions>,
1301 #[serde(default, skip_serializing_if = "Option::is_none")]
1303 pub headers: Option<HashMap<String, String>>,
1304}
1305
1306impl std::fmt::Debug for NamedProviderConfig {
1307 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1308 f.debug_struct("NamedProviderConfig")
1309 .field("name", &self.name)
1310 .field("provider_type", &self.provider_type)
1311 .field("wire_api", &self.wire_api)
1312 .field("base_url", &self.base_url)
1313 .field("api_key", &self.api_key)
1314 .field("bearer_token", &self.bearer_token)
1315 .field(
1316 "bearer_token_provider",
1317 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1318 )
1319 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1320 .field("azure", &self.azure)
1321 .field("headers", &self.headers)
1322 .finish()
1323 }
1324}
1325
1326impl NamedProviderConfig {
1327 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1330 Self {
1331 name: name.into(),
1332 base_url: base_url.into(),
1333 ..Self::default()
1334 }
1335 }
1336
1337 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1339 self.provider_type = Some(provider_type.into());
1340 self
1341 }
1342
1343 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1345 self.wire_api = Some(wire_api.into());
1346 self
1347 }
1348
1349 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1351 self.api_key = Some(api_key.into());
1352 self
1353 }
1354
1355 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1358 self.bearer_token = Some(bearer_token.into());
1359 self
1360 }
1361
1362 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1368 self.bearer_token_provider = Some(provider);
1369 self
1370 }
1371
1372 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1374 self.azure = Some(azure);
1375 self
1376 }
1377
1378 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1380 self.headers = Some(headers);
1381 self
1382 }
1383}
1384
1385fn prepare_bearer_token_providers(
1386 provider: &mut Option<ProviderConfig>,
1387 providers: &mut Option<Vec<NamedProviderConfig>>,
1388) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1389 let mut bearer_token_providers = HashMap::new();
1390
1391 if let Some(provider) = provider.as_mut()
1392 && let Some(token_provider) = provider.bearer_token_provider.take()
1393 {
1394 provider.has_bearer_token_provider = Some(true);
1395 bearer_token_providers.insert("default".to_string(), token_provider);
1396 }
1397
1398 if let Some(providers) = providers.as_mut() {
1399 for provider in providers {
1400 if let Some(token_provider) = provider.bearer_token_provider.take() {
1401 provider.has_bearer_token_provider = Some(true);
1402 bearer_token_providers.insert(provider.name.clone(), token_provider);
1403 }
1404 }
1405 }
1406
1407 bearer_token_providers
1408}
1409
1410#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1418#[serde(rename_all = "camelCase")]
1419#[non_exhaustive]
1420pub struct ProviderModelConfig {
1421 pub id: String,
1424 pub provider: String,
1426 #[serde(default, skip_serializing_if = "Option::is_none")]
1429 pub wire_model: Option<String>,
1430 #[serde(default, skip_serializing_if = "Option::is_none")]
1433 pub model_id: Option<String>,
1434 #[serde(default, skip_serializing_if = "Option::is_none")]
1436 pub name: Option<String>,
1437 #[serde(default, skip_serializing_if = "Option::is_none")]
1439 pub max_prompt_tokens: Option<i64>,
1440 #[serde(default, skip_serializing_if = "Option::is_none")]
1442 pub max_context_window_tokens: Option<i64>,
1443 #[serde(default, skip_serializing_if = "Option::is_none")]
1445 pub max_output_tokens: Option<i64>,
1446 #[serde(default, skip_serializing_if = "Option::is_none")]
1449 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1450}
1451
1452impl ProviderModelConfig {
1453 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1456 Self {
1457 id: id.into(),
1458 provider: provider.into(),
1459 ..Self::default()
1460 }
1461 }
1462
1463 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1465 self.wire_model = Some(wire_model.into());
1466 self
1467 }
1468
1469 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1472 self.model_id = Some(model_id.into());
1473 self
1474 }
1475
1476 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1478 self.name = Some(name.into());
1479 self
1480 }
1481
1482 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1484 self.max_prompt_tokens = Some(max);
1485 self
1486 }
1487
1488 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1490 self.max_context_window_tokens = Some(max);
1491 self
1492 }
1493
1494 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1496 self.max_output_tokens = Some(max);
1497 self
1498 }
1499
1500 pub fn with_capabilities(
1502 mut self,
1503 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1504 ) -> Self {
1505 self.capabilities = Some(capabilities);
1506 self
1507 }
1508}
1509
1510#[derive(Clone)]
1562#[non_exhaustive]
1563pub struct SessionConfig {
1564 pub session_id: Option<SessionId>,
1566 pub model: Option<String>,
1568 pub client_name: Option<String>,
1570 pub reasoning_effort: Option<String>,
1572 pub reasoning_summary: Option<ReasoningSummary>,
1576 pub context_tier: Option<String>,
1579 pub streaming: Option<bool>,
1581 pub system_message: Option<SystemMessageConfig>,
1583 pub tools: Option<Vec<Tool>>,
1585 pub canvases: Option<Vec<CanvasDeclaration>>,
1587 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1592 pub request_canvas_renderer: Option<bool>,
1594 pub request_extensions: Option<bool>,
1596 pub extension_sdk_path: Option<String>,
1600 pub extension_info: Option<ExtensionInfo>,
1602 pub available_tools: Option<Vec<String>>,
1604 pub excluded_tools: Option<Vec<String>>,
1606 pub excluded_builtin_agents: Option<Vec<String>>,
1612 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
1614 pub mcp_oauth_token_storage: Option<String>,
1623 pub enable_config_discovery: Option<bool>,
1625 pub skip_embedding_retrieval: Option<bool>,
1627 pub embedding_cache_storage: Option<String>,
1630 pub organization_custom_instructions: Option<String>,
1632 pub enable_on_demand_instruction_discovery: Option<bool>,
1634 pub enable_file_hooks: Option<bool>,
1636 pub enable_host_git_operations: Option<bool>,
1638 pub enable_session_store: Option<bool>,
1640 pub enable_skills: Option<bool>,
1642 pub enable_mcp_apps: Option<bool>,
1669 pub skill_directories: Option<Vec<PathBuf>>,
1671 pub instruction_directories: Option<Vec<PathBuf>>,
1674 pub plugin_directories: Option<Vec<PathBuf>>,
1676 pub large_output: Option<LargeToolOutputConfig>,
1678 pub disabled_skills: Option<Vec<String>>,
1681 pub hooks: Option<bool>,
1685 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1687 pub default_agent: Option<DefaultAgentConfig>,
1691 pub agent: Option<String>,
1694 pub infinite_sessions: Option<InfiniteSessionConfig>,
1697 pub provider: Option<ProviderConfig>,
1701 pub capi: Option<CapiSessionOptions>,
1707 pub providers: Option<Vec<NamedProviderConfig>>,
1714 pub models: Option<Vec<ProviderModelConfig>>,
1720 pub enable_session_telemetry: Option<bool>,
1728 pub enable_citations: Option<bool>,
1730 pub session_limits: Option<SessionLimitsConfig>,
1732 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1735 pub memory: Option<MemoryConfiguration>,
1737 pub config_directory: Option<PathBuf>,
1740 pub working_directory: Option<PathBuf>,
1743 pub github_token: Option<String>,
1749 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
1755 pub cloud: Option<CloudSessionOptions>,
1758 pub include_sub_agent_streaming_events: Option<bool>,
1762 pub commands: Option<Vec<CommandDefinition>>,
1766 #[doc(hidden)]
1773 pub exp_assignments: Option<Value>,
1774 pub enable_managed_settings: Option<bool>,
1781 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1786 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
1790 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
1793 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
1796 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
1800 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
1803 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
1806 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1810 pub(crate) permission_policy: Option<crate::permission::Policy>,
1814 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
1819 pub skip_custom_instructions: Option<bool>,
1823 pub custom_agents_local_only: Option<bool>,
1827 pub coauthor_enabled: Option<bool>,
1831 pub manage_schedule_enabled: Option<bool>,
1835}
1836
1837impl std::fmt::Debug for SessionConfig {
1838 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1839 f.debug_struct("SessionConfig")
1840 .field("session_id", &self.session_id)
1841 .field("model", &self.model)
1842 .field("client_name", &self.client_name)
1843 .field("reasoning_effort", &self.reasoning_effort)
1844 .field("reasoning_summary", &self.reasoning_summary)
1845 .field("context_tier", &self.context_tier)
1846 .field("streaming", &self.streaming)
1847 .field("system_message", &self.system_message)
1848 .field("tools", &self.tools)
1849 .field("canvases", &self.canvases)
1850 .field(
1851 "canvas_handler",
1852 &self.canvas_handler.as_ref().map(|_| "<set>"),
1853 )
1854 .field("request_canvas_renderer", &self.request_canvas_renderer)
1855 .field("request_extensions", &self.request_extensions)
1856 .field("extension_sdk_path", &self.extension_sdk_path)
1857 .field("extension_info", &self.extension_info)
1858 .field("available_tools", &self.available_tools)
1859 .field("excluded_tools", &self.excluded_tools)
1860 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
1861 .field("mcp_servers", &self.mcp_servers)
1862 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
1863 .field("embedding_cache_storage", &self.embedding_cache_storage)
1864 .field("enable_config_discovery", &self.enable_config_discovery)
1865 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
1866 .field(
1867 "organization_custom_instructions",
1868 &self
1869 .organization_custom_instructions
1870 .as_ref()
1871 .map(|_| "<redacted>"),
1872 )
1873 .field(
1874 "enable_on_demand_instruction_discovery",
1875 &self.enable_on_demand_instruction_discovery,
1876 )
1877 .field("enable_file_hooks", &self.enable_file_hooks)
1878 .field(
1879 "enable_host_git_operations",
1880 &self.enable_host_git_operations,
1881 )
1882 .field("enable_session_store", &self.enable_session_store)
1883 .field("enable_skills", &self.enable_skills)
1884 .field("enable_mcp_apps", &self.enable_mcp_apps)
1885 .field("skill_directories", &self.skill_directories)
1886 .field("instruction_directories", &self.instruction_directories)
1887 .field("plugin_directories", &self.plugin_directories)
1888 .field("large_output", &self.large_output)
1889 .field("disabled_skills", &self.disabled_skills)
1890 .field("hooks", &self.hooks)
1891 .field("custom_agents", &self.custom_agents)
1892 .field("default_agent", &self.default_agent)
1893 .field("agent", &self.agent)
1894 .field("infinite_sessions", &self.infinite_sessions)
1895 .field("provider", &self.provider)
1896 .field("capi", &self.capi)
1897 .field("enable_session_telemetry", &self.enable_session_telemetry)
1898 .field("enable_citations", &self.enable_citations)
1899 .field("session_limits", &self.session_limits)
1900 .field("model_capabilities", &self.model_capabilities)
1901 .field("memory", &self.memory)
1902 .field("config_directory", &self.config_directory)
1903 .field("working_directory", &self.working_directory)
1904 .field(
1905 "github_token",
1906 &self.github_token.as_ref().map(|_| "<redacted>"),
1907 )
1908 .field("remote_session", &self.remote_session)
1909 .field("cloud", &self.cloud)
1910 .field(
1911 "include_sub_agent_streaming_events",
1912 &self.include_sub_agent_streaming_events,
1913 )
1914 .field("commands", &self.commands)
1915 .field("exp_assignments", &self.exp_assignments)
1916 .field("enable_managed_settings", &self.enable_managed_settings)
1917 .field(
1918 "session_fs_provider",
1919 &self.session_fs_provider.as_ref().map(|_| "<set>"),
1920 )
1921 .field(
1922 "permission_handler",
1923 &self.permission_handler.as_ref().map(|_| "<set>"),
1924 )
1925 .field(
1926 "elicitation_handler",
1927 &self.elicitation_handler.as_ref().map(|_| "<set>"),
1928 )
1929 .field(
1930 "mcp_auth_handler",
1931 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
1932 )
1933 .field(
1934 "user_input_handler",
1935 &self.user_input_handler.as_ref().map(|_| "<set>"),
1936 )
1937 .field(
1938 "exit_plan_mode_handler",
1939 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
1940 )
1941 .field(
1942 "auto_mode_switch_handler",
1943 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
1944 )
1945 .field(
1946 "hooks_handler",
1947 &self.hooks_handler.as_ref().map(|_| "<set>"),
1948 )
1949 .field(
1950 "system_message_transform",
1951 &self.system_message_transform.as_ref().map(|_| "<set>"),
1952 )
1953 .finish()
1954 }
1955}
1956
1957impl Default for SessionConfig {
1958 fn default() -> Self {
1964 Self {
1965 session_id: None,
1966 model: None,
1967 client_name: None,
1968 reasoning_effort: None,
1969 reasoning_summary: None,
1970 context_tier: None,
1971 streaming: None,
1972 system_message: None,
1973 tools: None,
1974 canvases: None,
1975 canvas_handler: None,
1976 request_canvas_renderer: None,
1977 request_extensions: None,
1978 extension_sdk_path: None,
1979 extension_info: None,
1980 available_tools: None,
1981 excluded_tools: None,
1982 excluded_builtin_agents: None,
1983 mcp_servers: None,
1984 mcp_oauth_token_storage: None,
1985 enable_config_discovery: None,
1986 skip_embedding_retrieval: None,
1987 organization_custom_instructions: None,
1988 enable_on_demand_instruction_discovery: None,
1989 enable_file_hooks: None,
1990 enable_host_git_operations: None,
1991 enable_session_store: None,
1992 enable_skills: None,
1993 embedding_cache_storage: None,
1994 enable_mcp_apps: None,
1995 skill_directories: None,
1996 instruction_directories: None,
1997 plugin_directories: None,
1998 large_output: None,
1999 disabled_skills: None,
2000 hooks: None,
2001 custom_agents: None,
2002 default_agent: None,
2003 agent: None,
2004 infinite_sessions: None,
2005 provider: None,
2006 capi: None,
2007 providers: None,
2008 models: None,
2009 enable_session_telemetry: None,
2010 enable_citations: None,
2011 session_limits: None,
2012 model_capabilities: None,
2013 memory: None,
2014 config_directory: None,
2015 working_directory: None,
2016 github_token: None,
2017 remote_session: None,
2018 cloud: None,
2019 include_sub_agent_streaming_events: None,
2020 commands: None,
2021 exp_assignments: None,
2022 enable_managed_settings: None,
2023 session_fs_provider: None,
2024 permission_handler: None,
2025 elicitation_handler: None,
2026 mcp_auth_handler: None,
2027 user_input_handler: None,
2028 exit_plan_mode_handler: None,
2029 auto_mode_switch_handler: None,
2030 hooks_handler: None,
2031 permission_policy: None,
2032 system_message_transform: None,
2033 skip_custom_instructions: None,
2034 custom_agents_local_only: None,
2035 coauthor_enabled: None,
2036 manage_schedule_enabled: None,
2037 }
2038 }
2039}
2040
2041pub(crate) struct SessionConfigRuntime {
2047 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2048 pub permission_policy: Option<crate::permission::Policy>,
2049 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2050 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2051 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2052 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2053 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2054 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2055 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2056 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2057 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2058 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2059 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2060 pub commands: Option<Vec<CommandDefinition>>,
2061}
2062
2063impl SessionConfig {
2064 pub(crate) fn into_wire(
2076 mut self,
2077 session_id: Option<SessionId>,
2078 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2079 let permission_active =
2080 self.permission_handler.is_some() || self.permission_policy.is_some();
2081 let request_user_input = self.user_input_handler.is_some();
2082 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2083 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2084 let request_elicitation = self.elicitation_handler.is_some();
2085 let hooks_flag = self.hooks_handler.is_some();
2086
2087 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2088 if let Some(tools) = self.tools.as_mut() {
2089 for tool in tools.iter_mut() {
2090 if let Some(handler) = tool.handler.take()
2091 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2092 {
2093 return Err(crate::Error::with_message(
2094 crate::ErrorKind::InvalidConfig,
2095 format!("duplicate tool handler registered for name {:?}", tool.name),
2096 ));
2097 }
2098 }
2099 }
2100
2101 let wire_commands = self.commands.as_ref().map(|cmds| {
2102 cmds.iter()
2103 .map(|c| crate::wire::CommandWireDefinition {
2104 name: c.name.clone(),
2105 description: c.description.clone(),
2106 })
2107 .collect()
2108 });
2109 let wire_canvases = self.canvases.clone();
2110 let canvas_handler = self.canvas_handler.clone();
2111 let bearer_token_providers =
2112 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2113
2114 let wire = crate::wire::SessionCreateWire {
2115 session_id,
2116 model: self.model,
2117 client_name: self.client_name,
2118 reasoning_effort: self.reasoning_effort,
2119 reasoning_summary: self.reasoning_summary,
2120 context_tier: self.context_tier,
2121 streaming: self.streaming,
2122 system_message: self.system_message,
2123 tools: self.tools,
2124 canvases: wire_canvases,
2125 request_canvas_renderer: self.request_canvas_renderer,
2126 request_extensions: self.request_extensions,
2127 extension_sdk_path: self.extension_sdk_path,
2128 extension_info: self.extension_info,
2129 available_tools: self.available_tools,
2130 excluded_tools: self.excluded_tools,
2131 excluded_builtin_agents: self.excluded_builtin_agents,
2132 tool_filter_precedence: "excluded",
2133 mcp_servers: self.mcp_servers,
2134 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2135 embedding_cache_storage: self.embedding_cache_storage,
2136 env_value_mode: "direct",
2137 enable_config_discovery: self.enable_config_discovery,
2138 skip_embedding_retrieval: self.skip_embedding_retrieval,
2139 organization_custom_instructions: self.organization_custom_instructions,
2140 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2141 enable_file_hooks: self.enable_file_hooks,
2142 enable_host_git_operations: self.enable_host_git_operations,
2143 enable_session_store: self.enable_session_store,
2144 enable_skills: self.enable_skills,
2145 request_user_input,
2146 request_permission: permission_active,
2147 request_exit_plan_mode,
2148 request_auto_mode_switch,
2149 request_elicitation,
2150 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2151 hooks: hooks_flag,
2152 skill_directories: self.skill_directories,
2153 instruction_directories: self.instruction_directories,
2154 plugin_directories: self.plugin_directories,
2155 large_output: self.large_output,
2156 disabled_skills: self.disabled_skills,
2157 custom_agents: self.custom_agents,
2158 default_agent: self.default_agent,
2159 agent: self.agent,
2160 infinite_sessions: self.infinite_sessions,
2161 provider: self.provider,
2162 capi: self.capi,
2163 providers: self.providers,
2164 models: self.models,
2165 enable_session_telemetry: self.enable_session_telemetry,
2166 enable_citations: self.enable_citations,
2167 session_limits: self.session_limits,
2168 model_capabilities: self.model_capabilities,
2169 memory: self.memory,
2170 config_dir: self.config_directory,
2171 working_directory: self.working_directory,
2172 github_token: self.github_token,
2173 remote_session: self.remote_session,
2174 cloud: self.cloud,
2175 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2176 enable_github_telemetry_forwarding: None,
2177 commands: wire_commands,
2178 exp_assignments: self.exp_assignments,
2179 enable_managed_settings: self.enable_managed_settings,
2180 };
2181
2182 let runtime = SessionConfigRuntime {
2183 permission_handler: self.permission_handler,
2184 permission_policy: self.permission_policy,
2185 elicitation_handler: self.elicitation_handler,
2186 mcp_auth_handler: self.mcp_auth_handler,
2187 user_input_handler: self.user_input_handler,
2188 exit_plan_mode_handler: self.exit_plan_mode_handler,
2189 auto_mode_switch_handler: self.auto_mode_switch_handler,
2190 hooks_handler: self.hooks_handler,
2191 system_message_transform: self.system_message_transform,
2192 tool_handlers,
2193 canvas_handler,
2194 session_fs_provider: self.session_fs_provider,
2195 bearer_token_providers,
2196 commands: self.commands,
2197 };
2198
2199 Ok((wire, runtime))
2200 }
2201
2202 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2206 self.permission_handler = Some(handler);
2207 self
2208 }
2209
2210 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2213 self.elicitation_handler = Some(handler);
2214 self
2215 }
2216
2217 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2219 self.mcp_auth_handler = Some(handler);
2220 self
2221 }
2222
2223 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2226 self.user_input_handler = Some(handler);
2227 self
2228 }
2229
2230 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2232 self.exit_plan_mode_handler = Some(handler);
2233 self
2234 }
2235
2236 pub fn with_auto_mode_switch_handler(
2238 mut self,
2239 handler: Arc<dyn AutoModeSwitchHandler>,
2240 ) -> Self {
2241 self.auto_mode_switch_handler = Some(handler);
2242 self
2243 }
2244
2245 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2250 self.commands = Some(commands);
2251 self
2252 }
2253
2254 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2258 self.session_fs_provider = Some(provider);
2259 self
2260 }
2261
2262 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2265 self.hooks_handler = Some(hooks);
2266 self
2267 }
2268
2269 pub fn with_system_message_transform(
2273 mut self,
2274 transform: Arc<dyn SystemMessageTransform>,
2275 ) -> Self {
2276 self.system_message_transform = Some(transform);
2277 self
2278 }
2279
2280 pub fn approve_all_permissions(mut self) -> Self {
2286 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2287 self
2288 }
2289
2290 pub fn deny_all_permissions(mut self) -> Self {
2293 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2294 self
2295 }
2296
2297 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2302 where
2303 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2304 {
2305 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2306 self
2307 }
2308
2309 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2311 self.session_id = Some(id.into());
2312 self
2313 }
2314
2315 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2317 self.model = Some(model.into());
2318 self
2319 }
2320
2321 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2323 self.client_name = Some(name.into());
2324 self
2325 }
2326
2327 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2329 self.reasoning_effort = Some(effort.into());
2330 self
2331 }
2332
2333 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2335 self.reasoning_summary = Some(summary);
2336 self
2337 }
2338
2339 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2341 self.context_tier = Some(tier.into());
2342 self
2343 }
2344
2345 pub fn with_streaming(mut self, streaming: bool) -> Self {
2347 self.streaming = Some(streaming);
2348 self
2349 }
2350
2351 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2353 self.system_message = Some(system_message);
2354 self
2355 }
2356
2357 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2359 self.tools = Some(tools.into_iter().collect());
2360 self
2361 }
2362
2363 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2368 self.canvases = Some(canvases.into_iter().collect());
2369 self
2370 }
2371
2372 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2374 self.canvas_handler = Some(handler);
2375 self
2376 }
2377
2378 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2380 self.request_canvas_renderer = Some(request);
2381 self
2382 }
2383
2384 pub fn with_request_extensions(mut self, request: bool) -> Self {
2386 self.request_extensions = Some(request);
2387 self
2388 }
2389
2390 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2394 self.extension_sdk_path = Some(path.into());
2395 self
2396 }
2397
2398 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2400 self.extension_info = Some(extension_info);
2401 self
2402 }
2403
2404 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2406 where
2407 I: IntoIterator<Item = S>,
2408 S: Into<String>,
2409 {
2410 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2411 self
2412 }
2413
2414 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2416 where
2417 I: IntoIterator<Item = S>,
2418 S: Into<String>,
2419 {
2420 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2421 self
2422 }
2423
2424 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2426 where
2427 I: IntoIterator<Item = S>,
2428 S: Into<String>,
2429 {
2430 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2431 self
2432 }
2433
2434 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
2436 self.mcp_servers = Some(servers);
2437 self
2438 }
2439
2440 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2448 self.mcp_oauth_token_storage = Some(mode.into());
2449 self
2450 }
2451
2452 pub fn with_embedding_cache_storage(
2454 mut self,
2455 embedding_cache_storage: impl Into<String>,
2456 ) -> Self {
2457 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2458 self
2459 }
2460
2461 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2463 self.enable_config_discovery = Some(enable);
2464 self
2465 }
2466
2467 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2469 self.skip_embedding_retrieval = Some(value);
2470 self
2471 }
2472
2473 pub fn with_organization_custom_instructions(
2475 mut self,
2476 instructions: impl Into<String>,
2477 ) -> Self {
2478 self.organization_custom_instructions = Some(instructions.into());
2479 self
2480 }
2481
2482 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2484 self.enable_on_demand_instruction_discovery = Some(value);
2485 self
2486 }
2487
2488 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2490 self.enable_file_hooks = Some(value);
2491 self
2492 }
2493
2494 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2496 self.enable_host_git_operations = Some(value);
2497 self
2498 }
2499
2500 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2502 self.enable_session_store = Some(value);
2503 self
2504 }
2505
2506 pub fn with_enable_skills(mut self, value: bool) -> Self {
2508 self.enable_skills = Some(value);
2509 self
2510 }
2511
2512 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2518 self.enable_mcp_apps = Some(enable);
2519 self
2520 }
2521
2522 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2524 where
2525 I: IntoIterator<Item = P>,
2526 P: Into<PathBuf>,
2527 {
2528 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2529 self
2530 }
2531
2532 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2536 where
2537 I: IntoIterator<Item = P>,
2538 P: Into<PathBuf>,
2539 {
2540 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2541 self
2542 }
2543
2544 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2546 where
2547 I: IntoIterator<Item = P>,
2548 P: Into<PathBuf>,
2549 {
2550 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2551 self
2552 }
2553
2554 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2556 self.large_output = Some(config);
2557 self
2558 }
2559
2560 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2562 where
2563 I: IntoIterator<Item = S>,
2564 S: Into<String>,
2565 {
2566 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2567 self
2568 }
2569
2570 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2572 mut self,
2573 agents: I,
2574 ) -> Self {
2575 self.custom_agents = Some(agents.into_iter().collect());
2576 self
2577 }
2578
2579 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2581 self.default_agent = Some(agent);
2582 self
2583 }
2584
2585 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2588 self.agent = Some(name.into());
2589 self
2590 }
2591
2592 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2595 self.infinite_sessions = Some(config);
2596 self
2597 }
2598
2599 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2601 self.provider = Some(provider);
2602 self
2603 }
2604
2605 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
2607 self.capi = Some(capi);
2608 self
2609 }
2610
2611 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
2617 self.providers = Some(providers);
2618 self
2619 }
2620
2621 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
2627 self.models = Some(models);
2628 self
2629 }
2630
2631 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2635 self.enable_session_telemetry = Some(enable);
2636 self
2637 }
2638
2639 pub fn with_enable_citations(mut self, enable: bool) -> Self {
2641 self.enable_citations = Some(enable);
2642 self
2643 }
2644
2645 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
2647 self.session_limits = Some(limits);
2648 self
2649 }
2650
2651 pub fn with_model_capabilities(
2653 mut self,
2654 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2655 ) -> Self {
2656 self.model_capabilities = Some(capabilities);
2657 self
2658 }
2659
2660 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
2662 self.memory = Some(memory);
2663 self
2664 }
2665
2666 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2668 self.config_directory = Some(dir.into());
2669 self
2670 }
2671
2672 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2675 self.working_directory = Some(dir.into());
2676 self
2677 }
2678
2679 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2684 self.github_token = Some(token.into());
2685 self
2686 }
2687
2688 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2691 self.include_sub_agent_streaming_events = Some(include);
2692 self
2693 }
2694
2695 pub fn with_remote_session(
2697 mut self,
2698 mode: crate::generated::api_types::RemoteSessionMode,
2699 ) -> Self {
2700 self.remote_session = Some(mode);
2701 self
2702 }
2703
2704 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
2706 self.cloud = Some(cloud);
2707 self
2708 }
2709
2710 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
2712 self.skip_custom_instructions = Some(value);
2713 self
2714 }
2715
2716 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
2718 self.custom_agents_local_only = Some(value);
2719 self
2720 }
2721
2722 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
2724 self.coauthor_enabled = Some(value);
2725 self
2726 }
2727
2728 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
2730 self.manage_schedule_enabled = Some(value);
2731 self
2732 }
2733
2734 #[doc(hidden)]
2742 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
2743 self.exp_assignments = Some(assignments);
2744 self
2745 }
2746
2747 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
2753 self.enable_managed_settings = Some(enabled);
2754 self
2755 }
2756}
2757#[derive(Clone)]
2764#[non_exhaustive]
2765pub struct ResumeSessionConfig {
2766 pub session_id: SessionId,
2768 pub model: Option<String>,
2771 pub client_name: Option<String>,
2773 pub reasoning_effort: Option<String>,
2775 pub reasoning_summary: Option<ReasoningSummary>,
2779 pub context_tier: Option<String>,
2782 pub streaming: Option<bool>,
2784 pub system_message: Option<SystemMessageConfig>,
2787 pub tools: Option<Vec<Tool>>,
2789 pub canvases: Option<Vec<CanvasDeclaration>>,
2791 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2794 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
2796 pub request_canvas_renderer: Option<bool>,
2798 pub request_extensions: Option<bool>,
2800 pub extension_sdk_path: Option<String>,
2804 pub extension_info: Option<ExtensionInfo>,
2806 pub available_tools: Option<Vec<String>>,
2808 pub excluded_tools: Option<Vec<String>>,
2810 pub excluded_builtin_agents: Option<Vec<String>>,
2816 pub mcp_servers: Option<IndexMap<String, McpServerConfig>>,
2818 pub mcp_oauth_token_storage: Option<String>,
2821 pub enable_config_discovery: Option<bool>,
2823 pub skip_embedding_retrieval: Option<bool>,
2825 pub embedding_cache_storage: Option<String>,
2827 pub organization_custom_instructions: Option<String>,
2829 pub enable_on_demand_instruction_discovery: Option<bool>,
2831 pub enable_file_hooks: Option<bool>,
2833 pub enable_host_git_operations: Option<bool>,
2835 pub enable_session_store: Option<bool>,
2837 pub enable_skills: Option<bool>,
2839 pub enable_mcp_apps: Option<bool>,
2845 pub skill_directories: Option<Vec<PathBuf>>,
2847 pub instruction_directories: Option<Vec<PathBuf>>,
2850 pub plugin_directories: Option<Vec<PathBuf>>,
2852 pub large_output: Option<LargeToolOutputConfig>,
2854 pub disabled_skills: Option<Vec<String>>,
2856 pub hooks: Option<bool>,
2858 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2860 pub default_agent: Option<DefaultAgentConfig>,
2862 pub agent: Option<String>,
2864 pub infinite_sessions: Option<InfiniteSessionConfig>,
2866 pub provider: Option<ProviderConfig>,
2868 pub capi: Option<CapiSessionOptions>,
2874 pub providers: Option<Vec<NamedProviderConfig>>,
2880 pub models: Option<Vec<ProviderModelConfig>>,
2886 pub enable_session_telemetry: Option<bool>,
2894 pub enable_citations: Option<bool>,
2896 pub session_limits: Option<SessionLimitsConfig>,
2898 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2900 pub memory: Option<MemoryConfiguration>,
2902 pub config_directory: Option<PathBuf>,
2904 pub working_directory: Option<PathBuf>,
2906 pub github_token: Option<String>,
2909 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2912 pub include_sub_agent_streaming_events: Option<bool>,
2914 pub commands: Option<Vec<CommandDefinition>>,
2918 #[doc(hidden)]
2923 pub exp_assignments: Option<Value>,
2924 pub enable_managed_settings: Option<bool>,
2930 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2935 pub suppress_resume_event: Option<bool>,
2938 pub continue_pending_work: Option<bool>,
2946 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2949 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2952 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2954 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2957 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2960 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2963 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2965 pub(crate) permission_policy: Option<crate::permission::Policy>,
2967 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2969 pub skip_custom_instructions: Option<bool>,
2971 pub custom_agents_local_only: Option<bool>,
2973 pub coauthor_enabled: Option<bool>,
2975 pub manage_schedule_enabled: Option<bool>,
2977}
2978
2979impl std::fmt::Debug for ResumeSessionConfig {
2980 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2981 f.debug_struct("ResumeSessionConfig")
2982 .field("session_id", &self.session_id)
2983 .field("model", &self.model)
2984 .field("client_name", &self.client_name)
2985 .field("reasoning_effort", &self.reasoning_effort)
2986 .field("reasoning_summary", &self.reasoning_summary)
2987 .field("context_tier", &self.context_tier)
2988 .field("streaming", &self.streaming)
2989 .field("system_message", &self.system_message)
2990 .field("tools", &self.tools)
2991 .field("canvases", &self.canvases)
2992 .field(
2993 "canvas_handler",
2994 &self.canvas_handler.as_ref().map(|_| "<set>"),
2995 )
2996 .field("open_canvases", &self.open_canvases)
2997 .field("request_canvas_renderer", &self.request_canvas_renderer)
2998 .field("request_extensions", &self.request_extensions)
2999 .field("extension_sdk_path", &self.extension_sdk_path)
3000 .field("extension_info", &self.extension_info)
3001 .field("available_tools", &self.available_tools)
3002 .field("excluded_tools", &self.excluded_tools)
3003 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
3004 .field("mcp_servers", &self.mcp_servers)
3005 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
3006 .field("embedding_cache_storage", &self.embedding_cache_storage)
3007 .field("enable_config_discovery", &self.enable_config_discovery)
3008 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
3009 .field(
3010 "organization_custom_instructions",
3011 &self
3012 .organization_custom_instructions
3013 .as_ref()
3014 .map(|_| "<redacted>"),
3015 )
3016 .field(
3017 "enable_on_demand_instruction_discovery",
3018 &self.enable_on_demand_instruction_discovery,
3019 )
3020 .field("enable_file_hooks", &self.enable_file_hooks)
3021 .field(
3022 "enable_host_git_operations",
3023 &self.enable_host_git_operations,
3024 )
3025 .field("enable_session_store", &self.enable_session_store)
3026 .field("enable_skills", &self.enable_skills)
3027 .field("enable_mcp_apps", &self.enable_mcp_apps)
3028 .field("skill_directories", &self.skill_directories)
3029 .field("instruction_directories", &self.instruction_directories)
3030 .field("plugin_directories", &self.plugin_directories)
3031 .field("large_output", &self.large_output)
3032 .field("disabled_skills", &self.disabled_skills)
3033 .field("hooks", &self.hooks)
3034 .field("custom_agents", &self.custom_agents)
3035 .field("default_agent", &self.default_agent)
3036 .field("agent", &self.agent)
3037 .field("infinite_sessions", &self.infinite_sessions)
3038 .field("provider", &self.provider)
3039 .field("capi", &self.capi)
3040 .field("enable_session_telemetry", &self.enable_session_telemetry)
3041 .field("enable_citations", &self.enable_citations)
3042 .field("session_limits", &self.session_limits)
3043 .field("model_capabilities", &self.model_capabilities)
3044 .field("memory", &self.memory)
3045 .field("config_directory", &self.config_directory)
3046 .field("working_directory", &self.working_directory)
3047 .field(
3048 "github_token",
3049 &self.github_token.as_ref().map(|_| "<redacted>"),
3050 )
3051 .field("remote_session", &self.remote_session)
3052 .field(
3053 "include_sub_agent_streaming_events",
3054 &self.include_sub_agent_streaming_events,
3055 )
3056 .field("commands", &self.commands)
3057 .field("exp_assignments", &self.exp_assignments)
3058 .field("enable_managed_settings", &self.enable_managed_settings)
3059 .field(
3060 "session_fs_provider",
3061 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3062 )
3063 .field(
3064 "permission_handler",
3065 &self.permission_handler.as_ref().map(|_| "<set>"),
3066 )
3067 .field(
3068 "elicitation_handler",
3069 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3070 )
3071 .field(
3072 "user_input_handler",
3073 &self.user_input_handler.as_ref().map(|_| "<set>"),
3074 )
3075 .field(
3076 "exit_plan_mode_handler",
3077 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3078 )
3079 .field(
3080 "auto_mode_switch_handler",
3081 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3082 )
3083 .field(
3084 "hooks_handler",
3085 &self.hooks_handler.as_ref().map(|_| "<set>"),
3086 )
3087 .field(
3088 "system_message_transform",
3089 &self.system_message_transform.as_ref().map(|_| "<set>"),
3090 )
3091 .field("suppress_resume_event", &self.suppress_resume_event)
3092 .field("continue_pending_work", &self.continue_pending_work)
3093 .finish()
3094 }
3095}
3096
3097impl ResumeSessionConfig {
3098 pub(crate) fn into_wire(
3106 mut self,
3107 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3108 let permission_active =
3109 self.permission_handler.is_some() || self.permission_policy.is_some();
3110 let request_user_input = self.user_input_handler.is_some();
3111 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3112 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3113 let request_elicitation = self.elicitation_handler.is_some();
3114 let hooks_flag = self.hooks_handler.is_some();
3115
3116 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3117 if let Some(tools) = self.tools.as_mut() {
3118 for tool in tools.iter_mut() {
3119 if let Some(handler) = tool.handler.take()
3120 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3121 {
3122 return Err(crate::Error::with_message(
3123 crate::ErrorKind::InvalidConfig,
3124 format!("duplicate tool handler registered for name {:?}", tool.name),
3125 ));
3126 }
3127 }
3128 }
3129
3130 let wire_commands = self.commands.as_ref().map(|cmds| {
3131 cmds.iter()
3132 .map(|c| crate::wire::CommandWireDefinition {
3133 name: c.name.clone(),
3134 description: c.description.clone(),
3135 })
3136 .collect()
3137 });
3138 let wire_canvases = self.canvases.clone();
3139 let canvas_handler = self.canvas_handler.clone();
3140 let bearer_token_providers =
3141 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3142
3143 let wire = crate::wire::SessionResumeWire {
3144 session_id: self.session_id,
3145 model: self.model,
3146 client_name: self.client_name,
3147 reasoning_effort: self.reasoning_effort,
3148 reasoning_summary: self.reasoning_summary,
3149 context_tier: self.context_tier,
3150 streaming: self.streaming,
3151 system_message: self.system_message,
3152 tools: self.tools,
3153 canvases: wire_canvases,
3154 open_canvases: self.open_canvases,
3155 request_canvas_renderer: self.request_canvas_renderer,
3156 request_extensions: self.request_extensions,
3157 extension_sdk_path: self.extension_sdk_path,
3158 extension_info: self.extension_info,
3159 available_tools: self.available_tools,
3160 excluded_tools: self.excluded_tools,
3161 excluded_builtin_agents: self.excluded_builtin_agents,
3162 tool_filter_precedence: "excluded",
3163 mcp_servers: self.mcp_servers,
3164 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3165 embedding_cache_storage: self.embedding_cache_storage,
3166 env_value_mode: "direct",
3167 enable_config_discovery: self.enable_config_discovery,
3168 skip_embedding_retrieval: self.skip_embedding_retrieval,
3169 organization_custom_instructions: self.organization_custom_instructions,
3170 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3171 enable_file_hooks: self.enable_file_hooks,
3172 enable_host_git_operations: self.enable_host_git_operations,
3173 enable_session_store: self.enable_session_store,
3174 enable_skills: self.enable_skills,
3175 request_user_input,
3176 request_permission: permission_active,
3177 request_exit_plan_mode,
3178 request_auto_mode_switch,
3179 request_elicitation,
3180 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3181 hooks: hooks_flag,
3182 skill_directories: self.skill_directories,
3183 instruction_directories: self.instruction_directories,
3184 plugin_directories: self.plugin_directories,
3185 large_output: self.large_output,
3186 disabled_skills: self.disabled_skills,
3187 custom_agents: self.custom_agents,
3188 default_agent: self.default_agent,
3189 agent: self.agent,
3190 infinite_sessions: self.infinite_sessions,
3191 provider: self.provider,
3192 capi: self.capi,
3193 providers: self.providers,
3194 models: self.models,
3195 enable_session_telemetry: self.enable_session_telemetry,
3196 enable_citations: self.enable_citations,
3197 session_limits: self.session_limits,
3198 model_capabilities: self.model_capabilities,
3199 memory: self.memory,
3200 config_dir: self.config_directory,
3201 working_directory: self.working_directory,
3202 github_token: self.github_token,
3203 remote_session: self.remote_session,
3204 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3205 enable_github_telemetry_forwarding: None,
3206 commands: wire_commands,
3207 exp_assignments: self.exp_assignments,
3208 enable_managed_settings: self.enable_managed_settings,
3209 suppress_resume_event: self.suppress_resume_event,
3210 continue_pending_work: self.continue_pending_work,
3211 };
3212
3213 let runtime = SessionConfigRuntime {
3214 permission_handler: self.permission_handler,
3215 permission_policy: self.permission_policy,
3216 elicitation_handler: self.elicitation_handler,
3217 mcp_auth_handler: self.mcp_auth_handler,
3218 user_input_handler: self.user_input_handler,
3219 exit_plan_mode_handler: self.exit_plan_mode_handler,
3220 auto_mode_switch_handler: self.auto_mode_switch_handler,
3221 hooks_handler: self.hooks_handler,
3222 system_message_transform: self.system_message_transform,
3223 tool_handlers,
3224 canvas_handler,
3225 session_fs_provider: self.session_fs_provider,
3226 bearer_token_providers,
3227 commands: self.commands,
3228 };
3229
3230 Ok((wire, runtime))
3231 }
3232
3233 pub fn new(session_id: SessionId) -> Self {
3238 Self {
3239 session_id,
3240 model: None,
3241 client_name: None,
3242 reasoning_effort: None,
3243 reasoning_summary: None,
3244 context_tier: None,
3245 streaming: None,
3246 system_message: None,
3247 tools: None,
3248 canvases: None,
3249 canvas_handler: None,
3250 open_canvases: None,
3251 request_canvas_renderer: None,
3252 request_extensions: None,
3253 extension_sdk_path: None,
3254 extension_info: None,
3255 available_tools: None,
3256 excluded_tools: None,
3257 excluded_builtin_agents: None,
3258 mcp_servers: None,
3259 mcp_oauth_token_storage: None,
3260 enable_config_discovery: None,
3261 skip_embedding_retrieval: None,
3262 organization_custom_instructions: None,
3263 enable_on_demand_instruction_discovery: None,
3264 enable_file_hooks: None,
3265 enable_host_git_operations: None,
3266 enable_session_store: None,
3267 enable_skills: None,
3268 embedding_cache_storage: None,
3269 enable_mcp_apps: None,
3270 skill_directories: None,
3271 instruction_directories: None,
3272 plugin_directories: None,
3273 large_output: None,
3274 disabled_skills: None,
3275 hooks: None,
3276 custom_agents: None,
3277 default_agent: None,
3278 agent: None,
3279 infinite_sessions: None,
3280 provider: None,
3281 capi: None,
3282 providers: None,
3283 models: None,
3284 enable_session_telemetry: None,
3285 enable_citations: None,
3286 session_limits: None,
3287 model_capabilities: None,
3288 memory: None,
3289 config_directory: None,
3290 working_directory: None,
3291 github_token: None,
3292 remote_session: None,
3293 include_sub_agent_streaming_events: None,
3294 commands: None,
3295 exp_assignments: None,
3296 enable_managed_settings: None,
3297 session_fs_provider: None,
3298 suppress_resume_event: None,
3299 continue_pending_work: None,
3300 permission_handler: None,
3301 elicitation_handler: None,
3302 mcp_auth_handler: None,
3303 user_input_handler: None,
3304 exit_plan_mode_handler: None,
3305 auto_mode_switch_handler: None,
3306 hooks_handler: None,
3307 permission_policy: None,
3308 system_message_transform: None,
3309 skip_custom_instructions: None,
3310 custom_agents_local_only: None,
3311 coauthor_enabled: None,
3312 manage_schedule_enabled: None,
3313 }
3314 }
3315
3316 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3318 self.permission_handler = Some(handler);
3319 self
3320 }
3321
3322 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3324 self.elicitation_handler = Some(handler);
3325 self
3326 }
3327
3328 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3330 self.mcp_auth_handler = Some(handler);
3331 self
3332 }
3333
3334 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3336 self.user_input_handler = Some(handler);
3337 self
3338 }
3339
3340 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3342 self.exit_plan_mode_handler = Some(handler);
3343 self
3344 }
3345
3346 pub fn with_auto_mode_switch_handler(
3348 mut self,
3349 handler: Arc<dyn AutoModeSwitchHandler>,
3350 ) -> Self {
3351 self.auto_mode_switch_handler = Some(handler);
3352 self
3353 }
3354
3355 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3358 self.hooks_handler = Some(hooks);
3359 self
3360 }
3361
3362 pub fn with_system_message_transform(
3364 mut self,
3365 transform: Arc<dyn SystemMessageTransform>,
3366 ) -> Self {
3367 self.system_message_transform = Some(transform);
3368 self
3369 }
3370
3371 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3375 self.commands = Some(commands);
3376 self
3377 }
3378
3379 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3382 self.session_fs_provider = Some(provider);
3383 self
3384 }
3385
3386 pub fn approve_all_permissions(mut self) -> Self {
3389 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3390 self
3391 }
3392
3393 pub fn deny_all_permissions(mut self) -> Self {
3396 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3397 self
3398 }
3399
3400 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3403 where
3404 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3405 {
3406 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3407 self
3408 }
3409
3410 pub fn with_model(mut self, model: impl Into<String>) -> Self {
3412 self.model = Some(model.into());
3413 self
3414 }
3415
3416 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3418 self.client_name = Some(name.into());
3419 self
3420 }
3421
3422 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3424 self.reasoning_effort = Some(effort.into());
3425 self
3426 }
3427
3428 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3430 self.reasoning_summary = Some(summary);
3431 self
3432 }
3433
3434 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3437 self.context_tier = Some(tier.into());
3438 self
3439 }
3440
3441 pub fn with_streaming(mut self, streaming: bool) -> Self {
3443 self.streaming = Some(streaming);
3444 self
3445 }
3446
3447 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3450 self.system_message = Some(system_message);
3451 self
3452 }
3453
3454 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3456 self.tools = Some(tools.into_iter().collect());
3457 self
3458 }
3459
3460 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3462 self.canvases = Some(canvases.into_iter().collect());
3463 self
3464 }
3465
3466 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
3468 self.canvas_handler = Some(handler);
3469 self
3470 }
3471
3472 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
3474 mut self,
3475 open_canvases: I,
3476 ) -> Self {
3477 self.open_canvases = Some(open_canvases.into_iter().collect());
3478 self
3479 }
3480
3481 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
3483 self.request_canvas_renderer = Some(request);
3484 self
3485 }
3486
3487 pub fn with_request_extensions(mut self, request: bool) -> Self {
3489 self.request_extensions = Some(request);
3490 self
3491 }
3492
3493 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
3497 self.extension_sdk_path = Some(path.into());
3498 self
3499 }
3500
3501 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
3503 self.extension_info = Some(extension_info);
3504 self
3505 }
3506
3507 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
3509 where
3510 I: IntoIterator<Item = S>,
3511 S: Into<String>,
3512 {
3513 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
3514 self
3515 }
3516
3517 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
3519 where
3520 I: IntoIterator<Item = S>,
3521 S: Into<String>,
3522 {
3523 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
3524 self
3525 }
3526
3527 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
3529 where
3530 I: IntoIterator<Item = S>,
3531 S: Into<String>,
3532 {
3533 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
3534 self
3535 }
3536
3537 pub fn with_mcp_servers(mut self, servers: IndexMap<String, McpServerConfig>) -> Self {
3539 self.mcp_servers = Some(servers);
3540 self
3541 }
3542
3543 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3546 self.mcp_oauth_token_storage = Some(mode.into());
3547 self
3548 }
3549
3550 pub fn with_embedding_cache_storage(
3552 mut self,
3553 embedding_cache_storage: impl Into<String>,
3554 ) -> Self {
3555 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3556 self
3557 }
3558
3559 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3561 self.enable_config_discovery = Some(enable);
3562 self
3563 }
3564
3565 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3567 self.skip_embedding_retrieval = Some(value);
3568 self
3569 }
3570
3571 pub fn with_organization_custom_instructions(
3573 mut self,
3574 instructions: impl Into<String>,
3575 ) -> Self {
3576 self.organization_custom_instructions = Some(instructions.into());
3577 self
3578 }
3579
3580 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3582 self.enable_on_demand_instruction_discovery = Some(value);
3583 self
3584 }
3585
3586 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3588 self.enable_file_hooks = Some(value);
3589 self
3590 }
3591
3592 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3594 self.enable_host_git_operations = Some(value);
3595 self
3596 }
3597
3598 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3600 self.enable_session_store = Some(value);
3601 self
3602 }
3603
3604 pub fn with_enable_skills(mut self, value: bool) -> Self {
3606 self.enable_skills = Some(value);
3607 self
3608 }
3609
3610 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3616 self.enable_mcp_apps = Some(enable);
3617 self
3618 }
3619
3620 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3622 where
3623 I: IntoIterator<Item = P>,
3624 P: Into<PathBuf>,
3625 {
3626 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3627 self
3628 }
3629
3630 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3634 where
3635 I: IntoIterator<Item = P>,
3636 P: Into<PathBuf>,
3637 {
3638 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3639 self
3640 }
3641
3642 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3644 where
3645 I: IntoIterator<Item = P>,
3646 P: Into<PathBuf>,
3647 {
3648 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3649 self
3650 }
3651
3652 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3654 self.large_output = Some(config);
3655 self
3656 }
3657
3658 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3660 where
3661 I: IntoIterator<Item = S>,
3662 S: Into<String>,
3663 {
3664 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3665 self
3666 }
3667
3668 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3670 mut self,
3671 agents: I,
3672 ) -> Self {
3673 self.custom_agents = Some(agents.into_iter().collect());
3674 self
3675 }
3676
3677 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3679 self.default_agent = Some(agent);
3680 self
3681 }
3682
3683 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3685 self.agent = Some(name.into());
3686 self
3687 }
3688
3689 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3691 self.infinite_sessions = Some(config);
3692 self
3693 }
3694
3695 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3697 self.provider = Some(provider);
3698 self
3699 }
3700
3701 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3703 self.capi = Some(capi);
3704 self
3705 }
3706
3707 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3713 self.providers = Some(providers);
3714 self
3715 }
3716
3717 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3723 self.models = Some(models);
3724 self
3725 }
3726
3727 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3731 self.enable_session_telemetry = Some(enable);
3732 self
3733 }
3734
3735 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3737 self.enable_citations = Some(enable);
3738 self
3739 }
3740
3741 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3743 self.session_limits = Some(limits);
3744 self
3745 }
3746
3747 pub fn with_model_capabilities(
3749 mut self,
3750 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3751 ) -> Self {
3752 self.model_capabilities = Some(capabilities);
3753 self
3754 }
3755
3756 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3758 self.memory = Some(memory);
3759 self
3760 }
3761
3762 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3764 self.config_directory = Some(dir.into());
3765 self
3766 }
3767
3768 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3770 self.working_directory = Some(dir.into());
3771 self
3772 }
3773
3774 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3778 self.github_token = Some(token.into());
3779 self
3780 }
3781
3782 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3784 self.include_sub_agent_streaming_events = Some(include);
3785 self
3786 }
3787
3788 pub fn with_remote_session(
3790 mut self,
3791 mode: crate::generated::api_types::RemoteSessionMode,
3792 ) -> Self {
3793 self.remote_session = Some(mode);
3794 self
3795 }
3796
3797 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
3800 self.suppress_resume_event = Some(suppress);
3801 self
3802 }
3803
3804 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
3810 self.continue_pending_work = Some(continue_pending);
3811 self
3812 }
3813
3814 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3816 self.skip_custom_instructions = Some(value);
3817 self
3818 }
3819
3820 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3822 self.custom_agents_local_only = Some(value);
3823 self
3824 }
3825
3826 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3828 self.coauthor_enabled = Some(value);
3829 self
3830 }
3831
3832 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3834 self.manage_schedule_enabled = Some(value);
3835 self
3836 }
3837
3838 #[doc(hidden)]
3842 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
3843 self.exp_assignments = Some(assignments);
3844 self
3845 }
3846
3847 pub fn with_enable_managed_settings(mut self, enabled: bool) -> Self {
3850 self.enable_managed_settings = Some(enabled);
3851 self
3852 }
3853}
3854
3855#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3861#[serde(rename_all = "camelCase")]
3862#[non_exhaustive]
3863pub struct SystemMessageConfig {
3864 #[serde(skip_serializing_if = "Option::is_none")]
3866 pub mode: Option<String>,
3867 #[serde(skip_serializing_if = "Option::is_none")]
3869 pub content: Option<String>,
3870 #[serde(skip_serializing_if = "Option::is_none")]
3872 pub sections: Option<HashMap<String, SectionOverride>>,
3873}
3874
3875impl SystemMessageConfig {
3876 pub fn new() -> Self {
3879 Self::default()
3880 }
3881
3882 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
3885 self.mode = Some(mode.into());
3886 self
3887 }
3888
3889 pub fn with_content(mut self, content: impl Into<String>) -> Self {
3892 self.content = Some(content.into());
3893 self
3894 }
3895
3896 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
3898 self.sections = Some(sections);
3899 self
3900 }
3901}
3902
3903#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3909#[serde(rename_all = "camelCase")]
3910pub struct SectionOverride {
3911 #[serde(skip_serializing_if = "Option::is_none")]
3914 pub action: Option<String>,
3915 #[serde(skip_serializing_if = "Option::is_none")]
3917 pub content: Option<String>,
3918}
3919
3920#[derive(Debug, Clone, Serialize, Deserialize)]
3922#[serde(rename_all = "camelCase")]
3923pub struct CreateSessionResult {
3924 pub session_id: SessionId,
3926 #[serde(skip_serializing_if = "Option::is_none")]
3928 pub workspace_path: Option<PathBuf>,
3929 #[serde(default, alias = "remote_url")]
3931 pub remote_url: Option<String>,
3932 #[serde(skip_serializing_if = "Option::is_none")]
3934 pub capabilities: Option<SessionCapabilities>,
3935}
3936
3937#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3939#[serde(rename_all = "camelCase")]
3940pub(crate) struct ResumeSessionResult {
3941 #[serde(default)]
3943 pub session_id: Option<SessionId>,
3944 #[serde(default, skip_serializing_if = "Option::is_none")]
3946 pub workspace_path: Option<PathBuf>,
3947 #[serde(default, alias = "remote_url")]
3949 pub remote_url: Option<String>,
3950 #[serde(default, skip_serializing_if = "Option::is_none")]
3952 pub capabilities: Option<SessionCapabilities>,
3953 #[serde(
3955 default,
3956 alias = "openCanvasInstances",
3957 skip_serializing_if = "Option::is_none"
3958 )]
3959 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3960}
3961
3962#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3964#[serde(rename_all = "lowercase")]
3965pub enum LogLevel {
3966 #[default]
3968 Info,
3969 Warning,
3971 Error,
3973}
3974
3975#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
3980#[serde(rename_all = "camelCase")]
3981pub struct LogOptions {
3982 #[serde(skip_serializing_if = "Option::is_none")]
3984 pub level: Option<LogLevel>,
3985 #[serde(skip_serializing_if = "Option::is_none")]
3988 pub ephemeral: Option<bool>,
3989}
3990
3991impl LogOptions {
3992 pub fn with_level(mut self, level: LogLevel) -> Self {
3994 self.level = Some(level);
3995 self
3996 }
3997
3998 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
4000 self.ephemeral = Some(ephemeral);
4001 self
4002 }
4003}
4004
4005#[derive(Debug, Clone, Default)]
4009pub struct SetModelOptions {
4010 pub reasoning_effort: Option<String>,
4013 pub reasoning_summary: Option<ReasoningSummary>,
4017 pub context_tier: Option<ContextTier>,
4020 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
4024}
4025
4026impl SetModelOptions {
4027 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
4029 self.reasoning_effort = Some(effort.into());
4030 self
4031 }
4032
4033 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
4035 self.reasoning_summary = Some(summary);
4036 self
4037 }
4038
4039 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4041 self.context_tier = Some(tier);
4042 self
4043 }
4044
4045 pub fn with_model_capabilities(
4047 mut self,
4048 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4049 ) -> Self {
4050 self.model_capabilities = Some(caps);
4051 self
4052 }
4053}
4054
4055#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4062#[serde(rename_all = "camelCase")]
4063pub struct PingResponse {
4064 #[serde(default)]
4066 pub message: String,
4067 #[serde(default)]
4069 pub timestamp: String,
4070 #[serde(skip_serializing_if = "Option::is_none")]
4072 pub protocol_version: Option<u32>,
4073}
4074
4075#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4077#[serde(rename_all = "camelCase")]
4078pub struct AttachmentLineRange {
4079 pub start: u32,
4081 pub end: u32,
4083}
4084
4085#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4087#[serde(rename_all = "camelCase")]
4088pub struct AttachmentSelectionPosition {
4089 pub line: u32,
4091 pub character: u32,
4093}
4094
4095#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4097#[serde(rename_all = "camelCase")]
4098pub struct AttachmentSelectionRange {
4099 pub start: AttachmentSelectionPosition,
4101 pub end: AttachmentSelectionPosition,
4103}
4104
4105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4107#[serde(rename_all = "snake_case")]
4108#[non_exhaustive]
4109pub enum GitHubReferenceType {
4110 Issue,
4112 Pr,
4114 Discussion,
4116}
4117
4118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4124#[serde(rename_all = "camelCase")]
4125pub struct GitHubRepoPointer {
4126 #[serde(skip_serializing_if = "Option::is_none")]
4128 pub id: Option<i64>,
4129 pub name: String,
4131 pub owner: String,
4133}
4134
4135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4137#[serde(rename_all = "camelCase")]
4138pub struct GitHubFileDiffSide {
4139 pub path: String,
4141 pub r#ref: String,
4143 pub repo: GitHubRepoPointer,
4145}
4146
4147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4149#[serde(rename_all = "camelCase")]
4150pub struct GitHubTreeComparisonSide {
4151 pub repo: GitHubRepoPointer,
4153 pub revision: String,
4155}
4156
4157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4159#[serde(rename_all = "camelCase")]
4160pub struct GitHubSnippetLineRange {
4161 pub start: i64,
4163 pub end: i64,
4165}
4166
4167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4169#[serde(
4170 tag = "type",
4171 rename_all = "camelCase",
4172 rename_all_fields = "camelCase"
4173)]
4174#[non_exhaustive]
4175pub enum Attachment {
4176 File {
4178 path: PathBuf,
4180 #[serde(skip_serializing_if = "Option::is_none")]
4182 display_name: Option<String>,
4183 #[serde(skip_serializing_if = "Option::is_none")]
4185 line_range: Option<AttachmentLineRange>,
4186 },
4187 Directory {
4189 path: PathBuf,
4191 #[serde(skip_serializing_if = "Option::is_none")]
4193 display_name: Option<String>,
4194 },
4195 Selection {
4197 file_path: PathBuf,
4199 text: String,
4201 #[serde(skip_serializing_if = "Option::is_none")]
4203 display_name: Option<String>,
4204 selection: AttachmentSelectionRange,
4206 },
4207 Blob {
4209 data: String,
4211 mime_type: String,
4213 #[serde(skip_serializing_if = "Option::is_none")]
4215 display_name: Option<String>,
4216 },
4217 #[serde(rename = "github_reference")]
4219 GitHubReference {
4220 number: u64,
4222 title: String,
4224 reference_type: GitHubReferenceType,
4226 state: String,
4228 url: String,
4230 },
4231 #[serde(rename = "github_commit")]
4233 GitHubCommit {
4234 message: String,
4236 oid: String,
4238 repo: GitHubRepoPointer,
4240 url: String,
4242 },
4243 #[serde(rename = "github_release")]
4245 GitHubRelease {
4246 name: String,
4248 repo: GitHubRepoPointer,
4250 tag_name: String,
4252 url: String,
4254 },
4255 #[serde(rename = "github_actions_job")]
4257 GitHubActionsJob {
4258 #[serde(skip_serializing_if = "Option::is_none")]
4261 conclusion: Option<String>,
4262 job_id: i64,
4264 job_name: String,
4266 repo: GitHubRepoPointer,
4268 url: String,
4270 workflow_name: String,
4272 },
4273 #[serde(rename = "github_repository")]
4275 GitHubRepository {
4276 #[serde(skip_serializing_if = "Option::is_none")]
4278 description: Option<String>,
4279 #[serde(skip_serializing_if = "Option::is_none")]
4282 r#ref: Option<String>,
4283 repo: GitHubRepoPointer,
4285 url: String,
4287 },
4288 #[serde(rename = "github_file_diff")]
4290 GitHubFileDiff {
4291 #[serde(skip_serializing_if = "Option::is_none")]
4293 base: Option<GitHubFileDiffSide>,
4294 #[serde(skip_serializing_if = "Option::is_none")]
4296 head: Option<GitHubFileDiffSide>,
4297 url: String,
4299 },
4300 #[serde(rename = "github_tree_comparison")]
4302 GitHubTreeComparison {
4303 base: GitHubTreeComparisonSide,
4305 head: GitHubTreeComparisonSide,
4307 url: String,
4309 },
4310 #[serde(rename = "github_url")]
4312 GitHubUrl {
4313 url: String,
4315 },
4316 #[serde(rename = "github_file")]
4318 GitHubFile {
4319 path: String,
4321 r#ref: String,
4323 repo: GitHubRepoPointer,
4325 url: String,
4327 },
4328 #[serde(rename = "github_snippet")]
4330 GitHubSnippet {
4331 line_range: GitHubSnippetLineRange,
4333 path: String,
4335 r#ref: String,
4337 repo: GitHubRepoPointer,
4339 url: String,
4341 },
4342}
4343
4344impl Attachment {
4345 pub fn display_name(&self) -> Option<&str> {
4347 match self {
4348 Self::File { display_name, .. }
4349 | Self::Directory { display_name, .. }
4350 | Self::Selection { display_name, .. }
4351 | Self::Blob { display_name, .. } => display_name.as_deref(),
4352 Self::GitHubReference { .. }
4353 | Self::GitHubCommit { .. }
4354 | Self::GitHubRelease { .. }
4355 | Self::GitHubActionsJob { .. }
4356 | Self::GitHubRepository { .. }
4357 | Self::GitHubFileDiff { .. }
4358 | Self::GitHubTreeComparison { .. }
4359 | Self::GitHubUrl { .. }
4360 | Self::GitHubFile { .. }
4361 | Self::GitHubSnippet { .. } => None,
4362 }
4363 }
4364
4365 pub fn label(&self) -> Option<String> {
4367 if let Some(display_name) = self
4368 .display_name()
4369 .map(str::trim)
4370 .filter(|name| !name.is_empty())
4371 {
4372 return Some(display_name.to_string());
4373 }
4374
4375 match self {
4376 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4377 format!("#{}", number)
4378 } else {
4379 title.trim().to_string()
4380 }),
4381 _ => self.derived_display_name(),
4382 }
4383 }
4384
4385 pub fn ensure_display_name(&mut self) {
4387 if self
4388 .display_name()
4389 .map(str::trim)
4390 .is_some_and(|name| !name.is_empty())
4391 {
4392 return;
4393 }
4394
4395 let Some(derived_display_name) = self.derived_display_name() else {
4396 return;
4397 };
4398
4399 match self {
4400 Self::File { display_name, .. }
4401 | Self::Directory { display_name, .. }
4402 | Self::Selection { display_name, .. }
4403 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4404 Self::GitHubReference { .. }
4405 | Self::GitHubCommit { .. }
4406 | Self::GitHubRelease { .. }
4407 | Self::GitHubActionsJob { .. }
4408 | Self::GitHubRepository { .. }
4409 | Self::GitHubFileDiff { .. }
4410 | Self::GitHubTreeComparison { .. }
4411 | Self::GitHubUrl { .. }
4412 | Self::GitHubFile { .. }
4413 | Self::GitHubSnippet { .. } => {}
4414 }
4415 }
4416
4417 fn derived_display_name(&self) -> Option<String> {
4418 match self {
4419 Self::File { path, .. } | Self::Directory { path, .. } => {
4420 Some(attachment_name_from_path(path))
4421 }
4422 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4423 Self::Blob { .. } => Some("attachment".to_string()),
4424 Self::GitHubReference { .. }
4425 | Self::GitHubCommit { .. }
4426 | Self::GitHubRelease { .. }
4427 | Self::GitHubActionsJob { .. }
4428 | Self::GitHubRepository { .. }
4429 | Self::GitHubFileDiff { .. }
4430 | Self::GitHubTreeComparison { .. }
4431 | Self::GitHubUrl { .. }
4432 | Self::GitHubFile { .. }
4433 | Self::GitHubSnippet { .. } => None,
4434 }
4435 }
4436}
4437
4438fn attachment_name_from_path(path: &Path) -> String {
4439 path.file_name()
4440 .map(|name| name.to_string_lossy().into_owned())
4441 .filter(|name| !name.is_empty())
4442 .unwrap_or_else(|| {
4443 let full = path.to_string_lossy();
4444 if full.is_empty() {
4445 "attachment".to_string()
4446 } else {
4447 full.into_owned()
4448 }
4449 })
4450}
4451
4452pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
4454 for attachment in attachments {
4455 attachment.ensure_display_name();
4456 }
4457}
4458
4459#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4464#[serde(rename_all = "lowercase")]
4465#[non_exhaustive]
4466pub enum DeliveryMode {
4467 Enqueue,
4469 Immediate,
4471}
4472
4473#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4478#[serde(rename_all = "lowercase")]
4479#[non_exhaustive]
4480pub enum AgentMode {
4481 Interactive,
4483 Plan,
4485 Autopilot,
4487 Shell,
4489}
4490
4491#[derive(Debug, Clone)]
4520#[non_exhaustive]
4521pub struct MessageOptions {
4522 pub prompt: String,
4524 pub mode: Option<DeliveryMode>,
4530 pub agent_mode: Option<AgentMode>,
4534 pub attachments: Option<Vec<Attachment>>,
4536 pub wait_timeout: Option<Duration>,
4539 pub request_headers: Option<HashMap<String, String>>,
4543 pub traceparent: Option<String>,
4550 pub tracestate: Option<String>,
4554 pub display_prompt: Option<String>,
4556}
4557
4558impl MessageOptions {
4559 pub fn new(prompt: impl Into<String>) -> Self {
4561 Self {
4562 prompt: prompt.into(),
4563 mode: None,
4564 agent_mode: None,
4565 attachments: None,
4566 wait_timeout: None,
4567 request_headers: None,
4568 traceparent: None,
4569 tracestate: None,
4570 display_prompt: None,
4571 }
4572 }
4573
4574 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
4580 self.mode = Some(mode);
4581 self
4582 }
4583
4584 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
4588 self.agent_mode = Some(agent_mode);
4589 self
4590 }
4591
4592 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
4594 self.attachments = Some(attachments);
4595 self
4596 }
4597
4598 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
4600 self.wait_timeout = Some(timeout);
4601 self
4602 }
4603
4604 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
4606 self.request_headers = Some(headers);
4607 self
4608 }
4609
4610 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
4615 self.traceparent = ctx.traceparent;
4616 self.tracestate = ctx.tracestate;
4617 self
4618 }
4619
4620 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
4622 self.traceparent = Some(traceparent.into());
4623 self
4624 }
4625
4626 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
4628 self.tracestate = Some(tracestate.into());
4629 self
4630 }
4631
4632 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
4634 self.display_prompt = Some(display_prompt.into());
4635 self
4636 }
4637}
4638
4639impl From<&str> for MessageOptions {
4640 fn from(prompt: &str) -> Self {
4641 Self::new(prompt)
4642 }
4643}
4644
4645impl From<String> for MessageOptions {
4646 fn from(prompt: String) -> Self {
4647 Self::new(prompt)
4648 }
4649}
4650
4651impl From<&String> for MessageOptions {
4652 fn from(prompt: &String) -> Self {
4653 Self::new(prompt.clone())
4654 }
4655}
4656
4657#[derive(Debug, Clone, Serialize, Deserialize)]
4659#[serde(rename_all = "camelCase")]
4660#[non_exhaustive]
4661pub struct GetStatusResponse {
4662 pub version: String,
4664 pub protocol_version: u32,
4666}
4667
4668#[derive(Debug, Clone, Serialize, Deserialize)]
4670#[serde(rename_all = "camelCase")]
4671#[non_exhaustive]
4672pub struct GetAuthStatusResponse {
4673 pub is_authenticated: bool,
4675 #[serde(skip_serializing_if = "Option::is_none")]
4678 pub auth_type: Option<String>,
4679 #[serde(skip_serializing_if = "Option::is_none")]
4681 pub host: Option<String>,
4682 #[serde(skip_serializing_if = "Option::is_none")]
4684 pub login: Option<String>,
4685 #[serde(skip_serializing_if = "Option::is_none")]
4687 pub status_message: Option<String>,
4688}
4689
4690#[derive(Debug, Clone, Serialize, Deserialize)]
4694#[serde(rename_all = "camelCase")]
4695pub struct SessionEventNotification {
4696 pub session_id: SessionId,
4698 pub event: SessionEvent,
4700}
4701
4702#[derive(Debug, Clone, Serialize, Deserialize)]
4709#[serde(rename_all = "camelCase")]
4710pub struct SessionEvent {
4711 pub id: String,
4713 pub timestamp: String,
4715 pub parent_id: Option<String>,
4717 #[serde(skip_serializing_if = "Option::is_none")]
4719 pub ephemeral: Option<bool>,
4720 #[serde(skip_serializing_if = "Option::is_none")]
4723 pub agent_id: Option<String>,
4724 #[serde(skip_serializing_if = "Option::is_none")]
4726 pub debug_cli_received_at_ms: Option<i64>,
4727 #[serde(skip_serializing_if = "Option::is_none")]
4729 pub debug_ws_forwarded_at_ms: Option<i64>,
4730 #[serde(rename = "type")]
4732 pub event_type: String,
4733 pub data: Value,
4735}
4736
4737impl SessionEvent {
4738 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
4743 use serde::de::IntoDeserializer;
4744 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
4745 self.event_type.as_str().into_deserializer();
4746 crate::generated::SessionEventType::deserialize(deserializer)
4747 .unwrap_or(crate::generated::SessionEventType::Unknown)
4748 }
4749
4750 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
4756 serde_json::from_value(self.data.clone()).ok()
4757 }
4758
4759 pub fn is_transient_error(&self) -> bool {
4763 self.event_type == "session.error"
4764 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
4765 }
4766}
4767
4768#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4773#[serde(rename_all = "camelCase")]
4774#[non_exhaustive]
4775pub struct ToolInvocation {
4776 pub session_id: SessionId,
4778 pub tool_call_id: String,
4780 pub tool_name: String,
4782 pub arguments: Value,
4784 #[serde(default, skip_serializing_if = "Option::is_none")]
4789 pub traceparent: Option<String>,
4790 #[serde(default, skip_serializing_if = "Option::is_none")]
4793 pub tracestate: Option<String>,
4794}
4795
4796impl ToolInvocation {
4797 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
4818 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
4819 }
4820
4821 pub fn trace_context(&self) -> TraceContext {
4824 TraceContext {
4825 traceparent: self.traceparent.clone(),
4826 tracestate: self.tracestate.clone(),
4827 }
4828 }
4829}
4830
4831#[derive(Debug, Clone, Serialize, Deserialize)]
4833#[serde(rename_all = "camelCase")]
4834pub struct ToolBinaryResult {
4835 pub data: String,
4837 pub mime_type: String,
4839 pub r#type: String,
4841 #[serde(default, skip_serializing_if = "Option::is_none")]
4843 pub description: Option<String>,
4844}
4845
4846#[derive(Debug, Clone, Serialize, Deserialize)]
4848#[serde(rename_all = "camelCase")]
4849pub struct ToolResultExpanded {
4850 pub text_result_for_llm: String,
4852 pub result_type: String,
4854 #[serde(default, skip_serializing_if = "Option::is_none")]
4856 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
4857 #[serde(skip_serializing_if = "Option::is_none")]
4859 pub session_log: Option<String>,
4860 #[serde(skip_serializing_if = "Option::is_none")]
4862 pub error: Option<String>,
4863 #[serde(default, skip_serializing_if = "Option::is_none")]
4865 pub tool_telemetry: Option<HashMap<String, Value>>,
4866}
4867
4868#[derive(Debug, Clone, Serialize, Deserialize)]
4870#[serde(untagged)]
4871#[non_exhaustive]
4872pub enum ToolResult {
4873 Text(String),
4875 Expanded(ToolResultExpanded),
4877}
4878
4879#[derive(Debug, Clone, Serialize, Deserialize)]
4881#[serde(rename_all = "camelCase")]
4882pub struct ToolResultResponse {
4883 pub result: ToolResult,
4885}
4886
4887#[derive(Debug, Clone, Serialize, Deserialize)]
4889#[serde(rename_all = "camelCase")]
4890pub struct SessionMetadata {
4891 pub session_id: SessionId,
4893 pub start_time: String,
4895 pub modified_time: String,
4897 #[serde(skip_serializing_if = "Option::is_none")]
4899 pub summary: Option<String>,
4900 pub is_remote: bool,
4902}
4903
4904#[derive(Debug, Clone, Serialize, Deserialize)]
4906#[serde(rename_all = "camelCase")]
4907pub struct ListSessionsResponse {
4908 pub sessions: Vec<SessionMetadata>,
4910}
4911
4912#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4916#[serde(rename_all = "camelCase")]
4917pub struct SessionListFilter {
4918 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
4920 pub working_directory: Option<String>,
4921 #[serde(default, skip_serializing_if = "Option::is_none")]
4923 pub git_root: Option<String>,
4924 #[serde(default, skip_serializing_if = "Option::is_none")]
4926 pub repository: Option<String>,
4927 #[serde(default, skip_serializing_if = "Option::is_none")]
4929 pub branch: Option<String>,
4930}
4931
4932#[derive(Debug, Clone, Serialize, Deserialize)]
4934#[serde(rename_all = "camelCase")]
4935pub struct GetSessionMetadataResponse {
4936 #[serde(skip_serializing_if = "Option::is_none")]
4938 pub session: Option<SessionMetadata>,
4939}
4940
4941#[derive(Debug, Clone, Serialize, Deserialize)]
4943#[serde(rename_all = "camelCase")]
4944pub struct GetLastSessionIdResponse {
4945 #[serde(skip_serializing_if = "Option::is_none")]
4947 pub session_id: Option<SessionId>,
4948}
4949
4950#[derive(Debug, Clone, Serialize, Deserialize)]
4952#[serde(rename_all = "camelCase")]
4953pub struct GetForegroundSessionResponse {
4954 #[serde(skip_serializing_if = "Option::is_none")]
4956 pub session_id: Option<SessionId>,
4957}
4958
4959#[derive(Debug, Clone, Serialize, Deserialize)]
4961#[serde(rename_all = "camelCase")]
4962pub struct GetMessagesResponse {
4963 pub events: Vec<SessionEvent>,
4965}
4966
4967#[derive(Debug, Clone, Serialize, Deserialize)]
4969#[serde(rename_all = "camelCase")]
4970pub struct ElicitationResult {
4971 pub action: String,
4973 #[serde(skip_serializing_if = "Option::is_none")]
4975 pub content: Option<Value>,
4976}
4977
4978#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4984#[serde(rename_all = "camelCase")]
4985#[non_exhaustive]
4986pub enum ElicitationMode {
4987 Form,
4989 Url,
4991 #[serde(other)]
4993 Unknown,
4994}
4995
4996#[derive(Debug, Clone, Serialize, Deserialize)]
5003#[serde(rename_all = "camelCase")]
5004pub struct ElicitationRequest {
5005 pub message: String,
5007 #[serde(skip_serializing_if = "Option::is_none")]
5009 pub requested_schema: Option<Value>,
5010 #[serde(skip_serializing_if = "Option::is_none")]
5012 pub mode: Option<ElicitationMode>,
5013 #[serde(skip_serializing_if = "Option::is_none")]
5015 pub elicitation_source: Option<String>,
5016 #[serde(skip_serializing_if = "Option::is_none")]
5018 pub url: Option<String>,
5019}
5020
5021#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5026#[serde(rename_all = "camelCase")]
5027pub struct SessionCapabilities {
5028 #[serde(skip_serializing_if = "Option::is_none")]
5030 pub ui: Option<UiCapabilities>,
5031}
5032
5033#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5035#[serde(rename_all = "camelCase")]
5036pub struct UiCapabilities {
5037 #[serde(skip_serializing_if = "Option::is_none")]
5039 pub elicitation: Option<bool>,
5040 #[serde(skip_serializing_if = "Option::is_none")]
5051 pub mcp_apps: Option<bool>,
5052 #[serde(skip_serializing_if = "Option::is_none")]
5054 pub canvases: Option<bool>,
5055}
5056
5057#[derive(Debug, Clone, Default)]
5059pub struct UiInputOptions<'a> {
5060 pub title: Option<&'a str>,
5062 pub description: Option<&'a str>,
5064 pub min_length: Option<u64>,
5066 pub max_length: Option<u64>,
5068 pub format: Option<InputFormat>,
5070 pub default: Option<&'a str>,
5072}
5073
5074#[derive(Debug, Clone, Copy)]
5076#[non_exhaustive]
5077pub enum InputFormat {
5078 Email,
5080 Uri,
5082 Date,
5084 DateTime,
5086}
5087
5088impl InputFormat {
5089 pub fn as_str(&self) -> &'static str {
5091 match self {
5092 Self::Email => "email",
5093 Self::Uri => "uri",
5094 Self::Date => "date",
5095 Self::DateTime => "date-time",
5096 }
5097 }
5098}
5099
5100pub use crate::generated::api_types::{
5105 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5106 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5107 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5108 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
5109};
5110
5111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5117#[serde(rename_all = "kebab-case")]
5118#[non_exhaustive]
5119pub enum PermissionRequestKind {
5120 Shell,
5122 Write,
5124 Read,
5126 Url,
5128 Mcp,
5130 CustomTool,
5132 Memory,
5134 Hook,
5136 #[serde(other)]
5139 Unknown,
5140}
5141
5142#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5148#[serde(rename_all = "camelCase")]
5149pub struct PermissionRequestData {
5150 #[serde(default, skip_serializing_if = "Option::is_none")]
5154 pub kind: Option<PermissionRequestKind>,
5155 #[serde(default, skip_serializing_if = "Option::is_none")]
5158 pub tool_call_id: Option<String>,
5159 #[serde(flatten)]
5162 pub extra: Value,
5163}
5164
5165#[derive(Debug, Clone, Serialize, Deserialize)]
5167#[serde(rename_all = "camelCase")]
5168pub struct ExitPlanModeData {
5169 #[serde(default)]
5171 pub summary: String,
5172 #[serde(default, skip_serializing_if = "Option::is_none")]
5174 pub plan_content: Option<String>,
5175 #[serde(default)]
5177 pub actions: Vec<String>,
5178 #[serde(default = "default_recommended_action")]
5180 pub recommended_action: String,
5181}
5182
5183fn default_recommended_action() -> String {
5184 "autopilot".to_string()
5185}
5186
5187impl Default for ExitPlanModeData {
5188 fn default() -> Self {
5189 Self {
5190 summary: String::new(),
5191 plan_content: None,
5192 actions: Vec::new(),
5193 recommended_action: default_recommended_action(),
5194 }
5195 }
5196}
5197
5198#[cfg(test)]
5199mod tests {
5200 use std::path::PathBuf;
5201
5202 use serde_json::json;
5203
5204 use super::{
5205 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5206 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5207 CustomAgentConfig, DeliveryMode, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig,
5208 LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration,
5209 NamedProviderConfig, ProviderConfig, ProviderModelConfig, ReasoningSummary,
5210 ResumeSessionConfig, SessionConfig, SessionEvent, SessionId, SystemMessageConfig, Tool,
5211 ToolBinaryResult, ToolResult, ToolResultExpanded, ToolResultResponse,
5212 ensure_attachment_display_names,
5213 };
5214 use crate::generated::session_events::TypedSessionEvent;
5215
5216 #[test]
5217 fn tool_builder_composes() {
5218 let tool = Tool::new("greet")
5219 .with_description("Say hello")
5220 .with_namespaced_name("hello/greet")
5221 .with_instructions("Pass the user's name")
5222 .with_parameters(json!({
5223 "type": "object",
5224 "properties": { "name": { "type": "string" } },
5225 "required": ["name"]
5226 }))
5227 .with_overrides_built_in_tool(true)
5228 .with_skip_permission(true);
5229 assert_eq!(tool.name, "greet");
5230 assert_eq!(tool.description, "Say hello");
5231 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5232 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5233 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5234 assert!(tool.overrides_built_in_tool);
5235 assert!(tool.skip_permission);
5236 }
5237
5238 #[test]
5239 fn tool_defer_serialization() {
5240 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5241 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5242 let value = serde_json::to_value(&tool).unwrap();
5243 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5244
5245 let plain = Tool::new("plain");
5246 let value = serde_json::to_value(&plain).unwrap();
5247 assert!(value.get("defer").is_none());
5248 }
5249
5250 #[test]
5251 fn custom_agent_config_builder_with_model() {
5252 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5253 .with_model("claude-haiku-4.5")
5254 .with_display_name("My Agent");
5255 assert_eq!(agent.name, "my-agent");
5256 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5257 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5258 }
5259
5260 #[test]
5261 fn custom_agent_config_serializes_model() {
5262 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5263 let wire = serde_json::to_value(&agent).unwrap();
5264 assert_eq!(wire["model"], "claude-haiku-4.5");
5265 assert_eq!(wire["name"], "model-agent");
5266 }
5267
5268 #[test]
5269 fn custom_agent_config_omits_model_when_none() {
5270 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5271 let wire = serde_json::to_value(&agent).unwrap();
5272 assert!(wire.get("model").is_none());
5273 }
5274
5275 #[test]
5276 #[should_panic(expected = "tool parameter schema must be a JSON object")]
5277 fn tool_with_parameters_panics_on_non_object_value() {
5278 let _ = Tool::new("noop").with_parameters(json!(null));
5279 }
5280
5281 #[test]
5282 fn tool_result_expanded_serializes_binary_results_for_llm() {
5283 let response = ToolResultResponse {
5284 result: ToolResult::Expanded(ToolResultExpanded {
5285 text_result_for_llm: "rendered chart".to_string(),
5286 result_type: "success".to_string(),
5287 binary_results_for_llm: Some(vec![ToolBinaryResult {
5288 data: "aW1n".to_string(),
5289 mime_type: "image/png".to_string(),
5290 r#type: "image".to_string(),
5291 description: Some("chart preview".to_string()),
5292 }]),
5293 session_log: None,
5294 error: None,
5295 tool_telemetry: None,
5296 }),
5297 };
5298
5299 let wire = serde_json::to_value(&response).unwrap();
5300
5301 assert_eq!(
5302 wire,
5303 json!({
5304 "result": {
5305 "textResultForLlm": "rendered chart",
5306 "resultType": "success",
5307 "binaryResultsForLlm": [
5308 {
5309 "data": "aW1n",
5310 "mimeType": "image/png",
5311 "type": "image",
5312 "description": "chart preview"
5313 }
5314 ]
5315 }
5316 })
5317 );
5318 }
5319
5320 #[test]
5321 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
5322 let response = ToolResultResponse {
5323 result: ToolResult::Expanded(ToolResultExpanded {
5324 text_result_for_llm: "ok".to_string(),
5325 result_type: "success".to_string(),
5326 binary_results_for_llm: None,
5327 session_log: None,
5328 error: None,
5329 tool_telemetry: None,
5330 }),
5331 };
5332
5333 let wire = serde_json::to_value(&response).unwrap();
5334
5335 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5336 assert!(wire["result"].get("binaryResultsForLlm").is_none());
5337 }
5338
5339 #[test]
5340 fn session_config_default_wire_flags_off_without_handlers() {
5341 let cfg = SessionConfig::default();
5342 assert_eq!(cfg.mcp_oauth_token_storage, None);
5343 let (wire, _runtime) = cfg
5347 .into_wire(Some(SessionId::from("default-flags")))
5348 .expect("default config has no duplicate handlers");
5349 assert!(!wire.request_user_input);
5350 assert!(!wire.request_permission);
5351 assert!(!wire.request_elicitation);
5352 assert!(!wire.request_exit_plan_mode);
5353 assert!(!wire.request_auto_mode_switch);
5354 assert!(!wire.hooks);
5355 assert!(!wire.request_mcp_apps);
5356 }
5357
5358 #[test]
5359 fn resume_session_config_new_wire_flags_off_without_handlers() {
5360 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
5361 assert_eq!(cfg.mcp_oauth_token_storage, None);
5362 let (wire, _runtime) = cfg
5363 .into_wire()
5364 .expect("default resume config has no duplicate handlers");
5365 assert!(!wire.request_user_input);
5366 assert!(!wire.request_permission);
5367 assert!(!wire.request_elicitation);
5368 assert!(!wire.request_exit_plan_mode);
5369 assert!(!wire.request_auto_mode_switch);
5370 assert!(!wire.hooks);
5371 assert!(!wire.request_mcp_apps);
5372 }
5373
5374 #[test]
5375 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5376 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
5377 assert_eq!(cfg.enable_mcp_apps, Some(true));
5378
5379 let (wire, _runtime) = cfg
5380 .into_wire(Some(SessionId::from("enable-mcp-apps")))
5381 .expect("enable_mcp_apps config has no duplicate handlers");
5382 assert!(wire.request_mcp_apps);
5383
5384 let json = serde_json::to_value(&wire).unwrap();
5385 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5386 }
5387
5388 #[test]
5389 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5390 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
5391 .with_enable_mcp_apps(true);
5392 assert_eq!(cfg.enable_mcp_apps, Some(true));
5393
5394 let (wire, _runtime) = cfg
5395 .into_wire()
5396 .expect("resume enable_mcp_apps config has no duplicate handlers");
5397 assert!(wire.request_mcp_apps);
5398
5399 let json = serde_json::to_value(&wire).unwrap();
5400 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5401 }
5402
5403 #[test]
5404 fn memory_configuration_constructors_and_serde() {
5405 assert!(MemoryConfiguration::enabled().enabled);
5406 assert!(!MemoryConfiguration::disabled().enabled);
5407 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
5408
5409 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
5410 assert_eq!(json, serde_json::json!({ "enabled": true }));
5411 }
5412
5413 #[test]
5414 fn session_config_with_memory_serializes() {
5415 let (wire, _runtime) = SessionConfig::default()
5416 .with_memory(MemoryConfiguration::enabled())
5417 .into_wire(Some(SessionId::from("memory-on")))
5418 .expect("no duplicate handlers");
5419 let json = serde_json::to_value(&wire).unwrap();
5420 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5421
5422 let (wire_off, _) = SessionConfig::default()
5423 .with_memory(MemoryConfiguration::disabled())
5424 .into_wire(Some(SessionId::from("memory-off")))
5425 .expect("no duplicate handlers");
5426 let json_off = serde_json::to_value(&wire_off).unwrap();
5427 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
5428
5429 let (empty_wire, _) = SessionConfig::default()
5431 .into_wire(Some(SessionId::from("memory-unset")))
5432 .expect("no duplicate handlers");
5433 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5434 assert!(empty_json.get("memory").is_none());
5435 }
5436
5437 #[test]
5438 fn resume_session_config_with_memory_serializes() {
5439 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
5440 .with_memory(MemoryConfiguration::enabled())
5441 .into_wire()
5442 .expect("no duplicate handlers");
5443 let json = serde_json::to_value(&wire).unwrap();
5444 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5445
5446 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
5448 .into_wire()
5449 .expect("no duplicate handlers");
5450 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5451 assert!(empty_json.get("memory").is_none());
5452 }
5453
5454 #[test]
5455 fn session_config_with_exp_assignments_serializes() {
5456 let assignments = serde_json::json!({
5457 "Parameters": { "copilot_exp_flag": "treatment" },
5458 "AssignmentContext": "ctx-123",
5459 });
5460 let (wire, _runtime) = SessionConfig::default()
5461 .with_exp_assignments(assignments.clone())
5462 .into_wire(Some(SessionId::from("exp-on")))
5463 .expect("no duplicate handlers");
5464 let json = serde_json::to_value(&wire).unwrap();
5465 assert_eq!(json["expAssignments"], assignments);
5466
5467 let (empty_wire, _) = SessionConfig::default()
5469 .into_wire(Some(SessionId::from("exp-unset")))
5470 .expect("no duplicate handlers");
5471 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5472 assert!(empty_json.get("expAssignments").is_none());
5473 }
5474
5475 #[test]
5476 fn resume_session_config_with_exp_assignments_serializes() {
5477 let assignments = serde_json::json!({
5478 "Parameters": { "copilot_exp_flag": "treatment" },
5479 "AssignmentContext": "ctx-456",
5480 });
5481 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
5482 .with_exp_assignments(assignments.clone())
5483 .into_wire()
5484 .expect("no duplicate handlers");
5485 let json = serde_json::to_value(&wire).unwrap();
5486 assert_eq!(json["expAssignments"], assignments);
5487
5488 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
5490 .into_wire()
5491 .expect("no duplicate handlers");
5492 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5493 assert!(empty_json.get("expAssignments").is_none());
5494 }
5495
5496 #[test]
5497 fn session_config_clone_preserves_exp_assignments() {
5498 let assignments = serde_json::json!({
5499 "Parameters": { "copilot_exp_flag": "treatment" },
5500 "AssignmentContext": "ctx-clone",
5501 });
5502 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
5503 let cloned = config.clone();
5504
5505 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5506
5507 let (wire, _runtime) = cloned
5508 .into_wire(Some(SessionId::from("exp-clone")))
5509 .expect("no duplicate handlers");
5510 let json = serde_json::to_value(&wire).unwrap();
5511 assert_eq!(json["expAssignments"], assignments);
5512 }
5513
5514 #[test]
5515 fn resume_session_config_clone_preserves_exp_assignments() {
5516 let assignments = serde_json::json!({
5517 "Parameters": { "copilot_exp_flag": "treatment" },
5518 "AssignmentContext": "ctx-clone-resume",
5519 });
5520 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
5521 .with_exp_assignments(assignments.clone());
5522 let cloned = config.clone();
5523
5524 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5525
5526 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
5527 let json = serde_json::to_value(&wire).unwrap();
5528 assert_eq!(json["expAssignments"], assignments);
5529 }
5530
5531 #[test]
5532 #[allow(clippy::field_reassign_with_default)]
5533 fn session_config_into_wire_serializes_bucket_b_fields() {
5534 use std::path::PathBuf;
5535
5536 use super::{CloudSessionOptions, CloudSessionRepository};
5537
5538 let mut cfg = SessionConfig::default();
5539 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5540 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5541 cfg.github_token = Some("ghs_secret".to_string());
5542 cfg.include_sub_agent_streaming_events = Some(false);
5543 cfg.enable_session_telemetry = Some(false);
5544 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
5545 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
5546 cfg.enable_on_demand_instruction_discovery = Some(false);
5547 cfg.cloud = Some(CloudSessionOptions::with_repository(
5548 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
5549 ));
5550
5551 let (wire, _runtime) = cfg
5552 .into_wire(Some(SessionId::from("custom-id")))
5553 .expect("no duplicate handlers");
5554 let wire_json = serde_json::to_value(&wire).unwrap();
5555 assert_eq!(wire_json["sessionId"], "custom-id");
5556 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5557 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5558 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5559 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
5560 assert_eq!(wire_json["enableSessionTelemetry"], false);
5561 assert_eq!(wire_json["reasoningSummary"], "concise");
5562 assert_eq!(wire_json["remoteSession"], "export");
5563 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5564 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
5565 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
5566 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
5567
5568 let (empty_wire, _) = SessionConfig::default()
5570 .into_wire(Some(SessionId::from("empty")))
5571 .expect("default has no duplicate handlers");
5572 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5573 assert!(empty_json.get("gitHubToken").is_none());
5574 assert!(empty_json.get("enableSessionTelemetry").is_none());
5575 assert!(empty_json.get("reasoningSummary").is_none());
5576 assert!(empty_json.get("remoteSession").is_none());
5577 assert!(
5578 empty_json
5579 .get("enableOnDemandInstructionDiscovery")
5580 .is_none()
5581 );
5582 assert!(empty_json.get("cloud").is_none());
5583 }
5584
5585 #[test]
5586 fn session_config_into_wire_serializes_named_providers_and_models() {
5587 let cfg = SessionConfig::default()
5588 .with_providers(vec![
5589 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
5590 .with_provider_type("openai")
5591 .with_wire_api("responses")
5592 .with_api_key("sk-test"),
5593 ])
5594 .with_models(vec![
5595 ProviderModelConfig::new("gpt-x", "my-openai")
5596 .with_wire_model("gpt-x-2025")
5597 .with_max_output_tokens(2048),
5598 ]);
5599
5600 let (wire, _) = cfg
5601 .into_wire(Some(SessionId::from("sess-providers")))
5602 .expect("no duplicate handlers");
5603 let wire_json = serde_json::to_value(&wire).unwrap();
5604 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
5605 assert_eq!(
5606 wire_json["providers"][0]["baseUrl"],
5607 "https://api.example.com/v1"
5608 );
5609 assert_eq!(wire_json["providers"][0]["type"], "openai");
5610 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
5611 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
5612 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
5613 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
5614 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
5615 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
5616
5617 let (empty_wire, _) = SessionConfig::default()
5618 .into_wire(Some(SessionId::from("empty")))
5619 .expect("default has no duplicate handlers");
5620 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5621 assert!(empty_json.get("providers").is_none());
5622 assert!(empty_json.get("models").is_none());
5623 }
5624
5625 #[test]
5626 fn resume_config_into_wire_serializes_named_providers_and_models() {
5627 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
5628 .with_providers(vec![
5629 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
5630 .with_provider_type("azure")
5631 .with_azure(AzureProviderOptions {
5632 api_version: Some("2024-10-21".to_string()),
5633 }),
5634 ])
5635 .with_models(vec![
5636 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
5637 ]);
5638
5639 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5640 let wire_json = serde_json::to_value(&wire).unwrap();
5641 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
5642 assert_eq!(wire_json["providers"][0]["type"], "azure");
5643 assert_eq!(
5644 wire_json["providers"][0]["azure"]["apiVersion"],
5645 "2024-10-21"
5646 );
5647 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
5648 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
5649 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
5650
5651 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
5652 .into_wire()
5653 .expect("default has no duplicate handlers");
5654 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5655 assert!(empty_json.get("providers").is_none());
5656 assert!(empty_json.get("models").is_none());
5657 }
5658
5659 #[test]
5660 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
5661 use std::path::PathBuf;
5662
5663 let cfg = SessionConfig {
5664 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
5665 large_output: Some(
5666 LargeToolOutputConfig::new()
5667 .with_enabled(true)
5668 .with_max_size_bytes(1024)
5669 .with_output_directory(PathBuf::from("/tmp/large-output")),
5670 ),
5671 ..Default::default()
5672 };
5673
5674 let (wire, _) = cfg
5675 .into_wire(Some(SessionId::from("sess-1")))
5676 .expect("no duplicate handlers");
5677 let wire_json = serde_json::to_value(&wire).unwrap();
5678 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
5679 assert_eq!(wire_json["largeOutput"]["enabled"], true);
5680 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
5681 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
5682
5683 let (empty_wire, _) = SessionConfig::default()
5684 .into_wire(Some(SessionId::from("empty")))
5685 .expect("default has no duplicate handlers");
5686 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5687 assert!(empty_json.get("pluginDirectories").is_none());
5688 assert!(empty_json.get("largeOutput").is_none());
5689 }
5690
5691 #[test]
5692 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
5693 use std::path::PathBuf;
5694
5695 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5696 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5697 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5698 cfg.github_token = Some("ghs_secret".to_string());
5699 cfg.include_sub_agent_streaming_events = Some(true);
5700 cfg.enable_session_telemetry = Some(false);
5701 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
5702 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
5703 cfg.enable_on_demand_instruction_discovery = Some(false);
5704
5705 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5706 let wire_json = serde_json::to_value(&wire).unwrap();
5707 assert_eq!(wire_json["sessionId"], "sess-1");
5708 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5709 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5710 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5711 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
5712 assert_eq!(wire_json["enableSessionTelemetry"], false);
5713 assert_eq!(wire_json["reasoningSummary"], "detailed");
5714 assert_eq!(wire_json["remoteSession"], "on");
5715 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5716
5717 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5719 .into_wire()
5720 .expect("default resume has no duplicate handlers");
5721 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5722 assert!(empty_json.get("reasoningSummary").is_none());
5723 assert!(empty_json.get("remoteSession").is_none());
5724 assert!(
5725 empty_json
5726 .get("enableOnDemandInstructionDiscovery")
5727 .is_none()
5728 );
5729 }
5730
5731 #[test]
5732 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
5733 use std::path::PathBuf;
5734
5735 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5736 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
5737 cfg.large_output = Some(
5738 LargeToolOutputConfig::new()
5739 .with_enabled(false)
5740 .with_max_size_bytes(2048)
5741 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
5742 );
5743
5744 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5745 let wire_json = serde_json::to_value(&wire).unwrap();
5746 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
5747 assert_eq!(wire_json["largeOutput"]["enabled"], false);
5748 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
5749 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
5750
5751 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5752 .into_wire()
5753 .expect("default resume has no duplicate handlers");
5754 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5755 assert!(empty_json.get("pluginDirectories").is_none());
5756 assert!(empty_json.get("largeOutput").is_none());
5757 }
5758
5759 #[test]
5760 fn session_config_builder_composes() {
5761 use indexmap::IndexMap;
5762
5763 let cfg = SessionConfig::default()
5764 .with_session_id(SessionId::from("sess-1"))
5765 .with_model("claude-sonnet-4")
5766 .with_client_name("test-app")
5767 .with_reasoning_effort("medium")
5768 .with_reasoning_summary(ReasoningSummary::Concise)
5769 .with_context_tier("long_context")
5770 .with_streaming(true)
5771 .with_tools([Tool::new("greet")])
5772 .with_available_tools(["bash", "view"])
5773 .with_excluded_tools(["dangerous"])
5774 .with_mcp_servers(IndexMap::new())
5775 .with_mcp_oauth_token_storage("persistent")
5776 .with_enable_config_discovery(true)
5777 .with_enable_on_demand_instruction_discovery(true)
5778 .with_skill_directories([PathBuf::from("/tmp/skills")])
5779 .with_disabled_skills(["broken-skill"])
5780 .with_agent("researcher")
5781 .with_config_directory(PathBuf::from("/tmp/config"))
5782 .with_working_directory(PathBuf::from("/tmp/work"))
5783 .with_github_token("ghp_test")
5784 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5785 .with_enable_session_telemetry(false)
5786 .with_include_sub_agent_streaming_events(false)
5787 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5788
5789 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
5790 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
5791 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5792 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
5793 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
5794 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
5795 assert_eq!(cfg.streaming, Some(true));
5796 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5797 assert_eq!(
5798 cfg.available_tools.as_deref(),
5799 Some(&["bash".to_string(), "view".to_string()][..])
5800 );
5801 assert_eq!(
5802 cfg.excluded_tools.as_deref(),
5803 Some(&["dangerous".to_string()][..])
5804 );
5805 assert!(cfg.mcp_servers.is_some());
5806 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5807 assert_eq!(cfg.enable_config_discovery, Some(true));
5808 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
5809 assert_eq!(
5810 cfg.skill_directories.as_deref(),
5811 Some(&[PathBuf::from("/tmp/skills")][..])
5812 );
5813 assert_eq!(
5814 cfg.disabled_skills.as_deref(),
5815 Some(&["broken-skill".to_string()][..])
5816 );
5817 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5818 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5819 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5820 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5821 assert_eq!(
5822 cfg.capi,
5823 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5824 );
5825 assert_eq!(cfg.enable_session_telemetry, Some(false));
5826 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
5827 assert_eq!(
5828 cfg.extension_info,
5829 Some(ExtensionInfo::new("github-app", "counter"))
5830 );
5831 }
5832
5833 #[test]
5834 fn resume_session_config_builder_composes() {
5835 use indexmap::IndexMap;
5836
5837 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
5838 .with_client_name("test-app")
5839 .with_reasoning_summary(ReasoningSummary::None)
5840 .with_context_tier("default")
5841 .with_streaming(true)
5842 .with_tools([Tool::new("greet")])
5843 .with_available_tools(["bash", "view"])
5844 .with_excluded_tools(["dangerous"])
5845 .with_mcp_servers(IndexMap::new())
5846 .with_mcp_oauth_token_storage("persistent")
5847 .with_enable_config_discovery(true)
5848 .with_enable_on_demand_instruction_discovery(false)
5849 .with_skill_directories([PathBuf::from("/tmp/skills")])
5850 .with_disabled_skills(["broken-skill"])
5851 .with_agent("researcher")
5852 .with_config_directory(PathBuf::from("/tmp/config"))
5853 .with_working_directory(PathBuf::from("/tmp/work"))
5854 .with_github_token("ghp_test")
5855 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5856 .with_enable_session_telemetry(false)
5857 .with_include_sub_agent_streaming_events(true)
5858 .with_suppress_resume_event(true)
5859 .with_continue_pending_work(true)
5860 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5861
5862 assert_eq!(cfg.session_id.as_str(), "sess-2");
5863 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5864 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
5865 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
5866 assert_eq!(cfg.streaming, Some(true));
5867 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5868 assert_eq!(
5869 cfg.available_tools.as_deref(),
5870 Some(&["bash".to_string(), "view".to_string()][..])
5871 );
5872 assert_eq!(
5873 cfg.excluded_tools.as_deref(),
5874 Some(&["dangerous".to_string()][..])
5875 );
5876 assert!(cfg.mcp_servers.is_some());
5877 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5878 assert_eq!(cfg.enable_config_discovery, Some(true));
5879 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
5880 assert_eq!(
5881 cfg.skill_directories.as_deref(),
5882 Some(&[PathBuf::from("/tmp/skills")][..])
5883 );
5884 assert_eq!(
5885 cfg.disabled_skills.as_deref(),
5886 Some(&["broken-skill".to_string()][..])
5887 );
5888 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5889 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5890 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5891 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5892 assert_eq!(
5893 cfg.capi,
5894 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5895 );
5896 assert_eq!(cfg.enable_session_telemetry, Some(false));
5897 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
5898 assert_eq!(cfg.suppress_resume_event, Some(true));
5899 assert_eq!(cfg.continue_pending_work, Some(true));
5900 assert_eq!(
5901 cfg.extension_info,
5902 Some(ExtensionInfo::new("github-app", "counter"))
5903 );
5904 }
5905
5906 #[test]
5910 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
5911 let cfg =
5912 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
5913 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5914 let json = serde_json::to_value(&wire).unwrap();
5915 assert_eq!(json["continuePendingWork"], true);
5916
5917 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5919 .into_wire()
5920 .expect("no duplicate handlers");
5921 let json = serde_json::to_value(&wire).unwrap();
5922 assert!(json.get("continuePendingWork").is_none());
5923 }
5924
5925 #[test]
5929 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
5930 let cfg =
5931 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
5932 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5933 let json = serde_json::to_value(&wire).unwrap();
5934 assert_eq!(json["disableResume"], true);
5935 assert!(json.get("suppressResumeEvent").is_none());
5936 }
5937
5938 #[test]
5941 fn session_config_serializes_instruction_directories_to_camel_case() {
5942 let cfg =
5943 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
5944 let (wire, _) = cfg
5945 .into_wire(Some(SessionId::from("instr-on")))
5946 .expect("no duplicate handlers");
5947 let json = serde_json::to_value(&wire).unwrap();
5948 assert_eq!(
5949 json["instructionDirectories"],
5950 serde_json::json!(["/tmp/instr"])
5951 );
5952
5953 let (wire, _) = SessionConfig::default()
5955 .into_wire(Some(SessionId::from("instr-off")))
5956 .expect("no duplicate handlers");
5957 let json = serde_json::to_value(&wire).unwrap();
5958 assert!(json.get("instructionDirectories").is_none());
5959 }
5960
5961 #[test]
5964 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
5965 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
5966 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
5967 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5968 let json = serde_json::to_value(&wire).unwrap();
5969 assert_eq!(
5970 json["instructionDirectories"],
5971 serde_json::json!(["/tmp/instr"])
5972 );
5973
5974 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5975 .into_wire()
5976 .expect("no duplicate handlers");
5977 let json = serde_json::to_value(&wire).unwrap();
5978 assert!(json.get("instructionDirectories").is_none());
5979 }
5980
5981 #[test]
5982 fn custom_agent_config_builder_composes() {
5983 use indexmap::IndexMap;
5984
5985 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
5986 .with_display_name("Research Assistant")
5987 .with_description("Investigates technical questions.")
5988 .with_tools(["bash", "view"])
5989 .with_mcp_servers(IndexMap::new())
5990 .with_infer(true)
5991 .with_skills(["rust-coding-skill"]);
5992
5993 assert_eq!(cfg.name, "researcher");
5994 assert_eq!(cfg.prompt, "You are a research assistant.");
5995 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
5996 assert_eq!(
5997 cfg.description.as_deref(),
5998 Some("Investigates technical questions.")
5999 );
6000 assert_eq!(
6001 cfg.tools.as_deref(),
6002 Some(&["bash".to_string(), "view".to_string()][..])
6003 );
6004 assert!(cfg.mcp_servers.is_some());
6005 assert_eq!(cfg.infer, Some(true));
6006 assert_eq!(
6007 cfg.skills.as_deref(),
6008 Some(&["rust-coding-skill".to_string()][..])
6009 );
6010 }
6011
6012 #[test]
6013 fn mcp_servers_serialize_in_insertion_order() {
6014 use indexmap::IndexMap;
6015
6016 let order = [
6022 "zebra", "quartz", "delta", "ivy", "mango", "bravo", "xenon", "amber", "falcon",
6023 "ceres", "nova", "kelp", "otter", "yodel", "plum", "garnet",
6024 ];
6025 let mut servers = IndexMap::new();
6026 for name in order {
6027 servers.insert(
6028 name.to_string(),
6029 McpServerConfig::Stdio(McpStdioServerConfig {
6030 command: "run".to_string(),
6031 ..Default::default()
6032 }),
6033 );
6034 }
6035
6036 let (wire, _runtime) = SessionConfig::default()
6037 .with_mcp_servers(servers)
6038 .into_wire(None)
6039 .expect("into_wire should succeed");
6040 let json = serde_json::to_string(&wire).expect("serialize wire");
6041
6042 let positions: Vec<usize> = order
6043 .iter()
6044 .map(|name| {
6045 json.find(&format!("\"{name}\""))
6046 .unwrap_or_else(|| panic!("server {name} missing from wire JSON"))
6047 })
6048 .collect();
6049 let mut ascending = positions.clone();
6050 ascending.sort_unstable();
6051 assert_eq!(
6052 positions, ascending,
6053 "mcp server keys must serialize in insertion order: {json}"
6054 );
6055 }
6056
6057 #[test]
6058 fn infinite_session_config_builder_composes() {
6059 let cfg = InfiniteSessionConfig::new()
6060 .with_enabled(true)
6061 .with_background_compaction_threshold(0.75)
6062 .with_buffer_exhaustion_threshold(0.92);
6063
6064 assert_eq!(cfg.enabled, Some(true));
6065 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
6066 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
6067 }
6068
6069 #[test]
6070 fn provider_config_builder_composes() {
6071 use std::collections::HashMap;
6072
6073 let mut headers = HashMap::new();
6074 headers.insert("X-Custom".to_string(), "value".to_string());
6075
6076 let cfg = ProviderConfig::new("https://api.example.com")
6077 .with_provider_type("openai")
6078 .with_wire_api("completions")
6079 .with_transport("websockets")
6080 .with_api_key("sk-test")
6081 .with_bearer_token("bearer-test")
6082 .with_headers(headers)
6083 .with_model_id("gpt-4")
6084 .with_wire_model("azure-gpt-4-deployment")
6085 .with_max_prompt_tokens(8192)
6086 .with_max_output_tokens(2048);
6087
6088 assert_eq!(cfg.base_url, "https://api.example.com");
6089 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
6090 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
6091 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
6092 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
6093 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
6094 assert_eq!(
6095 cfg.headers
6096 .as_ref()
6097 .and_then(|h| h.get("X-Custom"))
6098 .map(String::as_str),
6099 Some("value"),
6100 );
6101 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
6102 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
6103 assert_eq!(cfg.max_prompt_tokens, Some(8192));
6104 assert_eq!(cfg.max_output_tokens, Some(2048));
6105
6106 let wire = serde_json::to_value(&cfg).unwrap();
6108 assert_eq!(wire["modelId"], "gpt-4");
6109 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
6110 assert_eq!(wire["maxPromptTokens"], 8192);
6111 assert_eq!(wire["maxOutputTokens"], 2048);
6112
6113 let unset = ProviderConfig::new("https://api.example.com");
6114 let wire_unset = serde_json::to_value(&unset).unwrap();
6115 assert!(wire_unset.get("modelId").is_none());
6116 assert!(wire_unset.get("wireModel").is_none());
6117 assert!(wire_unset.get("maxPromptTokens").is_none());
6118 assert!(wire_unset.get("maxOutputTokens").is_none());
6119 }
6120
6121 #[test]
6122 fn capi_session_options_builder_composes_and_serializes() {
6123 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
6124
6125 assert_eq!(cfg.enable_web_socket_responses, Some(false));
6126
6127 let wire = serde_json::to_value(&cfg).unwrap();
6128 assert_eq!(
6129 wire,
6130 serde_json::json!({ "enableWebSocketResponses": false })
6131 );
6132
6133 let unset = CapiSessionOptions::new();
6134 let wire_unset = serde_json::to_value(&unset).unwrap();
6135 assert!(wire_unset.get("enableWebSocketResponses").is_none());
6136 }
6137
6138 #[test]
6139 fn session_config_with_capi_serializes() {
6140 let (wire, _) = SessionConfig::default()
6141 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6142 .into_wire(Some(SessionId::from("capi-create")))
6143 .expect("no duplicate handlers");
6144 let json = serde_json::to_value(&wire).unwrap();
6145 assert_eq!(
6146 json["capi"],
6147 serde_json::json!({ "enableWebSocketResponses": false })
6148 );
6149
6150 let (empty_wire, _) = SessionConfig::default()
6151 .into_wire(Some(SessionId::from("capi-create-unset")))
6152 .expect("no duplicate handlers");
6153 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6154 assert!(empty_json.get("capi").is_none());
6155 }
6156
6157 #[test]
6158 fn resume_session_config_with_capi_serializes() {
6159 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
6160 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6161 .into_wire()
6162 .expect("no duplicate handlers");
6163 let json = serde_json::to_value(&wire).unwrap();
6164 assert_eq!(
6165 json["capi"],
6166 serde_json::json!({ "enableWebSocketResponses": false })
6167 );
6168
6169 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
6170 .into_wire()
6171 .expect("no duplicate handlers");
6172 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6173 assert!(empty_json.get("capi").is_none());
6174 }
6175
6176 #[test]
6177 fn system_message_config_builder_composes() {
6178 use std::collections::HashMap;
6179
6180 let cfg = SystemMessageConfig::new()
6181 .with_mode("replace")
6182 .with_content("Custom system message.")
6183 .with_sections(HashMap::new());
6184
6185 assert_eq!(cfg.mode.as_deref(), Some("replace"));
6186 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
6187 assert!(cfg.sections.is_some());
6188 }
6189
6190 #[test]
6191 fn delivery_mode_serializes_to_kebab_case_strings() {
6192 assert_eq!(
6193 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
6194 "\"enqueue\""
6195 );
6196 assert_eq!(
6197 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
6198 "\"immediate\""
6199 );
6200 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
6201 assert_eq!(parsed, DeliveryMode::Immediate);
6202 }
6203
6204 #[test]
6205 fn agent_mode_serializes_to_kebab_case_strings() {
6206 assert_eq!(
6207 serde_json::to_string(&AgentMode::Interactive).unwrap(),
6208 "\"interactive\""
6209 );
6210 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
6211 assert_eq!(
6212 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
6213 "\"autopilot\""
6214 );
6215 assert_eq!(
6216 serde_json::to_string(&AgentMode::Shell).unwrap(),
6217 "\"shell\""
6218 );
6219 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
6220 assert_eq!(parsed, AgentMode::Plan);
6221 }
6222
6223 #[test]
6224 fn connection_state_distinguishes_variants() {
6225 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
6228 }
6229
6230 #[test]
6236 fn session_event_round_trips_agent_id_on_envelope() {
6237 let wire = json!({
6238 "id": "evt-1",
6239 "timestamp": "2026-04-30T12:00:00Z",
6240 "parentId": null,
6241 "agentId": "sub-agent-42",
6242 "type": "assistant.message",
6243 "data": { "message": "hi" }
6244 });
6245
6246 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
6247 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6248
6249 let roundtripped = serde_json::to_value(&event).unwrap();
6251 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6252
6253 let main_agent_event: SessionEvent = serde_json::from_value(json!({
6255 "id": "evt-2",
6256 "timestamp": "2026-04-30T12:00:01Z",
6257 "parentId": null,
6258 "type": "session.idle",
6259 "data": {}
6260 }))
6261 .unwrap();
6262 assert!(main_agent_event.agent_id.is_none());
6263 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
6264 assert!(roundtripped.get("agentId").is_none());
6265 }
6266
6267 #[test]
6269 fn typed_session_event_round_trips_agent_id_on_envelope() {
6270 let wire = json!({
6271 "id": "evt-1",
6272 "timestamp": "2026-04-30T12:00:00Z",
6273 "parentId": null,
6274 "agentId": "sub-agent-42",
6275 "type": "session.idle",
6276 "data": {}
6277 });
6278
6279 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
6280 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6281
6282 let roundtripped = serde_json::to_value(&event).unwrap();
6283 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6284 }
6285
6286 #[test]
6287 fn connection_state_variants_compile() {
6288 let _ = ConnectionState::Disconnected;
6292 let _ = ConnectionState::Connecting;
6293 let _ = ConnectionState::Connected;
6294 let _ = ConnectionState::Error;
6295 }
6296
6297 #[test]
6298 fn deserializes_runtime_attachment_variants() {
6299 let attachments: Vec<Attachment> = serde_json::from_value(json!([
6300 {
6301 "type": "file",
6302 "path": "/tmp/file.rs",
6303 "displayName": "file.rs",
6304 "lineRange": { "start": 7, "end": 12 }
6305 },
6306 {
6307 "type": "directory",
6308 "path": "/tmp/project",
6309 "displayName": "project"
6310 },
6311 {
6312 "type": "selection",
6313 "filePath": "/tmp/lib.rs",
6314 "displayName": "lib.rs",
6315 "text": "fn main() {}",
6316 "selection": {
6317 "start": { "line": 1, "character": 2 },
6318 "end": { "line": 3, "character": 4 }
6319 }
6320 },
6321 {
6322 "type": "blob",
6323 "data": "Zm9v",
6324 "mimeType": "image/png",
6325 "displayName": "image.png"
6326 },
6327 {
6328 "type": "github_reference",
6329 "number": 42,
6330 "title": "Fix rendering",
6331 "referenceType": "issue",
6332 "state": "open",
6333 "url": "https://github.com/example/repo/issues/42"
6334 }
6335 ]))
6336 .expect("attachments should deserialize");
6337
6338 assert_eq!(attachments.len(), 5);
6339 assert!(matches!(
6340 &attachments[0],
6341 Attachment::File {
6342 path,
6343 display_name,
6344 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
6345 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
6346 ));
6347 assert!(matches!(
6348 &attachments[1],
6349 Attachment::Directory { path, display_name }
6350 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
6351 ));
6352 assert!(matches!(
6353 &attachments[2],
6354 Attachment::Selection {
6355 file_path,
6356 display_name,
6357 selection:
6358 AttachmentSelectionRange {
6359 start: AttachmentSelectionPosition { line: 1, character: 2 },
6360 end: AttachmentSelectionPosition { line: 3, character: 4 },
6361 },
6362 ..
6363 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
6364 ));
6365 assert!(matches!(
6366 &attachments[3],
6367 Attachment::Blob {
6368 data,
6369 mime_type,
6370 display_name,
6371 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
6372 ));
6373 assert!(matches!(
6374 &attachments[4],
6375 Attachment::GitHubReference {
6376 number: 42,
6377 title,
6378 reference_type: GitHubReferenceType::Issue,
6379 state,
6380 url,
6381 } if title == "Fix rendering"
6382 && state == "open"
6383 && url == "https://github.com/example/repo/issues/42"
6384 ));
6385 }
6386
6387 #[test]
6388 fn ensures_display_names_for_variants_that_support_them() {
6389 let mut attachments = vec![
6390 Attachment::File {
6391 path: PathBuf::from("/tmp/file.rs"),
6392 display_name: None,
6393 line_range: None,
6394 },
6395 Attachment::Selection {
6396 file_path: PathBuf::from("/tmp/src/lib.rs"),
6397 display_name: None,
6398 text: "fn main() {}".to_string(),
6399 selection: AttachmentSelectionRange {
6400 start: AttachmentSelectionPosition {
6401 line: 0,
6402 character: 0,
6403 },
6404 end: AttachmentSelectionPosition {
6405 line: 0,
6406 character: 10,
6407 },
6408 },
6409 },
6410 Attachment::Blob {
6411 data: "Zm9v".to_string(),
6412 mime_type: "image/png".to_string(),
6413 display_name: None,
6414 },
6415 Attachment::GitHubReference {
6416 number: 7,
6417 title: "Track regressions".to_string(),
6418 reference_type: GitHubReferenceType::Issue,
6419 state: "open".to_string(),
6420 url: "https://example.com/issues/7".to_string(),
6421 },
6422 ];
6423
6424 ensure_attachment_display_names(&mut attachments);
6425
6426 assert_eq!(attachments[0].display_name(), Some("file.rs"));
6427 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
6428 assert_eq!(attachments[2].display_name(), Some("attachment"));
6429 assert_eq!(attachments[3].display_name(), None);
6430 assert_eq!(
6431 attachments[3].label(),
6432 Some("Track regressions".to_string())
6433 );
6434 }
6435
6436 #[test]
6437 fn github_anchored_attachment_variants_round_trip() {
6438 let cases = vec![
6439 (
6440 "github_commit",
6441 json!({
6442 "type": "github_commit",
6443 "message": "Fix the thing",
6444 "oid": "abc123",
6445 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
6446 "url": "https://github.com/octocat/repo/commit/abc123"
6447 }),
6448 ),
6449 (
6450 "github_release",
6451 json!({
6452 "type": "github_release",
6453 "name": "v1.2.3",
6454 "repo": { "name": "repo", "owner": "octocat" },
6455 "tagName": "v1.2.3",
6456 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
6457 }),
6458 ),
6459 (
6460 "github_actions_job",
6461 json!({
6462 "type": "github_actions_job",
6463 "conclusion": "failure",
6464 "jobId": 99,
6465 "jobName": "build",
6466 "repo": { "name": "repo", "owner": "octocat" },
6467 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
6468 "workflowName": "CI"
6469 }),
6470 ),
6471 (
6472 "github_repository",
6473 json!({
6474 "type": "github_repository",
6475 "description": "An example repository",
6476 "ref": "main",
6477 "repo": { "name": "repo", "owner": "octocat" },
6478 "url": "https://github.com/octocat/repo"
6479 }),
6480 ),
6481 (
6482 "github_file_diff",
6483 json!({
6484 "type": "github_file_diff",
6485 "base": {
6486 "path": "src/lib.rs",
6487 "ref": "main",
6488 "repo": { "name": "repo", "owner": "octocat" }
6489 },
6490 "head": {
6491 "path": "src/lib.rs",
6492 "ref": "feature",
6493 "repo": { "name": "repo", "owner": "octocat" }
6494 },
6495 "url": "https://github.com/octocat/repo/compare/main...feature"
6496 }),
6497 ),
6498 (
6499 "github_tree_comparison",
6500 json!({
6501 "type": "github_tree_comparison",
6502 "base": {
6503 "repo": { "name": "repo", "owner": "octocat" },
6504 "revision": "main"
6505 },
6506 "head": {
6507 "repo": { "name": "repo", "owner": "octocat" },
6508 "revision": "feature"
6509 },
6510 "url": "https://github.com/octocat/repo/compare/main...feature"
6511 }),
6512 ),
6513 (
6514 "github_url",
6515 json!({
6516 "type": "github_url",
6517 "url": "https://github.com/octocat/repo/wiki"
6518 }),
6519 ),
6520 (
6521 "github_file",
6522 json!({
6523 "type": "github_file",
6524 "path": "src/main.rs",
6525 "ref": "main",
6526 "repo": { "name": "repo", "owner": "octocat" },
6527 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
6528 }),
6529 ),
6530 (
6531 "github_snippet",
6532 json!({
6533 "type": "github_snippet",
6534 "lineRange": { "start": 10, "end": 20 },
6535 "path": "src/main.rs",
6536 "ref": "main",
6537 "repo": { "name": "repo", "owner": "octocat" },
6538 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
6539 }),
6540 ),
6541 ];
6542
6543 for (expected_type, input) in cases {
6544 let attachment: Attachment = serde_json::from_value(input.clone())
6545 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
6546
6547 let serialized_string = serde_json::to_string(&attachment)
6552 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
6553
6554 assert_eq!(
6556 serialized_string.matches("\"type\":").count(),
6557 1,
6558 "{expected_type} must serialize a single `type` key"
6559 );
6560
6561 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
6562 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
6563 assert_eq!(
6564 serialized.get("type").and_then(|value| value.as_str()),
6565 Some(expected_type),
6566 "{expected_type} must serialize the correct discriminator"
6567 );
6568
6569 assert_eq!(
6571 serialized, input,
6572 "{expected_type} should round-trip without data loss"
6573 );
6574 let reparsed: Attachment = serde_json::from_value(serialized)
6575 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
6576 assert_eq!(
6577 reparsed, attachment,
6578 "{expected_type} should re-deserialize to the same value"
6579 );
6580 }
6581 }
6582}
6583
6584#[cfg(test)]
6585mod permission_builder_tests {
6586 use std::sync::Arc;
6587
6588 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
6589 use crate::permission;
6590 use crate::types::{
6591 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
6592 SessionId,
6593 };
6594
6595 fn data() -> PermissionRequestData {
6596 PermissionRequestData {
6597 extra: serde_json::json!({"tool": "shell"}),
6598 ..Default::default()
6599 }
6600 }
6601
6602 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6605 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6606 }
6607
6608 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6609 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6610 }
6611
6612 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
6613 handler
6614 .handle(SessionId::from("s1"), RequestId::new("1"), data())
6615 .await
6616 }
6617
6618 #[tokio::test]
6619 async fn approve_all_with_handler_present_approves() {
6620 let cfg = SessionConfig::default()
6621 .with_permission_handler(Arc::new(ApproveAllHandler))
6622 .approve_all_permissions();
6623 let h = resolve_create(cfg).expect("policy + handler yields handler");
6624 assert!(matches!(
6625 dispatch(&h).await,
6626 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6627 ));
6628 }
6629
6630 #[tokio::test]
6631 async fn approve_all_standalone_produces_handler() {
6632 let cfg = SessionConfig::default().approve_all_permissions();
6633 let h = resolve_create(cfg).expect("policy alone yields handler");
6634 assert!(matches!(
6635 dispatch(&h).await,
6636 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6637 ));
6638 }
6639
6640 #[tokio::test]
6643 async fn approve_all_is_order_independent() {
6644 let a = SessionConfig::default()
6645 .with_permission_handler(Arc::new(ApproveAllHandler))
6646 .approve_all_permissions();
6647 let b = SessionConfig::default()
6648 .approve_all_permissions()
6649 .with_permission_handler(Arc::new(ApproveAllHandler));
6650 let ha = resolve_create(a).unwrap();
6651 let hb = resolve_create(b).unwrap();
6652 assert!(matches!(
6653 dispatch(&ha).await,
6654 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6655 ));
6656 assert!(matches!(
6657 dispatch(&hb).await,
6658 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6659 ));
6660 }
6661
6662 #[tokio::test]
6663 async fn deny_all_is_order_independent() {
6664 let a = SessionConfig::default()
6665 .with_permission_handler(Arc::new(ApproveAllHandler))
6666 .deny_all_permissions();
6667 let b = SessionConfig::default()
6668 .deny_all_permissions()
6669 .with_permission_handler(Arc::new(ApproveAllHandler));
6670 let ha = resolve_create(a).unwrap();
6671 let hb = resolve_create(b).unwrap();
6672 assert!(matches!(
6673 dispatch(&ha).await,
6674 PermissionResult::Decision(PermissionDecision::Reject(_))
6675 ));
6676 assert!(matches!(
6677 dispatch(&hb).await,
6678 PermissionResult::Decision(PermissionDecision::Reject(_))
6679 ));
6680 }
6681
6682 #[tokio::test]
6683 async fn approve_permissions_if_consults_predicate() {
6684 let cfg = SessionConfig::default().approve_permissions_if(|d| {
6685 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6686 });
6687 let h = resolve_create(cfg).unwrap();
6688 assert!(matches!(
6689 dispatch(&h).await,
6690 PermissionResult::Decision(PermissionDecision::Reject(_))
6691 ));
6692 }
6693
6694 #[tokio::test]
6695 async fn approve_permissions_if_is_order_independent() {
6696 let predicate = |d: &PermissionRequestData| {
6697 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6698 };
6699 let a = SessionConfig::default()
6700 .with_permission_handler(Arc::new(ApproveAllHandler))
6701 .approve_permissions_if(predicate);
6702 let b = SessionConfig::default()
6703 .approve_permissions_if(predicate)
6704 .with_permission_handler(Arc::new(ApproveAllHandler));
6705 let ha = resolve_create(a).unwrap();
6706 let hb = resolve_create(b).unwrap();
6707 assert!(matches!(
6708 dispatch(&ha).await,
6709 PermissionResult::Decision(PermissionDecision::Reject(_))
6710 ));
6711 assert!(matches!(
6712 dispatch(&hb).await,
6713 PermissionResult::Decision(PermissionDecision::Reject(_))
6714 ));
6715 }
6716
6717 #[tokio::test]
6718 async fn resume_session_config_approve_all_works() {
6719 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
6720 .with_permission_handler(Arc::new(ApproveAllHandler))
6721 .approve_all_permissions();
6722 let h = resolve_resume(cfg).unwrap();
6723 assert!(matches!(
6724 dispatch(&h).await,
6725 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6726 ));
6727 }
6728
6729 #[tokio::test]
6730 async fn resume_session_config_approve_all_is_order_independent() {
6731 let a = ResumeSessionConfig::new(SessionId::from("s1"))
6732 .with_permission_handler(Arc::new(ApproveAllHandler))
6733 .approve_all_permissions();
6734 let b = ResumeSessionConfig::new(SessionId::from("s1"))
6735 .approve_all_permissions()
6736 .with_permission_handler(Arc::new(ApproveAllHandler));
6737 let ha = resolve_resume(a).unwrap();
6738 let hb = resolve_resume(b).unwrap();
6739 assert!(matches!(
6740 dispatch(&ha).await,
6741 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6742 ));
6743 assert!(matches!(
6744 dispatch(&hb).await,
6745 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6746 ));
6747 }
6748}