1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::time::Duration;
11
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14
15use crate::canvas::{CanvasDeclaration, CanvasHandler};
16pub use crate::copilot_request_handler::{
17 CopilotHttpRequest, CopilotHttpResponse, CopilotHttpResponseBody, CopilotRequestContext,
18 CopilotRequestError, CopilotRequestHandler, CopilotRequestTransport, CopilotWebSocketForwarder,
19 CopilotWebSocketForwarderBuilder, CopilotWebSocketHandler, CopilotWebSocketMessage,
20 CopilotWebSocketResponse, WebSocketTransform, forward_http,
21};
22use crate::generated::api_types::OpenCanvasInstance;
23use crate::generated::session_events::ReasoningSummary;
24pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig};
26use crate::handler::{
27 AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler,
28 PermissionHandler, UserInputHandler,
29};
30use crate::hooks::SessionHooks;
31use crate::provider_token::BearerTokenProvider;
32pub use crate::session_fs::{
33 DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig,
34 SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult,
35 SessionFsSqliteQueryType,
36};
37pub use crate::trace_context::{TraceContext, TraceContextProvider};
38use crate::transforms::SystemMessageTransform;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43#[allow(dead_code)]
44#[non_exhaustive]
45pub(crate) enum ConnectionState {
46 Disconnected,
48 Connecting,
50 Connected,
52 Error,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[non_exhaustive]
62pub enum SessionLifecycleEventType {
63 #[serde(rename = "session.created")]
65 Created,
66 #[serde(rename = "session.deleted")]
68 Deleted,
69 #[serde(rename = "session.updated")]
71 Updated,
72 #[serde(rename = "session.foreground")]
74 Foreground,
75 #[serde(rename = "session.background")]
77 Background,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82pub struct SessionLifecycleEventMetadata {
83 #[serde(rename = "startTime")]
85 pub start_time: String,
86 #[serde(rename = "modifiedTime")]
88 pub modified_time: String,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub summary: Option<String>,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct SessionLifecycleEvent {
98 #[serde(rename = "type")]
100 pub event_type: SessionLifecycleEventType,
101 #[serde(rename = "sessionId")]
103 pub session_id: SessionId,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub metadata: Option<SessionLifecycleEventMetadata>,
107}
108
109#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
115#[serde(transparent)]
116pub struct SessionId(String);
117
118impl SessionId {
119 pub fn new(id: impl Into<String>) -> Self {
121 Self(id.into())
122 }
123
124 pub fn as_str(&self) -> &str {
126 &self.0
127 }
128
129 pub fn into_inner(self) -> String {
131 self.0
132 }
133}
134
135impl std::ops::Deref for SessionId {
136 type Target = str;
137
138 fn deref(&self) -> &str {
139 &self.0
140 }
141}
142
143impl std::fmt::Display for SessionId {
144 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 f.write_str(&self.0)
146 }
147}
148
149impl From<String> for SessionId {
150 fn from(s: String) -> Self {
151 Self(s)
152 }
153}
154
155impl From<&str> for SessionId {
156 fn from(s: &str) -> Self {
157 Self(s.to_owned())
158 }
159}
160
161impl AsRef<str> for SessionId {
162 fn as_ref(&self) -> &str {
163 &self.0
164 }
165}
166
167impl std::borrow::Borrow<str> for SessionId {
168 fn borrow(&self) -> &str {
169 &self.0
170 }
171}
172
173impl From<SessionId> for String {
174 fn from(id: SessionId) -> String {
175 id.0
176 }
177}
178
179impl PartialEq<str> for SessionId {
180 fn eq(&self, other: &str) -> bool {
181 self.0 == other
182 }
183}
184
185impl PartialEq<String> for SessionId {
186 fn eq(&self, other: &String) -> bool {
187 &self.0 == other
188 }
189}
190
191impl PartialEq<SessionId> for String {
192 fn eq(&self, other: &SessionId) -> bool {
193 self == &other.0
194 }
195}
196
197impl PartialEq<&str> for SessionId {
198 fn eq(&self, other: &&str) -> bool {
199 self.0 == *other
200 }
201}
202
203impl PartialEq<&SessionId> for SessionId {
204 fn eq(&self, other: &&SessionId) -> bool {
205 self.0 == other.0
206 }
207}
208
209impl PartialEq<SessionId> for &SessionId {
210 fn eq(&self, other: &SessionId) -> bool {
211 self.0 == other.0
212 }
213}
214
215#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
221#[serde(transparent)]
222pub struct RequestId(String);
223
224impl RequestId {
225 pub fn new(id: impl Into<String>) -> Self {
227 Self(id.into())
228 }
229
230 pub fn into_inner(self) -> String {
232 self.0
233 }
234}
235
236impl std::ops::Deref for RequestId {
237 type Target = str;
238
239 fn deref(&self) -> &str {
240 &self.0
241 }
242}
243
244impl std::fmt::Display for RequestId {
245 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246 f.write_str(&self.0)
247 }
248}
249
250impl From<String> for RequestId {
251 fn from(s: String) -> Self {
252 Self(s)
253 }
254}
255
256impl From<&str> for RequestId {
257 fn from(s: &str) -> Self {
258 Self(s.to_owned())
259 }
260}
261
262impl AsRef<str> for RequestId {
263 fn as_ref(&self) -> &str {
264 &self.0
265 }
266}
267
268impl std::borrow::Borrow<str> for RequestId {
269 fn borrow(&self) -> &str {
270 &self.0
271 }
272}
273
274impl From<RequestId> for String {
275 fn from(id: RequestId) -> String {
276 id.0
277 }
278}
279
280impl PartialEq<str> for RequestId {
281 fn eq(&self, other: &str) -> bool {
282 self.0 == other
283 }
284}
285
286impl PartialEq<String> for RequestId {
287 fn eq(&self, other: &String) -> bool {
288 &self.0 == other
289 }
290}
291
292impl PartialEq<RequestId> for String {
293 fn eq(&self, other: &RequestId) -> bool {
294 self == &other.0
295 }
296}
297
298impl PartialEq<&str> for RequestId {
299 fn eq(&self, other: &&str) -> bool {
300 self.0 == *other
301 }
302}
303
304#[derive(Clone, Default, Serialize, Deserialize)]
319#[serde(rename_all = "camelCase")]
320#[non_exhaustive]
321pub struct Tool {
322 pub name: String,
324 #[serde(default, skip_serializing_if = "Option::is_none")]
327 pub namespaced_name: Option<String>,
328 #[serde(default)]
330 pub description: String,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub instructions: Option<String>,
334 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
336 pub parameters: HashMap<String, Value>,
337 #[serde(default, skip_serializing_if = "is_false")]
341 pub overrides_built_in_tool: bool,
342 #[serde(default, skip_serializing_if = "is_false")]
346 pub skip_permission: bool,
347 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub defer: Option<DeferMode>,
354 #[serde(skip)]
366 pub(crate) handler: Option<Arc<dyn crate::tool::ToolHandler>>,
367}
368
369#[inline]
370fn is_false(b: &bool) -> bool {
371 !*b
372}
373
374#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
377#[serde(rename_all = "lowercase")]
378pub enum DeferMode {
379 Auto,
381 Never,
383}
384
385impl Tool {
386 pub fn new(name: impl Into<String>) -> Self {
406 Self {
407 name: name.into(),
408 ..Default::default()
409 }
410 }
411
412 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
415 self.namespaced_name = Some(namespaced_name.into());
416 self
417 }
418
419 pub fn with_description(mut self, description: impl Into<String>) -> Self {
421 self.description = description.into();
422 self
423 }
424
425 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
427 self.instructions = Some(instructions.into());
428 self
429 }
430
431 pub fn with_parameters(mut self, parameters: Value) -> Self {
445 self.parameters = crate::tool::tool_parameters(parameters);
446 self
447 }
448
449 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
453 self.overrides_built_in_tool = overrides;
454 self
455 }
456
457 pub fn with_skip_permission(mut self, skip: bool) -> Self {
461 self.skip_permission = skip;
462 self
463 }
464
465 pub fn with_defer(mut self, defer: DeferMode) -> Self {
469 self.defer = Some(defer);
470 self
471 }
472
473 pub fn with_handler(mut self, handler: Arc<dyn crate::tool::ToolHandler>) -> Self {
477 self.handler = Some(handler);
478 self
479 }
480
481 pub fn handler(&self) -> Option<&Arc<dyn crate::tool::ToolHandler>> {
486 self.handler.as_ref()
487 }
488}
489
490impl std::fmt::Debug for Tool {
491 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
492 f.debug_struct("Tool")
493 .field("name", &self.name)
494 .field("namespaced_name", &self.namespaced_name)
495 .field("description", &self.description)
496 .field("instructions", &self.instructions)
497 .field("parameters", &self.parameters)
498 .field("overrides_built_in_tool", &self.overrides_built_in_tool)
499 .field("skip_permission", &self.skip_permission)
500 .field("defer", &self.defer)
501 .field(
502 "handler",
503 &self.handler.as_ref().map(|_| "<set>").unwrap_or("None"),
504 )
505 .finish()
506 }
507}
508
509#[non_exhaustive]
512#[derive(Debug, Clone)]
513pub struct CommandContext {
514 pub session_id: SessionId,
516 pub command: String,
518 pub command_name: String,
520 pub args: String,
522}
523
524#[async_trait::async_trait]
530pub trait CommandHandler: Send + Sync {
531 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
533}
534
535#[non_exhaustive]
541#[derive(Clone)]
542pub struct CommandDefinition {
543 pub name: String,
545 pub description: Option<String>,
547 pub handler: Arc<dyn CommandHandler>,
549}
550
551impl CommandDefinition {
552 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
555 Self {
556 name: name.into(),
557 description: None,
558 handler,
559 }
560 }
561
562 pub fn with_description(mut self, description: impl Into<String>) -> Self {
564 self.description = Some(description.into());
565 self
566 }
567}
568
569impl std::fmt::Debug for CommandDefinition {
570 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
571 f.debug_struct("CommandDefinition")
572 .field("name", &self.name)
573 .field("description", &self.description)
574 .field("handler", &"<set>")
575 .finish()
576 }
577}
578
579impl Serialize for CommandDefinition {
580 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
581 use serde::ser::SerializeStruct;
582 let len = if self.description.is_some() { 2 } else { 1 };
583 let mut state = serializer.serialize_struct("CommandDefinition", len)?;
584 state.serialize_field("name", &self.name)?;
585 if let Some(description) = &self.description {
586 state.serialize_field("description", description)?;
587 }
588 state.end()
589 }
590}
591
592#[derive(Debug, Clone, Default, Serialize, Deserialize)]
599#[serde(rename_all = "camelCase")]
600#[non_exhaustive]
601pub struct CustomAgentConfig {
602 pub name: String,
604 #[serde(default, skip_serializing_if = "Option::is_none")]
606 pub display_name: Option<String>,
607 #[serde(default, skip_serializing_if = "Option::is_none")]
609 pub description: Option<String>,
610 #[serde(default, skip_serializing_if = "Option::is_none")]
612 pub tools: Option<Vec<String>>,
613 pub prompt: String,
615 #[serde(default, skip_serializing_if = "Option::is_none")]
617 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
618 #[serde(default, skip_serializing_if = "Option::is_none")]
620 pub infer: Option<bool>,
621 #[serde(default, skip_serializing_if = "Option::is_none")]
623 pub skills: Option<Vec<String>>,
624 #[serde(default, skip_serializing_if = "Option::is_none")]
629 pub model: Option<String>,
630}
631
632impl CustomAgentConfig {
633 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
640 Self {
641 name: name.into(),
642 prompt: prompt.into(),
643 ..Self::default()
644 }
645 }
646
647 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
649 self.display_name = Some(display_name.into());
650 self
651 }
652
653 pub fn with_description(mut self, description: impl Into<String>) -> Self {
655 self.description = Some(description.into());
656 self
657 }
658
659 pub fn with_tools<I, S>(mut self, tools: I) -> Self
662 where
663 I: IntoIterator<Item = S>,
664 S: Into<String>,
665 {
666 self.tools = Some(tools.into_iter().map(Into::into).collect());
667 self
668 }
669
670 pub fn with_mcp_servers(mut self, mcp_servers: HashMap<String, McpServerConfig>) -> Self {
672 self.mcp_servers = Some(mcp_servers);
673 self
674 }
675
676 pub fn with_infer(mut self, infer: bool) -> Self {
678 self.infer = Some(infer);
679 self
680 }
681
682 pub fn with_skills<I, S>(mut self, skills: I) -> Self
684 where
685 I: IntoIterator<Item = S>,
686 S: Into<String>,
687 {
688 self.skills = Some(skills.into_iter().map(Into::into).collect());
689 self
690 }
691
692 pub fn with_model(mut self, model: impl Into<String>) -> Self {
694 self.model = Some(model.into());
695 self
696 }
697}
698
699#[derive(Debug, Clone, Default, Serialize, Deserialize)]
706#[serde(rename_all = "camelCase")]
707pub struct DefaultAgentConfig {
708 #[serde(default, skip_serializing_if = "Option::is_none")]
710 pub excluded_tools: Option<Vec<String>>,
711}
712
713#[derive(Debug, Clone, Default, Serialize, Deserialize)]
719#[serde(rename_all = "camelCase")]
720#[non_exhaustive]
721pub struct LargeToolOutputConfig {
722 #[serde(default, skip_serializing_if = "Option::is_none")]
724 pub enabled: Option<bool>,
725 #[serde(default, skip_serializing_if = "Option::is_none")]
728 pub max_size_bytes: Option<u64>,
729 #[serde(default, rename = "outputDir", skip_serializing_if = "Option::is_none")]
732 pub output_directory: Option<PathBuf>,
733}
734
735impl LargeToolOutputConfig {
736 pub fn new() -> Self {
739 Self::default()
740 }
741
742 pub fn with_enabled(mut self, enabled: bool) -> Self {
744 self.enabled = Some(enabled);
745 self
746 }
747
748 pub fn with_max_size_bytes(mut self, max_size_bytes: u64) -> Self {
750 self.max_size_bytes = Some(max_size_bytes);
751 self
752 }
753
754 pub fn with_output_directory<P: Into<PathBuf>>(mut self, output_directory: P) -> Self {
756 self.output_directory = Some(output_directory.into());
757 self
758 }
759}
760
761#[derive(Debug, Clone, Default, Serialize, Deserialize)]
768#[serde(rename_all = "camelCase")]
769#[non_exhaustive]
770pub struct InfiniteSessionConfig {
771 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub enabled: Option<bool>,
774 #[serde(default, skip_serializing_if = "Option::is_none")]
777 pub background_compaction_threshold: Option<f64>,
778 #[serde(default, skip_serializing_if = "Option::is_none")]
781 pub buffer_exhaustion_threshold: Option<f64>,
782}
783
784impl InfiniteSessionConfig {
785 pub fn new() -> Self {
788 Self::default()
789 }
790
791 pub fn with_enabled(mut self, enabled: bool) -> Self {
794 self.enabled = Some(enabled);
795 self
796 }
797
798 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
801 self.background_compaction_threshold = Some(threshold);
802 self
803 }
804
805 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
808 self.buffer_exhaustion_threshold = Some(threshold);
809 self
810 }
811}
812
813#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
824#[serde(rename_all = "camelCase")]
825#[non_exhaustive]
826pub struct MemoryConfiguration {
827 pub enabled: bool,
829}
830
831impl MemoryConfiguration {
832 pub fn enabled() -> Self {
834 Self { enabled: true }
835 }
836
837 pub fn disabled() -> Self {
839 Self { enabled: false }
840 }
841
842 pub fn with_enabled(mut self, enabled: bool) -> Self {
844 self.enabled = enabled;
845 self
846 }
847}
848
849#[derive(Debug, Clone, Serialize, Deserialize)]
851#[serde(rename_all = "camelCase")]
852#[non_exhaustive]
853pub struct CloudSessionRepository {
854 pub owner: String,
856 pub name: String,
858 #[serde(skip_serializing_if = "Option::is_none")]
860 pub branch: Option<String>,
861}
862
863impl CloudSessionRepository {
864 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
866 Self {
867 owner: owner.into(),
868 name: name.into(),
869 branch: None,
870 }
871 }
872
873 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
875 self.branch = Some(branch.into());
876 self
877 }
878}
879
880#[derive(Debug, Clone, Default, Serialize, Deserialize)]
882#[serde(rename_all = "camelCase")]
883#[non_exhaustive]
884pub struct CloudSessionOptions {
885 #[serde(skip_serializing_if = "Option::is_none")]
887 pub repository: Option<CloudSessionRepository>,
888}
889
890impl CloudSessionOptions {
891 pub fn with_repository(repository: CloudSessionRepository) -> Self {
893 Self {
894 repository: Some(repository),
895 }
896 }
897}
898
899#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
901#[serde(rename_all = "camelCase")]
902pub struct ExtensionInfo {
903 pub source: String,
905 pub name: String,
907}
908
909impl ExtensionInfo {
910 pub fn new(source: impl Into<String>, name: impl Into<String>) -> Self {
912 Self {
913 source: source.into(),
914 name: name.into(),
915 }
916 }
917}
918
919#[derive(Debug, Clone, Serialize, Deserialize)]
953#[serde(tag = "type", rename_all = "lowercase")]
954#[non_exhaustive]
955pub enum McpServerConfig {
956 #[serde(alias = "local")]
960 Stdio(McpStdioServerConfig),
961 Http(McpHttpServerConfig),
963 Sse(McpHttpServerConfig),
965}
966
967#[derive(Debug, Clone, Default, Serialize, Deserialize)]
971#[serde(rename_all = "camelCase")]
972pub struct McpStdioServerConfig {
973 #[serde(default, skip_serializing_if = "Option::is_none")]
979 pub tools: Option<Vec<String>>,
980 #[serde(default, skip_serializing_if = "Option::is_none")]
982 pub timeout: Option<i64>,
983 pub command: String,
985 #[serde(default, skip_serializing_if = "Vec::is_empty")]
987 pub args: Vec<String>,
988 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
991 pub env: HashMap<String, String>,
992 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
994 pub working_directory: Option<String>,
995}
996
997#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1001#[serde(rename_all = "camelCase")]
1002pub struct McpHttpServerConfig {
1003 #[serde(default, skip_serializing_if = "Option::is_none")]
1009 pub tools: Option<Vec<String>>,
1010 #[serde(default, skip_serializing_if = "Option::is_none")]
1012 pub timeout: Option<i64>,
1013 pub url: String,
1015 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1017 pub headers: HashMap<String, String>,
1018}
1019
1020#[derive(Clone, Default, Serialize, Deserialize)]
1026#[serde(rename_all = "camelCase")]
1027#[non_exhaustive]
1028pub struct ProviderConfig {
1029 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1032 pub provider_type: Option<String>,
1033 #[serde(default, skip_serializing_if = "Option::is_none")]
1036 pub wire_api: Option<String>,
1037 #[serde(default, skip_serializing_if = "Option::is_none")]
1042 pub transport: Option<String>,
1043 pub base_url: String,
1045 #[serde(default, skip_serializing_if = "Option::is_none")]
1047 pub api_key: Option<String>,
1048 #[serde(default, skip_serializing_if = "Option::is_none")]
1052 pub bearer_token: Option<String>,
1053 #[serde(skip)]
1056 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1057 #[serde(default, skip_serializing_if = "Option::is_none")]
1058 pub(crate) has_bearer_token_provider: Option<bool>,
1059 #[serde(default, skip_serializing_if = "Option::is_none")]
1061 pub azure: Option<AzureProviderOptions>,
1062 #[serde(default, skip_serializing_if = "Option::is_none")]
1064 pub headers: Option<HashMap<String, String>>,
1065 #[serde(default, skip_serializing_if = "Option::is_none")]
1069 pub model_id: Option<String>,
1070 #[serde(default, skip_serializing_if = "Option::is_none")]
1077 pub wire_model: Option<String>,
1078 #[serde(default, skip_serializing_if = "Option::is_none")]
1083 pub max_prompt_tokens: Option<i64>,
1084 #[serde(default, skip_serializing_if = "Option::is_none")]
1087 pub max_output_tokens: Option<i64>,
1088}
1089
1090impl std::fmt::Debug for ProviderConfig {
1091 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1092 f.debug_struct("ProviderConfig")
1093 .field("provider_type", &self.provider_type)
1094 .field("wire_api", &self.wire_api)
1095 .field("transport", &self.transport)
1096 .field("base_url", &self.base_url)
1097 .field("api_key", &self.api_key)
1098 .field("bearer_token", &self.bearer_token)
1099 .field(
1100 "bearer_token_provider",
1101 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1102 )
1103 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1104 .field("azure", &self.azure)
1105 .field("headers", &self.headers)
1106 .field("model_id", &self.model_id)
1107 .field("wire_model", &self.wire_model)
1108 .field("max_prompt_tokens", &self.max_prompt_tokens)
1109 .field("max_output_tokens", &self.max_output_tokens)
1110 .finish()
1111 }
1112}
1113
1114impl ProviderConfig {
1115 pub fn new(base_url: impl Into<String>) -> Self {
1118 Self {
1119 base_url: base_url.into(),
1120 ..Self::default()
1121 }
1122 }
1123
1124 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1126 self.provider_type = Some(provider_type.into());
1127 self
1128 }
1129
1130 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1132 self.wire_api = Some(wire_api.into());
1133 self
1134 }
1135
1136 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1139 self.transport = Some(transport.into());
1140 self
1141 }
1142
1143 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1145 self.api_key = Some(api_key.into());
1146 self
1147 }
1148
1149 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1152 self.bearer_token = Some(bearer_token.into());
1153 self
1154 }
1155
1156 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1162 self.bearer_token_provider = Some(provider);
1163 self
1164 }
1165
1166 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1168 self.azure = Some(azure);
1169 self
1170 }
1171
1172 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1174 self.headers = Some(headers);
1175 self
1176 }
1177
1178 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1181 self.model_id = Some(model_id.into());
1182 self
1183 }
1184
1185 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1190 self.wire_model = Some(wire_model.into());
1191 self
1192 }
1193
1194 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1198 self.max_prompt_tokens = Some(max);
1199 self
1200 }
1201
1202 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1205 self.max_output_tokens = Some(max);
1206 self
1207 }
1208}
1209
1210#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1223#[serde(rename_all = "camelCase")]
1224#[non_exhaustive]
1225pub struct CapiSessionOptions {
1226 #[serde(default, skip_serializing_if = "Option::is_none")]
1232 pub enable_web_socket_responses: Option<bool>,
1233}
1234
1235impl CapiSessionOptions {
1236 pub fn new() -> Self {
1238 Self::default()
1239 }
1240
1241 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1243 self.enable_web_socket_responses = Some(enable);
1244 self
1245 }
1246}
1247
1248#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1250#[serde(rename_all = "camelCase")]
1251pub struct AzureProviderOptions {
1252 #[serde(default, skip_serializing_if = "Option::is_none")]
1254 pub api_version: Option<String>,
1255}
1256
1257#[derive(Clone, Default, Serialize, Deserialize)]
1268#[serde(rename_all = "camelCase")]
1269#[non_exhaustive]
1270pub struct NamedProviderConfig {
1271 pub name: String,
1274 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1277 pub provider_type: Option<String>,
1278 #[serde(default, skip_serializing_if = "Option::is_none")]
1281 pub wire_api: Option<String>,
1282 pub base_url: String,
1284 #[serde(default, skip_serializing_if = "Option::is_none")]
1286 pub api_key: Option<String>,
1287 #[serde(default, skip_serializing_if = "Option::is_none")]
1290 pub bearer_token: Option<String>,
1291 #[serde(skip)]
1294 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1295 #[serde(default, skip_serializing_if = "Option::is_none")]
1296 pub(crate) has_bearer_token_provider: Option<bool>,
1297 #[serde(default, skip_serializing_if = "Option::is_none")]
1299 pub azure: Option<AzureProviderOptions>,
1300 #[serde(default, skip_serializing_if = "Option::is_none")]
1302 pub headers: Option<HashMap<String, String>>,
1303}
1304
1305impl std::fmt::Debug for NamedProviderConfig {
1306 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1307 f.debug_struct("NamedProviderConfig")
1308 .field("name", &self.name)
1309 .field("provider_type", &self.provider_type)
1310 .field("wire_api", &self.wire_api)
1311 .field("base_url", &self.base_url)
1312 .field("api_key", &self.api_key)
1313 .field("bearer_token", &self.bearer_token)
1314 .field(
1315 "bearer_token_provider",
1316 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1317 )
1318 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1319 .field("azure", &self.azure)
1320 .field("headers", &self.headers)
1321 .finish()
1322 }
1323}
1324
1325impl NamedProviderConfig {
1326 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1329 Self {
1330 name: name.into(),
1331 base_url: base_url.into(),
1332 ..Self::default()
1333 }
1334 }
1335
1336 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1338 self.provider_type = Some(provider_type.into());
1339 self
1340 }
1341
1342 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1344 self.wire_api = Some(wire_api.into());
1345 self
1346 }
1347
1348 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1350 self.api_key = Some(api_key.into());
1351 self
1352 }
1353
1354 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1357 self.bearer_token = Some(bearer_token.into());
1358 self
1359 }
1360
1361 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1367 self.bearer_token_provider = Some(provider);
1368 self
1369 }
1370
1371 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1373 self.azure = Some(azure);
1374 self
1375 }
1376
1377 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1379 self.headers = Some(headers);
1380 self
1381 }
1382}
1383
1384fn prepare_bearer_token_providers(
1385 provider: &mut Option<ProviderConfig>,
1386 providers: &mut Option<Vec<NamedProviderConfig>>,
1387) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1388 let mut bearer_token_providers = HashMap::new();
1389
1390 if let Some(provider) = provider.as_mut()
1391 && let Some(token_provider) = provider.bearer_token_provider.take()
1392 {
1393 provider.has_bearer_token_provider = Some(true);
1394 bearer_token_providers.insert("default".to_string(), token_provider);
1395 }
1396
1397 if let Some(providers) = providers.as_mut() {
1398 for provider in providers {
1399 if let Some(token_provider) = provider.bearer_token_provider.take() {
1400 provider.has_bearer_token_provider = Some(true);
1401 bearer_token_providers.insert(provider.name.clone(), token_provider);
1402 }
1403 }
1404 }
1405
1406 bearer_token_providers
1407}
1408
1409#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1417#[serde(rename_all = "camelCase")]
1418#[non_exhaustive]
1419pub struct ProviderModelConfig {
1420 pub id: String,
1423 pub provider: String,
1425 #[serde(default, skip_serializing_if = "Option::is_none")]
1428 pub wire_model: Option<String>,
1429 #[serde(default, skip_serializing_if = "Option::is_none")]
1432 pub model_id: Option<String>,
1433 #[serde(default, skip_serializing_if = "Option::is_none")]
1435 pub name: Option<String>,
1436 #[serde(default, skip_serializing_if = "Option::is_none")]
1438 pub max_prompt_tokens: Option<i64>,
1439 #[serde(default, skip_serializing_if = "Option::is_none")]
1441 pub max_context_window_tokens: Option<i64>,
1442 #[serde(default, skip_serializing_if = "Option::is_none")]
1444 pub max_output_tokens: Option<i64>,
1445 #[serde(default, skip_serializing_if = "Option::is_none")]
1448 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1449}
1450
1451impl ProviderModelConfig {
1452 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1455 Self {
1456 id: id.into(),
1457 provider: provider.into(),
1458 ..Self::default()
1459 }
1460 }
1461
1462 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1464 self.wire_model = Some(wire_model.into());
1465 self
1466 }
1467
1468 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1471 self.model_id = Some(model_id.into());
1472 self
1473 }
1474
1475 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1477 self.name = Some(name.into());
1478 self
1479 }
1480
1481 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1483 self.max_prompt_tokens = Some(max);
1484 self
1485 }
1486
1487 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1489 self.max_context_window_tokens = Some(max);
1490 self
1491 }
1492
1493 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1495 self.max_output_tokens = Some(max);
1496 self
1497 }
1498
1499 pub fn with_capabilities(
1501 mut self,
1502 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1503 ) -> Self {
1504 self.capabilities = Some(capabilities);
1505 self
1506 }
1507}
1508
1509#[derive(Clone)]
1561#[non_exhaustive]
1562pub struct SessionConfig {
1563 pub session_id: Option<SessionId>,
1565 pub model: Option<String>,
1567 pub client_name: Option<String>,
1569 pub reasoning_effort: Option<String>,
1571 pub reasoning_summary: Option<ReasoningSummary>,
1575 pub context_tier: Option<String>,
1578 pub streaming: Option<bool>,
1580 pub system_message: Option<SystemMessageConfig>,
1582 pub tools: Option<Vec<Tool>>,
1584 pub canvases: Option<Vec<CanvasDeclaration>>,
1586 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1591 pub request_canvas_renderer: Option<bool>,
1593 pub request_extensions: Option<bool>,
1595 pub extension_sdk_path: Option<String>,
1599 pub extension_info: Option<ExtensionInfo>,
1601 pub available_tools: Option<Vec<String>>,
1603 pub excluded_tools: Option<Vec<String>>,
1605 pub excluded_builtin_agents: Option<Vec<String>>,
1611 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
1613 pub mcp_oauth_token_storage: Option<String>,
1622 pub enable_config_discovery: Option<bool>,
1624 pub skip_embedding_retrieval: Option<bool>,
1626 pub embedding_cache_storage: Option<String>,
1629 pub organization_custom_instructions: Option<String>,
1631 pub enable_on_demand_instruction_discovery: Option<bool>,
1633 pub enable_file_hooks: Option<bool>,
1635 pub enable_host_git_operations: Option<bool>,
1637 pub enable_session_store: Option<bool>,
1639 pub enable_skills: Option<bool>,
1641 pub enable_mcp_apps: Option<bool>,
1668 pub skill_directories: Option<Vec<PathBuf>>,
1670 pub instruction_directories: Option<Vec<PathBuf>>,
1673 pub plugin_directories: Option<Vec<PathBuf>>,
1675 pub large_output: Option<LargeToolOutputConfig>,
1677 pub disabled_skills: Option<Vec<String>>,
1680 pub hooks: Option<bool>,
1684 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1686 pub default_agent: Option<DefaultAgentConfig>,
1690 pub agent: Option<String>,
1693 pub infinite_sessions: Option<InfiniteSessionConfig>,
1696 pub provider: Option<ProviderConfig>,
1700 pub capi: Option<CapiSessionOptions>,
1706 pub providers: Option<Vec<NamedProviderConfig>>,
1713 pub models: Option<Vec<ProviderModelConfig>>,
1719 pub enable_session_telemetry: Option<bool>,
1727 pub enable_citations: Option<bool>,
1729 pub session_limits: Option<SessionLimitsConfig>,
1731 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1734 pub memory: Option<MemoryConfiguration>,
1736 pub config_directory: Option<PathBuf>,
1739 pub working_directory: Option<PathBuf>,
1742 pub github_token: Option<String>,
1748 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
1754 pub cloud: Option<CloudSessionOptions>,
1757 pub include_sub_agent_streaming_events: Option<bool>,
1761 pub commands: Option<Vec<CommandDefinition>>,
1765 #[doc(hidden)]
1772 pub exp_assignments: Option<Value>,
1773 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1778 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
1782 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
1785 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
1788 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
1792 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
1795 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
1798 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1802 pub(crate) permission_policy: Option<crate::permission::Policy>,
1806 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
1811 pub skip_custom_instructions: Option<bool>,
1815 pub custom_agents_local_only: Option<bool>,
1819 pub coauthor_enabled: Option<bool>,
1823 pub manage_schedule_enabled: Option<bool>,
1827}
1828
1829impl std::fmt::Debug for SessionConfig {
1830 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1831 f.debug_struct("SessionConfig")
1832 .field("session_id", &self.session_id)
1833 .field("model", &self.model)
1834 .field("client_name", &self.client_name)
1835 .field("reasoning_effort", &self.reasoning_effort)
1836 .field("reasoning_summary", &self.reasoning_summary)
1837 .field("context_tier", &self.context_tier)
1838 .field("streaming", &self.streaming)
1839 .field("system_message", &self.system_message)
1840 .field("tools", &self.tools)
1841 .field("canvases", &self.canvases)
1842 .field(
1843 "canvas_handler",
1844 &self.canvas_handler.as_ref().map(|_| "<set>"),
1845 )
1846 .field("request_canvas_renderer", &self.request_canvas_renderer)
1847 .field("request_extensions", &self.request_extensions)
1848 .field("extension_sdk_path", &self.extension_sdk_path)
1849 .field("extension_info", &self.extension_info)
1850 .field("available_tools", &self.available_tools)
1851 .field("excluded_tools", &self.excluded_tools)
1852 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
1853 .field("mcp_servers", &self.mcp_servers)
1854 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
1855 .field("embedding_cache_storage", &self.embedding_cache_storage)
1856 .field("enable_config_discovery", &self.enable_config_discovery)
1857 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
1858 .field(
1859 "organization_custom_instructions",
1860 &self
1861 .organization_custom_instructions
1862 .as_ref()
1863 .map(|_| "<redacted>"),
1864 )
1865 .field(
1866 "enable_on_demand_instruction_discovery",
1867 &self.enable_on_demand_instruction_discovery,
1868 )
1869 .field("enable_file_hooks", &self.enable_file_hooks)
1870 .field(
1871 "enable_host_git_operations",
1872 &self.enable_host_git_operations,
1873 )
1874 .field("enable_session_store", &self.enable_session_store)
1875 .field("enable_skills", &self.enable_skills)
1876 .field("enable_mcp_apps", &self.enable_mcp_apps)
1877 .field("skill_directories", &self.skill_directories)
1878 .field("instruction_directories", &self.instruction_directories)
1879 .field("plugin_directories", &self.plugin_directories)
1880 .field("large_output", &self.large_output)
1881 .field("disabled_skills", &self.disabled_skills)
1882 .field("hooks", &self.hooks)
1883 .field("custom_agents", &self.custom_agents)
1884 .field("default_agent", &self.default_agent)
1885 .field("agent", &self.agent)
1886 .field("infinite_sessions", &self.infinite_sessions)
1887 .field("provider", &self.provider)
1888 .field("capi", &self.capi)
1889 .field("enable_session_telemetry", &self.enable_session_telemetry)
1890 .field("enable_citations", &self.enable_citations)
1891 .field("session_limits", &self.session_limits)
1892 .field("model_capabilities", &self.model_capabilities)
1893 .field("memory", &self.memory)
1894 .field("config_directory", &self.config_directory)
1895 .field("working_directory", &self.working_directory)
1896 .field(
1897 "github_token",
1898 &self.github_token.as_ref().map(|_| "<redacted>"),
1899 )
1900 .field("remote_session", &self.remote_session)
1901 .field("cloud", &self.cloud)
1902 .field(
1903 "include_sub_agent_streaming_events",
1904 &self.include_sub_agent_streaming_events,
1905 )
1906 .field("commands", &self.commands)
1907 .field("exp_assignments", &self.exp_assignments)
1908 .field(
1909 "session_fs_provider",
1910 &self.session_fs_provider.as_ref().map(|_| "<set>"),
1911 )
1912 .field(
1913 "permission_handler",
1914 &self.permission_handler.as_ref().map(|_| "<set>"),
1915 )
1916 .field(
1917 "elicitation_handler",
1918 &self.elicitation_handler.as_ref().map(|_| "<set>"),
1919 )
1920 .field(
1921 "mcp_auth_handler",
1922 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
1923 )
1924 .field(
1925 "user_input_handler",
1926 &self.user_input_handler.as_ref().map(|_| "<set>"),
1927 )
1928 .field(
1929 "exit_plan_mode_handler",
1930 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
1931 )
1932 .field(
1933 "auto_mode_switch_handler",
1934 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
1935 )
1936 .field(
1937 "hooks_handler",
1938 &self.hooks_handler.as_ref().map(|_| "<set>"),
1939 )
1940 .field(
1941 "system_message_transform",
1942 &self.system_message_transform.as_ref().map(|_| "<set>"),
1943 )
1944 .finish()
1945 }
1946}
1947
1948impl Default for SessionConfig {
1949 fn default() -> Self {
1955 Self {
1956 session_id: None,
1957 model: None,
1958 client_name: None,
1959 reasoning_effort: None,
1960 reasoning_summary: None,
1961 context_tier: None,
1962 streaming: None,
1963 system_message: None,
1964 tools: None,
1965 canvases: None,
1966 canvas_handler: None,
1967 request_canvas_renderer: None,
1968 request_extensions: None,
1969 extension_sdk_path: None,
1970 extension_info: None,
1971 available_tools: None,
1972 excluded_tools: None,
1973 excluded_builtin_agents: None,
1974 mcp_servers: None,
1975 mcp_oauth_token_storage: None,
1976 enable_config_discovery: None,
1977 skip_embedding_retrieval: None,
1978 organization_custom_instructions: None,
1979 enable_on_demand_instruction_discovery: None,
1980 enable_file_hooks: None,
1981 enable_host_git_operations: None,
1982 enable_session_store: None,
1983 enable_skills: None,
1984 embedding_cache_storage: None,
1985 enable_mcp_apps: None,
1986 skill_directories: None,
1987 instruction_directories: None,
1988 plugin_directories: None,
1989 large_output: None,
1990 disabled_skills: None,
1991 hooks: None,
1992 custom_agents: None,
1993 default_agent: None,
1994 agent: None,
1995 infinite_sessions: None,
1996 provider: None,
1997 capi: None,
1998 providers: None,
1999 models: None,
2000 enable_session_telemetry: None,
2001 enable_citations: None,
2002 session_limits: None,
2003 model_capabilities: None,
2004 memory: None,
2005 config_directory: None,
2006 working_directory: None,
2007 github_token: None,
2008 remote_session: None,
2009 cloud: None,
2010 include_sub_agent_streaming_events: None,
2011 commands: None,
2012 exp_assignments: None,
2013 session_fs_provider: None,
2014 permission_handler: None,
2015 elicitation_handler: None,
2016 mcp_auth_handler: None,
2017 user_input_handler: None,
2018 exit_plan_mode_handler: None,
2019 auto_mode_switch_handler: None,
2020 hooks_handler: None,
2021 permission_policy: None,
2022 system_message_transform: None,
2023 skip_custom_instructions: None,
2024 custom_agents_local_only: None,
2025 coauthor_enabled: None,
2026 manage_schedule_enabled: None,
2027 }
2028 }
2029}
2030
2031pub(crate) struct SessionConfigRuntime {
2037 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2038 pub permission_policy: Option<crate::permission::Policy>,
2039 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2040 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2041 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2042 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2043 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2044 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2045 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2046 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2047 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2048 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2049 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2050 pub commands: Option<Vec<CommandDefinition>>,
2051}
2052
2053impl SessionConfig {
2054 pub(crate) fn into_wire(
2066 mut self,
2067 session_id: Option<SessionId>,
2068 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2069 let permission_active =
2070 self.permission_handler.is_some() || self.permission_policy.is_some();
2071 let request_user_input = self.user_input_handler.is_some();
2072 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2073 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2074 let request_elicitation = self.elicitation_handler.is_some();
2075 let hooks_flag = self.hooks_handler.is_some();
2076
2077 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2078 if let Some(tools) = self.tools.as_mut() {
2079 for tool in tools.iter_mut() {
2080 if let Some(handler) = tool.handler.take()
2081 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2082 {
2083 return Err(crate::Error::with_message(
2084 crate::ErrorKind::InvalidConfig,
2085 format!("duplicate tool handler registered for name {:?}", tool.name),
2086 ));
2087 }
2088 }
2089 }
2090
2091 let wire_commands = self.commands.as_ref().map(|cmds| {
2092 cmds.iter()
2093 .map(|c| crate::wire::CommandWireDefinition {
2094 name: c.name.clone(),
2095 description: c.description.clone(),
2096 })
2097 .collect()
2098 });
2099 let wire_canvases = self.canvases.clone();
2100 let canvas_handler = self.canvas_handler.clone();
2101 let bearer_token_providers =
2102 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2103
2104 let wire = crate::wire::SessionCreateWire {
2105 session_id,
2106 model: self.model,
2107 client_name: self.client_name,
2108 reasoning_effort: self.reasoning_effort,
2109 reasoning_summary: self.reasoning_summary,
2110 context_tier: self.context_tier,
2111 streaming: self.streaming,
2112 system_message: self.system_message,
2113 tools: self.tools,
2114 canvases: wire_canvases,
2115 request_canvas_renderer: self.request_canvas_renderer,
2116 request_extensions: self.request_extensions,
2117 extension_sdk_path: self.extension_sdk_path,
2118 extension_info: self.extension_info,
2119 available_tools: self.available_tools,
2120 excluded_tools: self.excluded_tools,
2121 excluded_builtin_agents: self.excluded_builtin_agents,
2122 tool_filter_precedence: "excluded",
2123 mcp_servers: self.mcp_servers,
2124 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2125 embedding_cache_storage: self.embedding_cache_storage,
2126 env_value_mode: "direct",
2127 enable_config_discovery: self.enable_config_discovery,
2128 skip_embedding_retrieval: self.skip_embedding_retrieval,
2129 organization_custom_instructions: self.organization_custom_instructions,
2130 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2131 enable_file_hooks: self.enable_file_hooks,
2132 enable_host_git_operations: self.enable_host_git_operations,
2133 enable_session_store: self.enable_session_store,
2134 enable_skills: self.enable_skills,
2135 request_user_input,
2136 request_permission: permission_active,
2137 request_exit_plan_mode,
2138 request_auto_mode_switch,
2139 request_elicitation,
2140 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2141 hooks: hooks_flag,
2142 skill_directories: self.skill_directories,
2143 instruction_directories: self.instruction_directories,
2144 plugin_directories: self.plugin_directories,
2145 large_output: self.large_output,
2146 disabled_skills: self.disabled_skills,
2147 custom_agents: self.custom_agents,
2148 default_agent: self.default_agent,
2149 agent: self.agent,
2150 infinite_sessions: self.infinite_sessions,
2151 provider: self.provider,
2152 capi: self.capi,
2153 providers: self.providers,
2154 models: self.models,
2155 enable_session_telemetry: self.enable_session_telemetry,
2156 enable_citations: self.enable_citations,
2157 session_limits: self.session_limits,
2158 model_capabilities: self.model_capabilities,
2159 memory: self.memory,
2160 config_dir: self.config_directory,
2161 working_directory: self.working_directory,
2162 github_token: self.github_token,
2163 remote_session: self.remote_session,
2164 cloud: self.cloud,
2165 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2166 commands: wire_commands,
2167 exp_assignments: self.exp_assignments,
2168 };
2169
2170 let runtime = SessionConfigRuntime {
2171 permission_handler: self.permission_handler,
2172 permission_policy: self.permission_policy,
2173 elicitation_handler: self.elicitation_handler,
2174 mcp_auth_handler: self.mcp_auth_handler,
2175 user_input_handler: self.user_input_handler,
2176 exit_plan_mode_handler: self.exit_plan_mode_handler,
2177 auto_mode_switch_handler: self.auto_mode_switch_handler,
2178 hooks_handler: self.hooks_handler,
2179 system_message_transform: self.system_message_transform,
2180 tool_handlers,
2181 canvas_handler,
2182 session_fs_provider: self.session_fs_provider,
2183 bearer_token_providers,
2184 commands: self.commands,
2185 };
2186
2187 Ok((wire, runtime))
2188 }
2189
2190 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2194 self.permission_handler = Some(handler);
2195 self
2196 }
2197
2198 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2201 self.elicitation_handler = Some(handler);
2202 self
2203 }
2204
2205 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2207 self.mcp_auth_handler = Some(handler);
2208 self
2209 }
2210
2211 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2214 self.user_input_handler = Some(handler);
2215 self
2216 }
2217
2218 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2220 self.exit_plan_mode_handler = Some(handler);
2221 self
2222 }
2223
2224 pub fn with_auto_mode_switch_handler(
2226 mut self,
2227 handler: Arc<dyn AutoModeSwitchHandler>,
2228 ) -> Self {
2229 self.auto_mode_switch_handler = Some(handler);
2230 self
2231 }
2232
2233 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2238 self.commands = Some(commands);
2239 self
2240 }
2241
2242 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2246 self.session_fs_provider = Some(provider);
2247 self
2248 }
2249
2250 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2253 self.hooks_handler = Some(hooks);
2254 self
2255 }
2256
2257 pub fn with_system_message_transform(
2261 mut self,
2262 transform: Arc<dyn SystemMessageTransform>,
2263 ) -> Self {
2264 self.system_message_transform = Some(transform);
2265 self
2266 }
2267
2268 pub fn approve_all_permissions(mut self) -> Self {
2274 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2275 self
2276 }
2277
2278 pub fn deny_all_permissions(mut self) -> Self {
2281 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2282 self
2283 }
2284
2285 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2290 where
2291 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2292 {
2293 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2294 self
2295 }
2296
2297 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2299 self.session_id = Some(id.into());
2300 self
2301 }
2302
2303 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2305 self.model = Some(model.into());
2306 self
2307 }
2308
2309 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2311 self.client_name = Some(name.into());
2312 self
2313 }
2314
2315 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2317 self.reasoning_effort = Some(effort.into());
2318 self
2319 }
2320
2321 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2323 self.reasoning_summary = Some(summary);
2324 self
2325 }
2326
2327 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2329 self.context_tier = Some(tier.into());
2330 self
2331 }
2332
2333 pub fn with_streaming(mut self, streaming: bool) -> Self {
2335 self.streaming = Some(streaming);
2336 self
2337 }
2338
2339 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2341 self.system_message = Some(system_message);
2342 self
2343 }
2344
2345 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2347 self.tools = Some(tools.into_iter().collect());
2348 self
2349 }
2350
2351 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2356 self.canvases = Some(canvases.into_iter().collect());
2357 self
2358 }
2359
2360 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2362 self.canvas_handler = Some(handler);
2363 self
2364 }
2365
2366 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2368 self.request_canvas_renderer = Some(request);
2369 self
2370 }
2371
2372 pub fn with_request_extensions(mut self, request: bool) -> Self {
2374 self.request_extensions = Some(request);
2375 self
2376 }
2377
2378 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2382 self.extension_sdk_path = Some(path.into());
2383 self
2384 }
2385
2386 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2388 self.extension_info = Some(extension_info);
2389 self
2390 }
2391
2392 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2394 where
2395 I: IntoIterator<Item = S>,
2396 S: Into<String>,
2397 {
2398 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2399 self
2400 }
2401
2402 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2404 where
2405 I: IntoIterator<Item = S>,
2406 S: Into<String>,
2407 {
2408 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2409 self
2410 }
2411
2412 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2414 where
2415 I: IntoIterator<Item = S>,
2416 S: Into<String>,
2417 {
2418 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2419 self
2420 }
2421
2422 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
2424 self.mcp_servers = Some(servers);
2425 self
2426 }
2427
2428 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2436 self.mcp_oauth_token_storage = Some(mode.into());
2437 self
2438 }
2439
2440 pub fn with_embedding_cache_storage(
2442 mut self,
2443 embedding_cache_storage: impl Into<String>,
2444 ) -> Self {
2445 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2446 self
2447 }
2448
2449 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2451 self.enable_config_discovery = Some(enable);
2452 self
2453 }
2454
2455 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2457 self.skip_embedding_retrieval = Some(value);
2458 self
2459 }
2460
2461 pub fn with_organization_custom_instructions(
2463 mut self,
2464 instructions: impl Into<String>,
2465 ) -> Self {
2466 self.organization_custom_instructions = Some(instructions.into());
2467 self
2468 }
2469
2470 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2472 self.enable_on_demand_instruction_discovery = Some(value);
2473 self
2474 }
2475
2476 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2478 self.enable_file_hooks = Some(value);
2479 self
2480 }
2481
2482 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2484 self.enable_host_git_operations = Some(value);
2485 self
2486 }
2487
2488 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2490 self.enable_session_store = Some(value);
2491 self
2492 }
2493
2494 pub fn with_enable_skills(mut self, value: bool) -> Self {
2496 self.enable_skills = Some(value);
2497 self
2498 }
2499
2500 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2506 self.enable_mcp_apps = Some(enable);
2507 self
2508 }
2509
2510 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2512 where
2513 I: IntoIterator<Item = P>,
2514 P: Into<PathBuf>,
2515 {
2516 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2517 self
2518 }
2519
2520 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2524 where
2525 I: IntoIterator<Item = P>,
2526 P: Into<PathBuf>,
2527 {
2528 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2529 self
2530 }
2531
2532 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2534 where
2535 I: IntoIterator<Item = P>,
2536 P: Into<PathBuf>,
2537 {
2538 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2539 self
2540 }
2541
2542 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2544 self.large_output = Some(config);
2545 self
2546 }
2547
2548 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2550 where
2551 I: IntoIterator<Item = S>,
2552 S: Into<String>,
2553 {
2554 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2555 self
2556 }
2557
2558 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2560 mut self,
2561 agents: I,
2562 ) -> Self {
2563 self.custom_agents = Some(agents.into_iter().collect());
2564 self
2565 }
2566
2567 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2569 self.default_agent = Some(agent);
2570 self
2571 }
2572
2573 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2576 self.agent = Some(name.into());
2577 self
2578 }
2579
2580 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2583 self.infinite_sessions = Some(config);
2584 self
2585 }
2586
2587 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2589 self.provider = Some(provider);
2590 self
2591 }
2592
2593 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
2595 self.capi = Some(capi);
2596 self
2597 }
2598
2599 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
2605 self.providers = Some(providers);
2606 self
2607 }
2608
2609 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
2615 self.models = Some(models);
2616 self
2617 }
2618
2619 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2623 self.enable_session_telemetry = Some(enable);
2624 self
2625 }
2626
2627 pub fn with_enable_citations(mut self, enable: bool) -> Self {
2629 self.enable_citations = Some(enable);
2630 self
2631 }
2632
2633 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
2635 self.session_limits = Some(limits);
2636 self
2637 }
2638
2639 pub fn with_model_capabilities(
2641 mut self,
2642 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2643 ) -> Self {
2644 self.model_capabilities = Some(capabilities);
2645 self
2646 }
2647
2648 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
2650 self.memory = Some(memory);
2651 self
2652 }
2653
2654 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2656 self.config_directory = Some(dir.into());
2657 self
2658 }
2659
2660 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2663 self.working_directory = Some(dir.into());
2664 self
2665 }
2666
2667 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2672 self.github_token = Some(token.into());
2673 self
2674 }
2675
2676 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2679 self.include_sub_agent_streaming_events = Some(include);
2680 self
2681 }
2682
2683 pub fn with_remote_session(
2685 mut self,
2686 mode: crate::generated::api_types::RemoteSessionMode,
2687 ) -> Self {
2688 self.remote_session = Some(mode);
2689 self
2690 }
2691
2692 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
2694 self.cloud = Some(cloud);
2695 self
2696 }
2697
2698 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
2700 self.skip_custom_instructions = Some(value);
2701 self
2702 }
2703
2704 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
2706 self.custom_agents_local_only = Some(value);
2707 self
2708 }
2709
2710 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
2712 self.coauthor_enabled = Some(value);
2713 self
2714 }
2715
2716 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
2718 self.manage_schedule_enabled = Some(value);
2719 self
2720 }
2721
2722 #[doc(hidden)]
2730 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
2731 self.exp_assignments = Some(assignments);
2732 self
2733 }
2734}
2735#[derive(Clone)]
2742#[non_exhaustive]
2743pub struct ResumeSessionConfig {
2744 pub session_id: SessionId,
2746 pub model: Option<String>,
2749 pub client_name: Option<String>,
2751 pub reasoning_effort: Option<String>,
2753 pub reasoning_summary: Option<ReasoningSummary>,
2757 pub context_tier: Option<String>,
2760 pub streaming: Option<bool>,
2762 pub system_message: Option<SystemMessageConfig>,
2765 pub tools: Option<Vec<Tool>>,
2767 pub canvases: Option<Vec<CanvasDeclaration>>,
2769 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2772 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
2774 pub request_canvas_renderer: Option<bool>,
2776 pub request_extensions: Option<bool>,
2778 pub extension_sdk_path: Option<String>,
2782 pub extension_info: Option<ExtensionInfo>,
2784 pub available_tools: Option<Vec<String>>,
2786 pub excluded_tools: Option<Vec<String>>,
2788 pub excluded_builtin_agents: Option<Vec<String>>,
2794 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
2796 pub mcp_oauth_token_storage: Option<String>,
2799 pub enable_config_discovery: Option<bool>,
2801 pub skip_embedding_retrieval: Option<bool>,
2803 pub embedding_cache_storage: Option<String>,
2805 pub organization_custom_instructions: Option<String>,
2807 pub enable_on_demand_instruction_discovery: Option<bool>,
2809 pub enable_file_hooks: Option<bool>,
2811 pub enable_host_git_operations: Option<bool>,
2813 pub enable_session_store: Option<bool>,
2815 pub enable_skills: Option<bool>,
2817 pub enable_mcp_apps: Option<bool>,
2823 pub skill_directories: Option<Vec<PathBuf>>,
2825 pub instruction_directories: Option<Vec<PathBuf>>,
2828 pub plugin_directories: Option<Vec<PathBuf>>,
2830 pub large_output: Option<LargeToolOutputConfig>,
2832 pub disabled_skills: Option<Vec<String>>,
2834 pub hooks: Option<bool>,
2836 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2838 pub default_agent: Option<DefaultAgentConfig>,
2840 pub agent: Option<String>,
2842 pub infinite_sessions: Option<InfiniteSessionConfig>,
2844 pub provider: Option<ProviderConfig>,
2846 pub capi: Option<CapiSessionOptions>,
2852 pub providers: Option<Vec<NamedProviderConfig>>,
2858 pub models: Option<Vec<ProviderModelConfig>>,
2864 pub enable_session_telemetry: Option<bool>,
2872 pub enable_citations: Option<bool>,
2874 pub session_limits: Option<SessionLimitsConfig>,
2876 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2878 pub memory: Option<MemoryConfiguration>,
2880 pub config_directory: Option<PathBuf>,
2882 pub working_directory: Option<PathBuf>,
2884 pub github_token: Option<String>,
2887 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2890 pub include_sub_agent_streaming_events: Option<bool>,
2892 pub commands: Option<Vec<CommandDefinition>>,
2896 #[doc(hidden)]
2901 pub exp_assignments: Option<Value>,
2902 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2907 pub suppress_resume_event: Option<bool>,
2910 pub continue_pending_work: Option<bool>,
2918 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2921 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2924 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2926 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2929 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2932 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2935 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2937 pub(crate) permission_policy: Option<crate::permission::Policy>,
2939 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2941 pub skip_custom_instructions: Option<bool>,
2943 pub custom_agents_local_only: Option<bool>,
2945 pub coauthor_enabled: Option<bool>,
2947 pub manage_schedule_enabled: Option<bool>,
2949}
2950
2951impl std::fmt::Debug for ResumeSessionConfig {
2952 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2953 f.debug_struct("ResumeSessionConfig")
2954 .field("session_id", &self.session_id)
2955 .field("model", &self.model)
2956 .field("client_name", &self.client_name)
2957 .field("reasoning_effort", &self.reasoning_effort)
2958 .field("reasoning_summary", &self.reasoning_summary)
2959 .field("context_tier", &self.context_tier)
2960 .field("streaming", &self.streaming)
2961 .field("system_message", &self.system_message)
2962 .field("tools", &self.tools)
2963 .field("canvases", &self.canvases)
2964 .field(
2965 "canvas_handler",
2966 &self.canvas_handler.as_ref().map(|_| "<set>"),
2967 )
2968 .field("open_canvases", &self.open_canvases)
2969 .field("request_canvas_renderer", &self.request_canvas_renderer)
2970 .field("request_extensions", &self.request_extensions)
2971 .field("extension_sdk_path", &self.extension_sdk_path)
2972 .field("extension_info", &self.extension_info)
2973 .field("available_tools", &self.available_tools)
2974 .field("excluded_tools", &self.excluded_tools)
2975 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2976 .field("mcp_servers", &self.mcp_servers)
2977 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2978 .field("embedding_cache_storage", &self.embedding_cache_storage)
2979 .field("enable_config_discovery", &self.enable_config_discovery)
2980 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2981 .field(
2982 "organization_custom_instructions",
2983 &self
2984 .organization_custom_instructions
2985 .as_ref()
2986 .map(|_| "<redacted>"),
2987 )
2988 .field(
2989 "enable_on_demand_instruction_discovery",
2990 &self.enable_on_demand_instruction_discovery,
2991 )
2992 .field("enable_file_hooks", &self.enable_file_hooks)
2993 .field(
2994 "enable_host_git_operations",
2995 &self.enable_host_git_operations,
2996 )
2997 .field("enable_session_store", &self.enable_session_store)
2998 .field("enable_skills", &self.enable_skills)
2999 .field("enable_mcp_apps", &self.enable_mcp_apps)
3000 .field("skill_directories", &self.skill_directories)
3001 .field("instruction_directories", &self.instruction_directories)
3002 .field("plugin_directories", &self.plugin_directories)
3003 .field("large_output", &self.large_output)
3004 .field("disabled_skills", &self.disabled_skills)
3005 .field("hooks", &self.hooks)
3006 .field("custom_agents", &self.custom_agents)
3007 .field("default_agent", &self.default_agent)
3008 .field("agent", &self.agent)
3009 .field("infinite_sessions", &self.infinite_sessions)
3010 .field("provider", &self.provider)
3011 .field("capi", &self.capi)
3012 .field("enable_session_telemetry", &self.enable_session_telemetry)
3013 .field("enable_citations", &self.enable_citations)
3014 .field("session_limits", &self.session_limits)
3015 .field("model_capabilities", &self.model_capabilities)
3016 .field("memory", &self.memory)
3017 .field("config_directory", &self.config_directory)
3018 .field("working_directory", &self.working_directory)
3019 .field(
3020 "github_token",
3021 &self.github_token.as_ref().map(|_| "<redacted>"),
3022 )
3023 .field("remote_session", &self.remote_session)
3024 .field(
3025 "include_sub_agent_streaming_events",
3026 &self.include_sub_agent_streaming_events,
3027 )
3028 .field("commands", &self.commands)
3029 .field("exp_assignments", &self.exp_assignments)
3030 .field(
3031 "session_fs_provider",
3032 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3033 )
3034 .field(
3035 "permission_handler",
3036 &self.permission_handler.as_ref().map(|_| "<set>"),
3037 )
3038 .field(
3039 "elicitation_handler",
3040 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3041 )
3042 .field(
3043 "user_input_handler",
3044 &self.user_input_handler.as_ref().map(|_| "<set>"),
3045 )
3046 .field(
3047 "exit_plan_mode_handler",
3048 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3049 )
3050 .field(
3051 "auto_mode_switch_handler",
3052 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3053 )
3054 .field(
3055 "hooks_handler",
3056 &self.hooks_handler.as_ref().map(|_| "<set>"),
3057 )
3058 .field(
3059 "system_message_transform",
3060 &self.system_message_transform.as_ref().map(|_| "<set>"),
3061 )
3062 .field("suppress_resume_event", &self.suppress_resume_event)
3063 .field("continue_pending_work", &self.continue_pending_work)
3064 .finish()
3065 }
3066}
3067
3068impl ResumeSessionConfig {
3069 pub(crate) fn into_wire(
3077 mut self,
3078 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3079 let permission_active =
3080 self.permission_handler.is_some() || self.permission_policy.is_some();
3081 let request_user_input = self.user_input_handler.is_some();
3082 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3083 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3084 let request_elicitation = self.elicitation_handler.is_some();
3085 let hooks_flag = self.hooks_handler.is_some();
3086
3087 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3088 if let Some(tools) = self.tools.as_mut() {
3089 for tool in tools.iter_mut() {
3090 if let Some(handler) = tool.handler.take()
3091 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3092 {
3093 return Err(crate::Error::with_message(
3094 crate::ErrorKind::InvalidConfig,
3095 format!("duplicate tool handler registered for name {:?}", tool.name),
3096 ));
3097 }
3098 }
3099 }
3100
3101 let wire_commands = self.commands.as_ref().map(|cmds| {
3102 cmds.iter()
3103 .map(|c| crate::wire::CommandWireDefinition {
3104 name: c.name.clone(),
3105 description: c.description.clone(),
3106 })
3107 .collect()
3108 });
3109 let wire_canvases = self.canvases.clone();
3110 let canvas_handler = self.canvas_handler.clone();
3111 let bearer_token_providers =
3112 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3113
3114 let wire = crate::wire::SessionResumeWire {
3115 session_id: self.session_id,
3116 model: self.model,
3117 client_name: self.client_name,
3118 reasoning_effort: self.reasoning_effort,
3119 reasoning_summary: self.reasoning_summary,
3120 context_tier: self.context_tier,
3121 streaming: self.streaming,
3122 system_message: self.system_message,
3123 tools: self.tools,
3124 canvases: wire_canvases,
3125 open_canvases: self.open_canvases,
3126 request_canvas_renderer: self.request_canvas_renderer,
3127 request_extensions: self.request_extensions,
3128 extension_sdk_path: self.extension_sdk_path,
3129 extension_info: self.extension_info,
3130 available_tools: self.available_tools,
3131 excluded_tools: self.excluded_tools,
3132 excluded_builtin_agents: self.excluded_builtin_agents,
3133 tool_filter_precedence: "excluded",
3134 mcp_servers: self.mcp_servers,
3135 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3136 embedding_cache_storage: self.embedding_cache_storage,
3137 env_value_mode: "direct",
3138 enable_config_discovery: self.enable_config_discovery,
3139 skip_embedding_retrieval: self.skip_embedding_retrieval,
3140 organization_custom_instructions: self.organization_custom_instructions,
3141 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3142 enable_file_hooks: self.enable_file_hooks,
3143 enable_host_git_operations: self.enable_host_git_operations,
3144 enable_session_store: self.enable_session_store,
3145 enable_skills: self.enable_skills,
3146 request_user_input,
3147 request_permission: permission_active,
3148 request_exit_plan_mode,
3149 request_auto_mode_switch,
3150 request_elicitation,
3151 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3152 hooks: hooks_flag,
3153 skill_directories: self.skill_directories,
3154 instruction_directories: self.instruction_directories,
3155 plugin_directories: self.plugin_directories,
3156 large_output: self.large_output,
3157 disabled_skills: self.disabled_skills,
3158 custom_agents: self.custom_agents,
3159 default_agent: self.default_agent,
3160 agent: self.agent,
3161 infinite_sessions: self.infinite_sessions,
3162 provider: self.provider,
3163 capi: self.capi,
3164 providers: self.providers,
3165 models: self.models,
3166 enable_session_telemetry: self.enable_session_telemetry,
3167 enable_citations: self.enable_citations,
3168 session_limits: self.session_limits,
3169 model_capabilities: self.model_capabilities,
3170 memory: self.memory,
3171 config_dir: self.config_directory,
3172 working_directory: self.working_directory,
3173 github_token: self.github_token,
3174 remote_session: self.remote_session,
3175 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3176 commands: wire_commands,
3177 exp_assignments: self.exp_assignments,
3178 suppress_resume_event: self.suppress_resume_event,
3179 continue_pending_work: self.continue_pending_work,
3180 };
3181
3182 let runtime = SessionConfigRuntime {
3183 permission_handler: self.permission_handler,
3184 permission_policy: self.permission_policy,
3185 elicitation_handler: self.elicitation_handler,
3186 mcp_auth_handler: self.mcp_auth_handler,
3187 user_input_handler: self.user_input_handler,
3188 exit_plan_mode_handler: self.exit_plan_mode_handler,
3189 auto_mode_switch_handler: self.auto_mode_switch_handler,
3190 hooks_handler: self.hooks_handler,
3191 system_message_transform: self.system_message_transform,
3192 tool_handlers,
3193 canvas_handler,
3194 session_fs_provider: self.session_fs_provider,
3195 bearer_token_providers,
3196 commands: self.commands,
3197 };
3198
3199 Ok((wire, runtime))
3200 }
3201
3202 pub fn new(session_id: SessionId) -> Self {
3207 Self {
3208 session_id,
3209 model: None,
3210 client_name: None,
3211 reasoning_effort: None,
3212 reasoning_summary: None,
3213 context_tier: None,
3214 streaming: None,
3215 system_message: None,
3216 tools: None,
3217 canvases: None,
3218 canvas_handler: None,
3219 open_canvases: None,
3220 request_canvas_renderer: None,
3221 request_extensions: None,
3222 extension_sdk_path: None,
3223 extension_info: None,
3224 available_tools: None,
3225 excluded_tools: None,
3226 excluded_builtin_agents: None,
3227 mcp_servers: None,
3228 mcp_oauth_token_storage: None,
3229 enable_config_discovery: None,
3230 skip_embedding_retrieval: None,
3231 organization_custom_instructions: None,
3232 enable_on_demand_instruction_discovery: None,
3233 enable_file_hooks: None,
3234 enable_host_git_operations: None,
3235 enable_session_store: None,
3236 enable_skills: None,
3237 embedding_cache_storage: None,
3238 enable_mcp_apps: None,
3239 skill_directories: None,
3240 instruction_directories: None,
3241 plugin_directories: None,
3242 large_output: None,
3243 disabled_skills: None,
3244 hooks: None,
3245 custom_agents: None,
3246 default_agent: None,
3247 agent: None,
3248 infinite_sessions: None,
3249 provider: None,
3250 capi: None,
3251 providers: None,
3252 models: None,
3253 enable_session_telemetry: None,
3254 enable_citations: None,
3255 session_limits: None,
3256 model_capabilities: None,
3257 memory: None,
3258 config_directory: None,
3259 working_directory: None,
3260 github_token: None,
3261 remote_session: None,
3262 include_sub_agent_streaming_events: None,
3263 commands: None,
3264 exp_assignments: None,
3265 session_fs_provider: None,
3266 suppress_resume_event: None,
3267 continue_pending_work: None,
3268 permission_handler: None,
3269 elicitation_handler: None,
3270 mcp_auth_handler: None,
3271 user_input_handler: None,
3272 exit_plan_mode_handler: None,
3273 auto_mode_switch_handler: None,
3274 hooks_handler: None,
3275 permission_policy: None,
3276 system_message_transform: None,
3277 skip_custom_instructions: None,
3278 custom_agents_local_only: None,
3279 coauthor_enabled: None,
3280 manage_schedule_enabled: None,
3281 }
3282 }
3283
3284 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3286 self.permission_handler = Some(handler);
3287 self
3288 }
3289
3290 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3292 self.elicitation_handler = Some(handler);
3293 self
3294 }
3295
3296 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3298 self.mcp_auth_handler = Some(handler);
3299 self
3300 }
3301
3302 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3304 self.user_input_handler = Some(handler);
3305 self
3306 }
3307
3308 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3310 self.exit_plan_mode_handler = Some(handler);
3311 self
3312 }
3313
3314 pub fn with_auto_mode_switch_handler(
3316 mut self,
3317 handler: Arc<dyn AutoModeSwitchHandler>,
3318 ) -> Self {
3319 self.auto_mode_switch_handler = Some(handler);
3320 self
3321 }
3322
3323 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3326 self.hooks_handler = Some(hooks);
3327 self
3328 }
3329
3330 pub fn with_system_message_transform(
3332 mut self,
3333 transform: Arc<dyn SystemMessageTransform>,
3334 ) -> Self {
3335 self.system_message_transform = Some(transform);
3336 self
3337 }
3338
3339 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3343 self.commands = Some(commands);
3344 self
3345 }
3346
3347 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3350 self.session_fs_provider = Some(provider);
3351 self
3352 }
3353
3354 pub fn approve_all_permissions(mut self) -> Self {
3357 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3358 self
3359 }
3360
3361 pub fn deny_all_permissions(mut self) -> Self {
3364 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3365 self
3366 }
3367
3368 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3371 where
3372 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3373 {
3374 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3375 self
3376 }
3377
3378 pub fn with_model(mut self, model: impl Into<String>) -> Self {
3380 self.model = Some(model.into());
3381 self
3382 }
3383
3384 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3386 self.client_name = Some(name.into());
3387 self
3388 }
3389
3390 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3392 self.reasoning_effort = Some(effort.into());
3393 self
3394 }
3395
3396 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3398 self.reasoning_summary = Some(summary);
3399 self
3400 }
3401
3402 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3405 self.context_tier = Some(tier.into());
3406 self
3407 }
3408
3409 pub fn with_streaming(mut self, streaming: bool) -> Self {
3411 self.streaming = Some(streaming);
3412 self
3413 }
3414
3415 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3418 self.system_message = Some(system_message);
3419 self
3420 }
3421
3422 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3424 self.tools = Some(tools.into_iter().collect());
3425 self
3426 }
3427
3428 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3430 self.canvases = Some(canvases.into_iter().collect());
3431 self
3432 }
3433
3434 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
3436 self.canvas_handler = Some(handler);
3437 self
3438 }
3439
3440 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
3442 mut self,
3443 open_canvases: I,
3444 ) -> Self {
3445 self.open_canvases = Some(open_canvases.into_iter().collect());
3446 self
3447 }
3448
3449 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
3451 self.request_canvas_renderer = Some(request);
3452 self
3453 }
3454
3455 pub fn with_request_extensions(mut self, request: bool) -> Self {
3457 self.request_extensions = Some(request);
3458 self
3459 }
3460
3461 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
3465 self.extension_sdk_path = Some(path.into());
3466 self
3467 }
3468
3469 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
3471 self.extension_info = Some(extension_info);
3472 self
3473 }
3474
3475 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
3477 where
3478 I: IntoIterator<Item = S>,
3479 S: Into<String>,
3480 {
3481 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
3482 self
3483 }
3484
3485 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
3487 where
3488 I: IntoIterator<Item = S>,
3489 S: Into<String>,
3490 {
3491 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
3492 self
3493 }
3494
3495 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
3497 where
3498 I: IntoIterator<Item = S>,
3499 S: Into<String>,
3500 {
3501 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
3502 self
3503 }
3504
3505 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
3507 self.mcp_servers = Some(servers);
3508 self
3509 }
3510
3511 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3514 self.mcp_oauth_token_storage = Some(mode.into());
3515 self
3516 }
3517
3518 pub fn with_embedding_cache_storage(
3520 mut self,
3521 embedding_cache_storage: impl Into<String>,
3522 ) -> Self {
3523 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3524 self
3525 }
3526
3527 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3529 self.enable_config_discovery = Some(enable);
3530 self
3531 }
3532
3533 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3535 self.skip_embedding_retrieval = Some(value);
3536 self
3537 }
3538
3539 pub fn with_organization_custom_instructions(
3541 mut self,
3542 instructions: impl Into<String>,
3543 ) -> Self {
3544 self.organization_custom_instructions = Some(instructions.into());
3545 self
3546 }
3547
3548 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3550 self.enable_on_demand_instruction_discovery = Some(value);
3551 self
3552 }
3553
3554 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3556 self.enable_file_hooks = Some(value);
3557 self
3558 }
3559
3560 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3562 self.enable_host_git_operations = Some(value);
3563 self
3564 }
3565
3566 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3568 self.enable_session_store = Some(value);
3569 self
3570 }
3571
3572 pub fn with_enable_skills(mut self, value: bool) -> Self {
3574 self.enable_skills = Some(value);
3575 self
3576 }
3577
3578 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3584 self.enable_mcp_apps = Some(enable);
3585 self
3586 }
3587
3588 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3590 where
3591 I: IntoIterator<Item = P>,
3592 P: Into<PathBuf>,
3593 {
3594 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3595 self
3596 }
3597
3598 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3602 where
3603 I: IntoIterator<Item = P>,
3604 P: Into<PathBuf>,
3605 {
3606 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3607 self
3608 }
3609
3610 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3612 where
3613 I: IntoIterator<Item = P>,
3614 P: Into<PathBuf>,
3615 {
3616 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3617 self
3618 }
3619
3620 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3622 self.large_output = Some(config);
3623 self
3624 }
3625
3626 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3628 where
3629 I: IntoIterator<Item = S>,
3630 S: Into<String>,
3631 {
3632 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3633 self
3634 }
3635
3636 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3638 mut self,
3639 agents: I,
3640 ) -> Self {
3641 self.custom_agents = Some(agents.into_iter().collect());
3642 self
3643 }
3644
3645 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3647 self.default_agent = Some(agent);
3648 self
3649 }
3650
3651 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3653 self.agent = Some(name.into());
3654 self
3655 }
3656
3657 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3659 self.infinite_sessions = Some(config);
3660 self
3661 }
3662
3663 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3665 self.provider = Some(provider);
3666 self
3667 }
3668
3669 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3671 self.capi = Some(capi);
3672 self
3673 }
3674
3675 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3681 self.providers = Some(providers);
3682 self
3683 }
3684
3685 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3691 self.models = Some(models);
3692 self
3693 }
3694
3695 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3699 self.enable_session_telemetry = Some(enable);
3700 self
3701 }
3702
3703 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3705 self.enable_citations = Some(enable);
3706 self
3707 }
3708
3709 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3711 self.session_limits = Some(limits);
3712 self
3713 }
3714
3715 pub fn with_model_capabilities(
3717 mut self,
3718 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3719 ) -> Self {
3720 self.model_capabilities = Some(capabilities);
3721 self
3722 }
3723
3724 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3726 self.memory = Some(memory);
3727 self
3728 }
3729
3730 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3732 self.config_directory = Some(dir.into());
3733 self
3734 }
3735
3736 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3738 self.working_directory = Some(dir.into());
3739 self
3740 }
3741
3742 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3746 self.github_token = Some(token.into());
3747 self
3748 }
3749
3750 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3752 self.include_sub_agent_streaming_events = Some(include);
3753 self
3754 }
3755
3756 pub fn with_remote_session(
3758 mut self,
3759 mode: crate::generated::api_types::RemoteSessionMode,
3760 ) -> Self {
3761 self.remote_session = Some(mode);
3762 self
3763 }
3764
3765 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
3768 self.suppress_resume_event = Some(suppress);
3769 self
3770 }
3771
3772 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
3778 self.continue_pending_work = Some(continue_pending);
3779 self
3780 }
3781
3782 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3784 self.skip_custom_instructions = Some(value);
3785 self
3786 }
3787
3788 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3790 self.custom_agents_local_only = Some(value);
3791 self
3792 }
3793
3794 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3796 self.coauthor_enabled = Some(value);
3797 self
3798 }
3799
3800 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3802 self.manage_schedule_enabled = Some(value);
3803 self
3804 }
3805
3806 #[doc(hidden)]
3810 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
3811 self.exp_assignments = Some(assignments);
3812 self
3813 }
3814}
3815
3816#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3822#[serde(rename_all = "camelCase")]
3823#[non_exhaustive]
3824pub struct SystemMessageConfig {
3825 #[serde(skip_serializing_if = "Option::is_none")]
3827 pub mode: Option<String>,
3828 #[serde(skip_serializing_if = "Option::is_none")]
3830 pub content: Option<String>,
3831 #[serde(skip_serializing_if = "Option::is_none")]
3833 pub sections: Option<HashMap<String, SectionOverride>>,
3834}
3835
3836impl SystemMessageConfig {
3837 pub fn new() -> Self {
3840 Self::default()
3841 }
3842
3843 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
3846 self.mode = Some(mode.into());
3847 self
3848 }
3849
3850 pub fn with_content(mut self, content: impl Into<String>) -> Self {
3853 self.content = Some(content.into());
3854 self
3855 }
3856
3857 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
3859 self.sections = Some(sections);
3860 self
3861 }
3862}
3863
3864#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3870#[serde(rename_all = "camelCase")]
3871pub struct SectionOverride {
3872 #[serde(skip_serializing_if = "Option::is_none")]
3875 pub action: Option<String>,
3876 #[serde(skip_serializing_if = "Option::is_none")]
3878 pub content: Option<String>,
3879}
3880
3881#[derive(Debug, Clone, Serialize, Deserialize)]
3883#[serde(rename_all = "camelCase")]
3884pub struct CreateSessionResult {
3885 pub session_id: SessionId,
3887 #[serde(skip_serializing_if = "Option::is_none")]
3889 pub workspace_path: Option<PathBuf>,
3890 #[serde(default, alias = "remote_url")]
3892 pub remote_url: Option<String>,
3893 #[serde(skip_serializing_if = "Option::is_none")]
3895 pub capabilities: Option<SessionCapabilities>,
3896}
3897
3898#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3900#[serde(rename_all = "camelCase")]
3901pub(crate) struct ResumeSessionResult {
3902 #[serde(default)]
3904 pub session_id: Option<SessionId>,
3905 #[serde(default, skip_serializing_if = "Option::is_none")]
3907 pub workspace_path: Option<PathBuf>,
3908 #[serde(default, alias = "remote_url")]
3910 pub remote_url: Option<String>,
3911 #[serde(default, skip_serializing_if = "Option::is_none")]
3913 pub capabilities: Option<SessionCapabilities>,
3914 #[serde(
3916 default,
3917 alias = "openCanvasInstances",
3918 skip_serializing_if = "Option::is_none"
3919 )]
3920 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3921}
3922
3923#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3925#[serde(rename_all = "lowercase")]
3926pub enum LogLevel {
3927 #[default]
3929 Info,
3930 Warning,
3932 Error,
3934}
3935
3936#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
3941#[serde(rename_all = "camelCase")]
3942pub struct LogOptions {
3943 #[serde(skip_serializing_if = "Option::is_none")]
3945 pub level: Option<LogLevel>,
3946 #[serde(skip_serializing_if = "Option::is_none")]
3949 pub ephemeral: Option<bool>,
3950}
3951
3952impl LogOptions {
3953 pub fn with_level(mut self, level: LogLevel) -> Self {
3955 self.level = Some(level);
3956 self
3957 }
3958
3959 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
3961 self.ephemeral = Some(ephemeral);
3962 self
3963 }
3964}
3965
3966#[derive(Debug, Clone, Default)]
3970pub struct SetModelOptions {
3971 pub reasoning_effort: Option<String>,
3974 pub reasoning_summary: Option<ReasoningSummary>,
3978 pub context_tier: Option<ContextTier>,
3981 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3985}
3986
3987impl SetModelOptions {
3988 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3990 self.reasoning_effort = Some(effort.into());
3991 self
3992 }
3993
3994 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3996 self.reasoning_summary = Some(summary);
3997 self
3998 }
3999
4000 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4002 self.context_tier = Some(tier);
4003 self
4004 }
4005
4006 pub fn with_model_capabilities(
4008 mut self,
4009 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4010 ) -> Self {
4011 self.model_capabilities = Some(caps);
4012 self
4013 }
4014}
4015
4016#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4023#[serde(rename_all = "camelCase")]
4024pub struct PingResponse {
4025 #[serde(default)]
4027 pub message: String,
4028 #[serde(default)]
4030 pub timestamp: String,
4031 #[serde(skip_serializing_if = "Option::is_none")]
4033 pub protocol_version: Option<u32>,
4034}
4035
4036#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4038#[serde(rename_all = "camelCase")]
4039pub struct AttachmentLineRange {
4040 pub start: u32,
4042 pub end: u32,
4044}
4045
4046#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4048#[serde(rename_all = "camelCase")]
4049pub struct AttachmentSelectionPosition {
4050 pub line: u32,
4052 pub character: u32,
4054}
4055
4056#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4058#[serde(rename_all = "camelCase")]
4059pub struct AttachmentSelectionRange {
4060 pub start: AttachmentSelectionPosition,
4062 pub end: AttachmentSelectionPosition,
4064}
4065
4066#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4068#[serde(rename_all = "snake_case")]
4069#[non_exhaustive]
4070pub enum GitHubReferenceType {
4071 Issue,
4073 Pr,
4075 Discussion,
4077}
4078
4079#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4085#[serde(rename_all = "camelCase")]
4086pub struct GitHubRepoPointer {
4087 #[serde(skip_serializing_if = "Option::is_none")]
4089 pub id: Option<i64>,
4090 pub name: String,
4092 pub owner: String,
4094}
4095
4096#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4098#[serde(rename_all = "camelCase")]
4099pub struct GitHubFileDiffSide {
4100 pub path: String,
4102 pub r#ref: String,
4104 pub repo: GitHubRepoPointer,
4106}
4107
4108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4110#[serde(rename_all = "camelCase")]
4111pub struct GitHubTreeComparisonSide {
4112 pub repo: GitHubRepoPointer,
4114 pub revision: String,
4116}
4117
4118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4120#[serde(rename_all = "camelCase")]
4121pub struct GitHubSnippetLineRange {
4122 pub start: i64,
4124 pub end: i64,
4126}
4127
4128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4130#[serde(
4131 tag = "type",
4132 rename_all = "camelCase",
4133 rename_all_fields = "camelCase"
4134)]
4135#[non_exhaustive]
4136pub enum Attachment {
4137 File {
4139 path: PathBuf,
4141 #[serde(skip_serializing_if = "Option::is_none")]
4143 display_name: Option<String>,
4144 #[serde(skip_serializing_if = "Option::is_none")]
4146 line_range: Option<AttachmentLineRange>,
4147 },
4148 Directory {
4150 path: PathBuf,
4152 #[serde(skip_serializing_if = "Option::is_none")]
4154 display_name: Option<String>,
4155 },
4156 Selection {
4158 file_path: PathBuf,
4160 text: String,
4162 #[serde(skip_serializing_if = "Option::is_none")]
4164 display_name: Option<String>,
4165 selection: AttachmentSelectionRange,
4167 },
4168 Blob {
4170 data: String,
4172 mime_type: String,
4174 #[serde(skip_serializing_if = "Option::is_none")]
4176 display_name: Option<String>,
4177 },
4178 #[serde(rename = "github_reference")]
4180 GitHubReference {
4181 number: u64,
4183 title: String,
4185 reference_type: GitHubReferenceType,
4187 state: String,
4189 url: String,
4191 },
4192 #[serde(rename = "github_commit")]
4194 GitHubCommit {
4195 message: String,
4197 oid: String,
4199 repo: GitHubRepoPointer,
4201 url: String,
4203 },
4204 #[serde(rename = "github_release")]
4206 GitHubRelease {
4207 name: String,
4209 repo: GitHubRepoPointer,
4211 tag_name: String,
4213 url: String,
4215 },
4216 #[serde(rename = "github_actions_job")]
4218 GitHubActionsJob {
4219 #[serde(skip_serializing_if = "Option::is_none")]
4222 conclusion: Option<String>,
4223 job_id: i64,
4225 job_name: String,
4227 repo: GitHubRepoPointer,
4229 url: String,
4231 workflow_name: String,
4233 },
4234 #[serde(rename = "github_repository")]
4236 GitHubRepository {
4237 #[serde(skip_serializing_if = "Option::is_none")]
4239 description: Option<String>,
4240 #[serde(skip_serializing_if = "Option::is_none")]
4243 r#ref: Option<String>,
4244 repo: GitHubRepoPointer,
4246 url: String,
4248 },
4249 #[serde(rename = "github_file_diff")]
4251 GitHubFileDiff {
4252 #[serde(skip_serializing_if = "Option::is_none")]
4254 base: Option<GitHubFileDiffSide>,
4255 #[serde(skip_serializing_if = "Option::is_none")]
4257 head: Option<GitHubFileDiffSide>,
4258 url: String,
4260 },
4261 #[serde(rename = "github_tree_comparison")]
4263 GitHubTreeComparison {
4264 base: GitHubTreeComparisonSide,
4266 head: GitHubTreeComparisonSide,
4268 url: String,
4270 },
4271 #[serde(rename = "github_url")]
4273 GitHubUrl {
4274 url: String,
4276 },
4277 #[serde(rename = "github_file")]
4279 GitHubFile {
4280 path: String,
4282 r#ref: String,
4284 repo: GitHubRepoPointer,
4286 url: String,
4288 },
4289 #[serde(rename = "github_snippet")]
4291 GitHubSnippet {
4292 line_range: GitHubSnippetLineRange,
4294 path: String,
4296 r#ref: String,
4298 repo: GitHubRepoPointer,
4300 url: String,
4302 },
4303}
4304
4305impl Attachment {
4306 pub fn display_name(&self) -> Option<&str> {
4308 match self {
4309 Self::File { display_name, .. }
4310 | Self::Directory { display_name, .. }
4311 | Self::Selection { display_name, .. }
4312 | Self::Blob { display_name, .. } => display_name.as_deref(),
4313 Self::GitHubReference { .. }
4314 | Self::GitHubCommit { .. }
4315 | Self::GitHubRelease { .. }
4316 | Self::GitHubActionsJob { .. }
4317 | Self::GitHubRepository { .. }
4318 | Self::GitHubFileDiff { .. }
4319 | Self::GitHubTreeComparison { .. }
4320 | Self::GitHubUrl { .. }
4321 | Self::GitHubFile { .. }
4322 | Self::GitHubSnippet { .. } => None,
4323 }
4324 }
4325
4326 pub fn label(&self) -> Option<String> {
4328 if let Some(display_name) = self
4329 .display_name()
4330 .map(str::trim)
4331 .filter(|name| !name.is_empty())
4332 {
4333 return Some(display_name.to_string());
4334 }
4335
4336 match self {
4337 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4338 format!("#{}", number)
4339 } else {
4340 title.trim().to_string()
4341 }),
4342 _ => self.derived_display_name(),
4343 }
4344 }
4345
4346 pub fn ensure_display_name(&mut self) {
4348 if self
4349 .display_name()
4350 .map(str::trim)
4351 .is_some_and(|name| !name.is_empty())
4352 {
4353 return;
4354 }
4355
4356 let Some(derived_display_name) = self.derived_display_name() else {
4357 return;
4358 };
4359
4360 match self {
4361 Self::File { display_name, .. }
4362 | Self::Directory { display_name, .. }
4363 | Self::Selection { display_name, .. }
4364 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4365 Self::GitHubReference { .. }
4366 | Self::GitHubCommit { .. }
4367 | Self::GitHubRelease { .. }
4368 | Self::GitHubActionsJob { .. }
4369 | Self::GitHubRepository { .. }
4370 | Self::GitHubFileDiff { .. }
4371 | Self::GitHubTreeComparison { .. }
4372 | Self::GitHubUrl { .. }
4373 | Self::GitHubFile { .. }
4374 | Self::GitHubSnippet { .. } => {}
4375 }
4376 }
4377
4378 fn derived_display_name(&self) -> Option<String> {
4379 match self {
4380 Self::File { path, .. } | Self::Directory { path, .. } => {
4381 Some(attachment_name_from_path(path))
4382 }
4383 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4384 Self::Blob { .. } => Some("attachment".to_string()),
4385 Self::GitHubReference { .. }
4386 | Self::GitHubCommit { .. }
4387 | Self::GitHubRelease { .. }
4388 | Self::GitHubActionsJob { .. }
4389 | Self::GitHubRepository { .. }
4390 | Self::GitHubFileDiff { .. }
4391 | Self::GitHubTreeComparison { .. }
4392 | Self::GitHubUrl { .. }
4393 | Self::GitHubFile { .. }
4394 | Self::GitHubSnippet { .. } => None,
4395 }
4396 }
4397}
4398
4399fn attachment_name_from_path(path: &Path) -> String {
4400 path.file_name()
4401 .map(|name| name.to_string_lossy().into_owned())
4402 .filter(|name| !name.is_empty())
4403 .unwrap_or_else(|| {
4404 let full = path.to_string_lossy();
4405 if full.is_empty() {
4406 "attachment".to_string()
4407 } else {
4408 full.into_owned()
4409 }
4410 })
4411}
4412
4413pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
4415 for attachment in attachments {
4416 attachment.ensure_display_name();
4417 }
4418}
4419
4420#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4425#[serde(rename_all = "lowercase")]
4426#[non_exhaustive]
4427pub enum DeliveryMode {
4428 Enqueue,
4430 Immediate,
4432}
4433
4434#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4439#[serde(rename_all = "lowercase")]
4440#[non_exhaustive]
4441pub enum AgentMode {
4442 Interactive,
4444 Plan,
4446 Autopilot,
4448 Shell,
4450}
4451
4452#[derive(Debug, Clone)]
4481#[non_exhaustive]
4482pub struct MessageOptions {
4483 pub prompt: String,
4485 pub mode: Option<DeliveryMode>,
4491 pub agent_mode: Option<AgentMode>,
4495 pub attachments: Option<Vec<Attachment>>,
4497 pub wait_timeout: Option<Duration>,
4500 pub request_headers: Option<HashMap<String, String>>,
4504 pub traceparent: Option<String>,
4511 pub tracestate: Option<String>,
4515 pub display_prompt: Option<String>,
4517}
4518
4519impl MessageOptions {
4520 pub fn new(prompt: impl Into<String>) -> Self {
4522 Self {
4523 prompt: prompt.into(),
4524 mode: None,
4525 agent_mode: None,
4526 attachments: None,
4527 wait_timeout: None,
4528 request_headers: None,
4529 traceparent: None,
4530 tracestate: None,
4531 display_prompt: None,
4532 }
4533 }
4534
4535 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
4541 self.mode = Some(mode);
4542 self
4543 }
4544
4545 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
4549 self.agent_mode = Some(agent_mode);
4550 self
4551 }
4552
4553 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
4555 self.attachments = Some(attachments);
4556 self
4557 }
4558
4559 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
4561 self.wait_timeout = Some(timeout);
4562 self
4563 }
4564
4565 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
4567 self.request_headers = Some(headers);
4568 self
4569 }
4570
4571 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
4576 self.traceparent = ctx.traceparent;
4577 self.tracestate = ctx.tracestate;
4578 self
4579 }
4580
4581 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
4583 self.traceparent = Some(traceparent.into());
4584 self
4585 }
4586
4587 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
4589 self.tracestate = Some(tracestate.into());
4590 self
4591 }
4592
4593 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
4595 self.display_prompt = Some(display_prompt.into());
4596 self
4597 }
4598}
4599
4600impl From<&str> for MessageOptions {
4601 fn from(prompt: &str) -> Self {
4602 Self::new(prompt)
4603 }
4604}
4605
4606impl From<String> for MessageOptions {
4607 fn from(prompt: String) -> Self {
4608 Self::new(prompt)
4609 }
4610}
4611
4612impl From<&String> for MessageOptions {
4613 fn from(prompt: &String) -> Self {
4614 Self::new(prompt.clone())
4615 }
4616}
4617
4618#[derive(Debug, Clone, Serialize, Deserialize)]
4620#[serde(rename_all = "camelCase")]
4621#[non_exhaustive]
4622pub struct GetStatusResponse {
4623 pub version: String,
4625 pub protocol_version: u32,
4627}
4628
4629#[derive(Debug, Clone, Serialize, Deserialize)]
4631#[serde(rename_all = "camelCase")]
4632#[non_exhaustive]
4633pub struct GetAuthStatusResponse {
4634 pub is_authenticated: bool,
4636 #[serde(skip_serializing_if = "Option::is_none")]
4639 pub auth_type: Option<String>,
4640 #[serde(skip_serializing_if = "Option::is_none")]
4642 pub host: Option<String>,
4643 #[serde(skip_serializing_if = "Option::is_none")]
4645 pub login: Option<String>,
4646 #[serde(skip_serializing_if = "Option::is_none")]
4648 pub status_message: Option<String>,
4649}
4650
4651#[derive(Debug, Clone, Serialize, Deserialize)]
4655#[serde(rename_all = "camelCase")]
4656pub struct SessionEventNotification {
4657 pub session_id: SessionId,
4659 pub event: SessionEvent,
4661}
4662
4663#[derive(Debug, Clone, Serialize, Deserialize)]
4670#[serde(rename_all = "camelCase")]
4671pub struct SessionEvent {
4672 pub id: String,
4674 pub timestamp: String,
4676 pub parent_id: Option<String>,
4678 #[serde(skip_serializing_if = "Option::is_none")]
4680 pub ephemeral: Option<bool>,
4681 #[serde(skip_serializing_if = "Option::is_none")]
4684 pub agent_id: Option<String>,
4685 #[serde(skip_serializing_if = "Option::is_none")]
4687 pub debug_cli_received_at_ms: Option<i64>,
4688 #[serde(skip_serializing_if = "Option::is_none")]
4690 pub debug_ws_forwarded_at_ms: Option<i64>,
4691 #[serde(rename = "type")]
4693 pub event_type: String,
4694 pub data: Value,
4696}
4697
4698impl SessionEvent {
4699 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
4704 use serde::de::IntoDeserializer;
4705 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
4706 self.event_type.as_str().into_deserializer();
4707 crate::generated::SessionEventType::deserialize(deserializer)
4708 .unwrap_or(crate::generated::SessionEventType::Unknown)
4709 }
4710
4711 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
4717 serde_json::from_value(self.data.clone()).ok()
4718 }
4719
4720 pub fn is_transient_error(&self) -> bool {
4724 self.event_type == "session.error"
4725 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
4726 }
4727}
4728
4729#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4734#[serde(rename_all = "camelCase")]
4735#[non_exhaustive]
4736pub struct ToolInvocation {
4737 pub session_id: SessionId,
4739 pub tool_call_id: String,
4741 pub tool_name: String,
4743 pub arguments: Value,
4745 #[serde(default, skip_serializing_if = "Option::is_none")]
4750 pub traceparent: Option<String>,
4751 #[serde(default, skip_serializing_if = "Option::is_none")]
4754 pub tracestate: Option<String>,
4755}
4756
4757impl ToolInvocation {
4758 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
4779 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
4780 }
4781
4782 pub fn trace_context(&self) -> TraceContext {
4785 TraceContext {
4786 traceparent: self.traceparent.clone(),
4787 tracestate: self.tracestate.clone(),
4788 }
4789 }
4790}
4791
4792#[derive(Debug, Clone, Serialize, Deserialize)]
4794#[serde(rename_all = "camelCase")]
4795pub struct ToolBinaryResult {
4796 pub data: String,
4798 pub mime_type: String,
4800 pub r#type: String,
4802 #[serde(default, skip_serializing_if = "Option::is_none")]
4804 pub description: Option<String>,
4805}
4806
4807#[derive(Debug, Clone, Serialize, Deserialize)]
4809#[serde(rename_all = "camelCase")]
4810pub struct ToolResultExpanded {
4811 pub text_result_for_llm: String,
4813 pub result_type: String,
4815 #[serde(default, skip_serializing_if = "Option::is_none")]
4817 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
4818 #[serde(skip_serializing_if = "Option::is_none")]
4820 pub session_log: Option<String>,
4821 #[serde(skip_serializing_if = "Option::is_none")]
4823 pub error: Option<String>,
4824 #[serde(default, skip_serializing_if = "Option::is_none")]
4826 pub tool_telemetry: Option<HashMap<String, Value>>,
4827}
4828
4829#[derive(Debug, Clone, Serialize, Deserialize)]
4831#[serde(untagged)]
4832#[non_exhaustive]
4833pub enum ToolResult {
4834 Text(String),
4836 Expanded(ToolResultExpanded),
4838}
4839
4840#[derive(Debug, Clone, Serialize, Deserialize)]
4842#[serde(rename_all = "camelCase")]
4843pub struct ToolResultResponse {
4844 pub result: ToolResult,
4846}
4847
4848#[derive(Debug, Clone, Serialize, Deserialize)]
4850#[serde(rename_all = "camelCase")]
4851pub struct SessionMetadata {
4852 pub session_id: SessionId,
4854 pub start_time: String,
4856 pub modified_time: String,
4858 #[serde(skip_serializing_if = "Option::is_none")]
4860 pub summary: Option<String>,
4861 pub is_remote: bool,
4863}
4864
4865#[derive(Debug, Clone, Serialize, Deserialize)]
4867#[serde(rename_all = "camelCase")]
4868pub struct ListSessionsResponse {
4869 pub sessions: Vec<SessionMetadata>,
4871}
4872
4873#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4877#[serde(rename_all = "camelCase")]
4878pub struct SessionListFilter {
4879 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
4881 pub working_directory: Option<String>,
4882 #[serde(default, skip_serializing_if = "Option::is_none")]
4884 pub git_root: Option<String>,
4885 #[serde(default, skip_serializing_if = "Option::is_none")]
4887 pub repository: Option<String>,
4888 #[serde(default, skip_serializing_if = "Option::is_none")]
4890 pub branch: Option<String>,
4891}
4892
4893#[derive(Debug, Clone, Serialize, Deserialize)]
4895#[serde(rename_all = "camelCase")]
4896pub struct GetSessionMetadataResponse {
4897 #[serde(skip_serializing_if = "Option::is_none")]
4899 pub session: Option<SessionMetadata>,
4900}
4901
4902#[derive(Debug, Clone, Serialize, Deserialize)]
4904#[serde(rename_all = "camelCase")]
4905pub struct GetLastSessionIdResponse {
4906 #[serde(skip_serializing_if = "Option::is_none")]
4908 pub session_id: Option<SessionId>,
4909}
4910
4911#[derive(Debug, Clone, Serialize, Deserialize)]
4913#[serde(rename_all = "camelCase")]
4914pub struct GetForegroundSessionResponse {
4915 #[serde(skip_serializing_if = "Option::is_none")]
4917 pub session_id: Option<SessionId>,
4918}
4919
4920#[derive(Debug, Clone, Serialize, Deserialize)]
4922#[serde(rename_all = "camelCase")]
4923pub struct GetMessagesResponse {
4924 pub events: Vec<SessionEvent>,
4926}
4927
4928#[derive(Debug, Clone, Serialize, Deserialize)]
4930#[serde(rename_all = "camelCase")]
4931pub struct ElicitationResult {
4932 pub action: String,
4934 #[serde(skip_serializing_if = "Option::is_none")]
4936 pub content: Option<Value>,
4937}
4938
4939#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4945#[serde(rename_all = "camelCase")]
4946#[non_exhaustive]
4947pub enum ElicitationMode {
4948 Form,
4950 Url,
4952 #[serde(other)]
4954 Unknown,
4955}
4956
4957#[derive(Debug, Clone, Serialize, Deserialize)]
4964#[serde(rename_all = "camelCase")]
4965pub struct ElicitationRequest {
4966 pub message: String,
4968 #[serde(skip_serializing_if = "Option::is_none")]
4970 pub requested_schema: Option<Value>,
4971 #[serde(skip_serializing_if = "Option::is_none")]
4973 pub mode: Option<ElicitationMode>,
4974 #[serde(skip_serializing_if = "Option::is_none")]
4976 pub elicitation_source: Option<String>,
4977 #[serde(skip_serializing_if = "Option::is_none")]
4979 pub url: Option<String>,
4980}
4981
4982#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4987#[serde(rename_all = "camelCase")]
4988pub struct SessionCapabilities {
4989 #[serde(skip_serializing_if = "Option::is_none")]
4991 pub ui: Option<UiCapabilities>,
4992}
4993
4994#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4996#[serde(rename_all = "camelCase")]
4997pub struct UiCapabilities {
4998 #[serde(skip_serializing_if = "Option::is_none")]
5000 pub elicitation: Option<bool>,
5001 #[serde(skip_serializing_if = "Option::is_none")]
5012 pub mcp_apps: Option<bool>,
5013 #[serde(skip_serializing_if = "Option::is_none")]
5015 pub canvases: Option<bool>,
5016}
5017
5018#[derive(Debug, Clone, Default)]
5020pub struct UiInputOptions<'a> {
5021 pub title: Option<&'a str>,
5023 pub description: Option<&'a str>,
5025 pub min_length: Option<u64>,
5027 pub max_length: Option<u64>,
5029 pub format: Option<InputFormat>,
5031 pub default: Option<&'a str>,
5033}
5034
5035#[derive(Debug, Clone, Copy)]
5037#[non_exhaustive]
5038pub enum InputFormat {
5039 Email,
5041 Uri,
5043 Date,
5045 DateTime,
5047}
5048
5049impl InputFormat {
5050 pub fn as_str(&self) -> &'static str {
5052 match self {
5053 Self::Email => "email",
5054 Self::Uri => "uri",
5055 Self::Date => "date",
5056 Self::DateTime => "date-time",
5057 }
5058 }
5059}
5060
5061pub use crate::generated::api_types::{
5066 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5067 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5068 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5069 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
5070};
5071
5072#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5078#[serde(rename_all = "kebab-case")]
5079#[non_exhaustive]
5080pub enum PermissionRequestKind {
5081 Shell,
5083 Write,
5085 Read,
5087 Url,
5089 Mcp,
5091 CustomTool,
5093 Memory,
5095 Hook,
5097 #[serde(other)]
5100 Unknown,
5101}
5102
5103#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5109#[serde(rename_all = "camelCase")]
5110pub struct PermissionRequestData {
5111 #[serde(default, skip_serializing_if = "Option::is_none")]
5115 pub kind: Option<PermissionRequestKind>,
5116 #[serde(default, skip_serializing_if = "Option::is_none")]
5119 pub tool_call_id: Option<String>,
5120 #[serde(flatten)]
5123 pub extra: Value,
5124}
5125
5126#[derive(Debug, Clone, Serialize, Deserialize)]
5128#[serde(rename_all = "camelCase")]
5129pub struct ExitPlanModeData {
5130 #[serde(default)]
5132 pub summary: String,
5133 #[serde(default, skip_serializing_if = "Option::is_none")]
5135 pub plan_content: Option<String>,
5136 #[serde(default)]
5138 pub actions: Vec<String>,
5139 #[serde(default = "default_recommended_action")]
5141 pub recommended_action: String,
5142}
5143
5144fn default_recommended_action() -> String {
5145 "autopilot".to_string()
5146}
5147
5148impl Default for ExitPlanModeData {
5149 fn default() -> Self {
5150 Self {
5151 summary: String::new(),
5152 plan_content: None,
5153 actions: Vec::new(),
5154 recommended_action: default_recommended_action(),
5155 }
5156 }
5157}
5158
5159#[cfg(test)]
5160mod tests {
5161 use std::path::PathBuf;
5162
5163 use serde_json::json;
5164
5165 use super::{
5166 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5167 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5168 CustomAgentConfig, DeliveryMode, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig,
5169 LargeToolOutputConfig, MemoryConfiguration, NamedProviderConfig, ProviderConfig,
5170 ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
5171 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
5172 ToolResultResponse, ensure_attachment_display_names,
5173 };
5174 use crate::generated::session_events::TypedSessionEvent;
5175
5176 #[test]
5177 fn tool_builder_composes() {
5178 let tool = Tool::new("greet")
5179 .with_description("Say hello")
5180 .with_namespaced_name("hello/greet")
5181 .with_instructions("Pass the user's name")
5182 .with_parameters(json!({
5183 "type": "object",
5184 "properties": { "name": { "type": "string" } },
5185 "required": ["name"]
5186 }))
5187 .with_overrides_built_in_tool(true)
5188 .with_skip_permission(true);
5189 assert_eq!(tool.name, "greet");
5190 assert_eq!(tool.description, "Say hello");
5191 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5192 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5193 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5194 assert!(tool.overrides_built_in_tool);
5195 assert!(tool.skip_permission);
5196 }
5197
5198 #[test]
5199 fn tool_defer_serialization() {
5200 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5201 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5202 let value = serde_json::to_value(&tool).unwrap();
5203 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5204
5205 let plain = Tool::new("plain");
5206 let value = serde_json::to_value(&plain).unwrap();
5207 assert!(value.get("defer").is_none());
5208 }
5209
5210 #[test]
5211 fn custom_agent_config_builder_with_model() {
5212 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5213 .with_model("claude-haiku-4.5")
5214 .with_display_name("My Agent");
5215 assert_eq!(agent.name, "my-agent");
5216 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5217 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5218 }
5219
5220 #[test]
5221 fn custom_agent_config_serializes_model() {
5222 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5223 let wire = serde_json::to_value(&agent).unwrap();
5224 assert_eq!(wire["model"], "claude-haiku-4.5");
5225 assert_eq!(wire["name"], "model-agent");
5226 }
5227
5228 #[test]
5229 fn custom_agent_config_omits_model_when_none() {
5230 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5231 let wire = serde_json::to_value(&agent).unwrap();
5232 assert!(wire.get("model").is_none());
5233 }
5234
5235 #[test]
5236 #[should_panic(expected = "tool parameter schema must be a JSON object")]
5237 fn tool_with_parameters_panics_on_non_object_value() {
5238 let _ = Tool::new("noop").with_parameters(json!(null));
5239 }
5240
5241 #[test]
5242 fn tool_result_expanded_serializes_binary_results_for_llm() {
5243 let response = ToolResultResponse {
5244 result: ToolResult::Expanded(ToolResultExpanded {
5245 text_result_for_llm: "rendered chart".to_string(),
5246 result_type: "success".to_string(),
5247 binary_results_for_llm: Some(vec![ToolBinaryResult {
5248 data: "aW1n".to_string(),
5249 mime_type: "image/png".to_string(),
5250 r#type: "image".to_string(),
5251 description: Some("chart preview".to_string()),
5252 }]),
5253 session_log: None,
5254 error: None,
5255 tool_telemetry: None,
5256 }),
5257 };
5258
5259 let wire = serde_json::to_value(&response).unwrap();
5260
5261 assert_eq!(
5262 wire,
5263 json!({
5264 "result": {
5265 "textResultForLlm": "rendered chart",
5266 "resultType": "success",
5267 "binaryResultsForLlm": [
5268 {
5269 "data": "aW1n",
5270 "mimeType": "image/png",
5271 "type": "image",
5272 "description": "chart preview"
5273 }
5274 ]
5275 }
5276 })
5277 );
5278 }
5279
5280 #[test]
5281 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
5282 let response = ToolResultResponse {
5283 result: ToolResult::Expanded(ToolResultExpanded {
5284 text_result_for_llm: "ok".to_string(),
5285 result_type: "success".to_string(),
5286 binary_results_for_llm: None,
5287 session_log: None,
5288 error: None,
5289 tool_telemetry: None,
5290 }),
5291 };
5292
5293 let wire = serde_json::to_value(&response).unwrap();
5294
5295 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5296 assert!(wire["result"].get("binaryResultsForLlm").is_none());
5297 }
5298
5299 #[test]
5300 fn session_config_default_wire_flags_off_without_handlers() {
5301 let cfg = SessionConfig::default();
5302 assert_eq!(cfg.mcp_oauth_token_storage, None);
5303 let (wire, _runtime) = cfg
5307 .into_wire(Some(SessionId::from("default-flags")))
5308 .expect("default config has no duplicate handlers");
5309 assert!(!wire.request_user_input);
5310 assert!(!wire.request_permission);
5311 assert!(!wire.request_elicitation);
5312 assert!(!wire.request_exit_plan_mode);
5313 assert!(!wire.request_auto_mode_switch);
5314 assert!(!wire.hooks);
5315 assert!(!wire.request_mcp_apps);
5316 }
5317
5318 #[test]
5319 fn resume_session_config_new_wire_flags_off_without_handlers() {
5320 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
5321 assert_eq!(cfg.mcp_oauth_token_storage, None);
5322 let (wire, _runtime) = cfg
5323 .into_wire()
5324 .expect("default resume config has no duplicate handlers");
5325 assert!(!wire.request_user_input);
5326 assert!(!wire.request_permission);
5327 assert!(!wire.request_elicitation);
5328 assert!(!wire.request_exit_plan_mode);
5329 assert!(!wire.request_auto_mode_switch);
5330 assert!(!wire.hooks);
5331 assert!(!wire.request_mcp_apps);
5332 }
5333
5334 #[test]
5335 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5336 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
5337 assert_eq!(cfg.enable_mcp_apps, Some(true));
5338
5339 let (wire, _runtime) = cfg
5340 .into_wire(Some(SessionId::from("enable-mcp-apps")))
5341 .expect("enable_mcp_apps config has no duplicate handlers");
5342 assert!(wire.request_mcp_apps);
5343
5344 let json = serde_json::to_value(&wire).unwrap();
5345 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5346 }
5347
5348 #[test]
5349 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5350 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
5351 .with_enable_mcp_apps(true);
5352 assert_eq!(cfg.enable_mcp_apps, Some(true));
5353
5354 let (wire, _runtime) = cfg
5355 .into_wire()
5356 .expect("resume enable_mcp_apps config has no duplicate handlers");
5357 assert!(wire.request_mcp_apps);
5358
5359 let json = serde_json::to_value(&wire).unwrap();
5360 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5361 }
5362
5363 #[test]
5364 fn memory_configuration_constructors_and_serde() {
5365 assert!(MemoryConfiguration::enabled().enabled);
5366 assert!(!MemoryConfiguration::disabled().enabled);
5367 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
5368
5369 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
5370 assert_eq!(json, serde_json::json!({ "enabled": true }));
5371 }
5372
5373 #[test]
5374 fn session_config_with_memory_serializes() {
5375 let (wire, _runtime) = SessionConfig::default()
5376 .with_memory(MemoryConfiguration::enabled())
5377 .into_wire(Some(SessionId::from("memory-on")))
5378 .expect("no duplicate handlers");
5379 let json = serde_json::to_value(&wire).unwrap();
5380 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5381
5382 let (wire_off, _) = SessionConfig::default()
5383 .with_memory(MemoryConfiguration::disabled())
5384 .into_wire(Some(SessionId::from("memory-off")))
5385 .expect("no duplicate handlers");
5386 let json_off = serde_json::to_value(&wire_off).unwrap();
5387 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
5388
5389 let (empty_wire, _) = SessionConfig::default()
5391 .into_wire(Some(SessionId::from("memory-unset")))
5392 .expect("no duplicate handlers");
5393 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5394 assert!(empty_json.get("memory").is_none());
5395 }
5396
5397 #[test]
5398 fn resume_session_config_with_memory_serializes() {
5399 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
5400 .with_memory(MemoryConfiguration::enabled())
5401 .into_wire()
5402 .expect("no duplicate handlers");
5403 let json = serde_json::to_value(&wire).unwrap();
5404 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5405
5406 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
5408 .into_wire()
5409 .expect("no duplicate handlers");
5410 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5411 assert!(empty_json.get("memory").is_none());
5412 }
5413
5414 #[test]
5415 fn session_config_with_exp_assignments_serializes() {
5416 let assignments = serde_json::json!({
5417 "Parameters": { "copilot_exp_flag": "treatment" },
5418 "AssignmentContext": "ctx-123",
5419 });
5420 let (wire, _runtime) = SessionConfig::default()
5421 .with_exp_assignments(assignments.clone())
5422 .into_wire(Some(SessionId::from("exp-on")))
5423 .expect("no duplicate handlers");
5424 let json = serde_json::to_value(&wire).unwrap();
5425 assert_eq!(json["expAssignments"], assignments);
5426
5427 let (empty_wire, _) = SessionConfig::default()
5429 .into_wire(Some(SessionId::from("exp-unset")))
5430 .expect("no duplicate handlers");
5431 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5432 assert!(empty_json.get("expAssignments").is_none());
5433 }
5434
5435 #[test]
5436 fn resume_session_config_with_exp_assignments_serializes() {
5437 let assignments = serde_json::json!({
5438 "Parameters": { "copilot_exp_flag": "treatment" },
5439 "AssignmentContext": "ctx-456",
5440 });
5441 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
5442 .with_exp_assignments(assignments.clone())
5443 .into_wire()
5444 .expect("no duplicate handlers");
5445 let json = serde_json::to_value(&wire).unwrap();
5446 assert_eq!(json["expAssignments"], assignments);
5447
5448 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
5450 .into_wire()
5451 .expect("no duplicate handlers");
5452 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5453 assert!(empty_json.get("expAssignments").is_none());
5454 }
5455
5456 #[test]
5457 fn session_config_clone_preserves_exp_assignments() {
5458 let assignments = serde_json::json!({
5459 "Parameters": { "copilot_exp_flag": "treatment" },
5460 "AssignmentContext": "ctx-clone",
5461 });
5462 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
5463 let cloned = config.clone();
5464
5465 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5466
5467 let (wire, _runtime) = cloned
5468 .into_wire(Some(SessionId::from("exp-clone")))
5469 .expect("no duplicate handlers");
5470 let json = serde_json::to_value(&wire).unwrap();
5471 assert_eq!(json["expAssignments"], assignments);
5472 }
5473
5474 #[test]
5475 fn resume_session_config_clone_preserves_exp_assignments() {
5476 let assignments = serde_json::json!({
5477 "Parameters": { "copilot_exp_flag": "treatment" },
5478 "AssignmentContext": "ctx-clone-resume",
5479 });
5480 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
5481 .with_exp_assignments(assignments.clone());
5482 let cloned = config.clone();
5483
5484 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5485
5486 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
5487 let json = serde_json::to_value(&wire).unwrap();
5488 assert_eq!(json["expAssignments"], assignments);
5489 }
5490
5491 #[test]
5492 #[allow(clippy::field_reassign_with_default)]
5493 fn session_config_into_wire_serializes_bucket_b_fields() {
5494 use std::path::PathBuf;
5495
5496 use super::{CloudSessionOptions, CloudSessionRepository};
5497
5498 let mut cfg = SessionConfig::default();
5499 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5500 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5501 cfg.github_token = Some("ghs_secret".to_string());
5502 cfg.include_sub_agent_streaming_events = Some(false);
5503 cfg.enable_session_telemetry = Some(false);
5504 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
5505 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
5506 cfg.enable_on_demand_instruction_discovery = Some(false);
5507 cfg.cloud = Some(CloudSessionOptions::with_repository(
5508 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
5509 ));
5510
5511 let (wire, _runtime) = cfg
5512 .into_wire(Some(SessionId::from("custom-id")))
5513 .expect("no duplicate handlers");
5514 let wire_json = serde_json::to_value(&wire).unwrap();
5515 assert_eq!(wire_json["sessionId"], "custom-id");
5516 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5517 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5518 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5519 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
5520 assert_eq!(wire_json["enableSessionTelemetry"], false);
5521 assert_eq!(wire_json["reasoningSummary"], "concise");
5522 assert_eq!(wire_json["remoteSession"], "export");
5523 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5524 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
5525 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
5526 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
5527
5528 let (empty_wire, _) = SessionConfig::default()
5530 .into_wire(Some(SessionId::from("empty")))
5531 .expect("default has no duplicate handlers");
5532 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5533 assert!(empty_json.get("gitHubToken").is_none());
5534 assert!(empty_json.get("enableSessionTelemetry").is_none());
5535 assert!(empty_json.get("reasoningSummary").is_none());
5536 assert!(empty_json.get("remoteSession").is_none());
5537 assert!(
5538 empty_json
5539 .get("enableOnDemandInstructionDiscovery")
5540 .is_none()
5541 );
5542 assert!(empty_json.get("cloud").is_none());
5543 }
5544
5545 #[test]
5546 fn session_config_into_wire_serializes_named_providers_and_models() {
5547 let cfg = SessionConfig::default()
5548 .with_providers(vec![
5549 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
5550 .with_provider_type("openai")
5551 .with_wire_api("responses")
5552 .with_api_key("sk-test"),
5553 ])
5554 .with_models(vec![
5555 ProviderModelConfig::new("gpt-x", "my-openai")
5556 .with_wire_model("gpt-x-2025")
5557 .with_max_output_tokens(2048),
5558 ]);
5559
5560 let (wire, _) = cfg
5561 .into_wire(Some(SessionId::from("sess-providers")))
5562 .expect("no duplicate handlers");
5563 let wire_json = serde_json::to_value(&wire).unwrap();
5564 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
5565 assert_eq!(
5566 wire_json["providers"][0]["baseUrl"],
5567 "https://api.example.com/v1"
5568 );
5569 assert_eq!(wire_json["providers"][0]["type"], "openai");
5570 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
5571 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
5572 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
5573 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
5574 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
5575 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
5576
5577 let (empty_wire, _) = SessionConfig::default()
5578 .into_wire(Some(SessionId::from("empty")))
5579 .expect("default has no duplicate handlers");
5580 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5581 assert!(empty_json.get("providers").is_none());
5582 assert!(empty_json.get("models").is_none());
5583 }
5584
5585 #[test]
5586 fn resume_config_into_wire_serializes_named_providers_and_models() {
5587 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
5588 .with_providers(vec![
5589 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
5590 .with_provider_type("azure")
5591 .with_azure(AzureProviderOptions {
5592 api_version: Some("2024-10-21".to_string()),
5593 }),
5594 ])
5595 .with_models(vec![
5596 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
5597 ]);
5598
5599 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5600 let wire_json = serde_json::to_value(&wire).unwrap();
5601 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
5602 assert_eq!(wire_json["providers"][0]["type"], "azure");
5603 assert_eq!(
5604 wire_json["providers"][0]["azure"]["apiVersion"],
5605 "2024-10-21"
5606 );
5607 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
5608 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
5609 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
5610
5611 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
5612 .into_wire()
5613 .expect("default has no duplicate handlers");
5614 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5615 assert!(empty_json.get("providers").is_none());
5616 assert!(empty_json.get("models").is_none());
5617 }
5618
5619 #[test]
5620 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
5621 use std::path::PathBuf;
5622
5623 let cfg = SessionConfig {
5624 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
5625 large_output: Some(
5626 LargeToolOutputConfig::new()
5627 .with_enabled(true)
5628 .with_max_size_bytes(1024)
5629 .with_output_directory(PathBuf::from("/tmp/large-output")),
5630 ),
5631 ..Default::default()
5632 };
5633
5634 let (wire, _) = cfg
5635 .into_wire(Some(SessionId::from("sess-1")))
5636 .expect("no duplicate handlers");
5637 let wire_json = serde_json::to_value(&wire).unwrap();
5638 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
5639 assert_eq!(wire_json["largeOutput"]["enabled"], true);
5640 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
5641 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
5642
5643 let (empty_wire, _) = SessionConfig::default()
5644 .into_wire(Some(SessionId::from("empty")))
5645 .expect("default has no duplicate handlers");
5646 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5647 assert!(empty_json.get("pluginDirectories").is_none());
5648 assert!(empty_json.get("largeOutput").is_none());
5649 }
5650
5651 #[test]
5652 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
5653 use std::path::PathBuf;
5654
5655 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5656 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5657 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5658 cfg.github_token = Some("ghs_secret".to_string());
5659 cfg.include_sub_agent_streaming_events = Some(true);
5660 cfg.enable_session_telemetry = Some(false);
5661 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
5662 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
5663 cfg.enable_on_demand_instruction_discovery = Some(false);
5664
5665 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5666 let wire_json = serde_json::to_value(&wire).unwrap();
5667 assert_eq!(wire_json["sessionId"], "sess-1");
5668 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5669 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5670 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5671 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
5672 assert_eq!(wire_json["enableSessionTelemetry"], false);
5673 assert_eq!(wire_json["reasoningSummary"], "detailed");
5674 assert_eq!(wire_json["remoteSession"], "on");
5675 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5676
5677 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5679 .into_wire()
5680 .expect("default resume has no duplicate handlers");
5681 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5682 assert!(empty_json.get("reasoningSummary").is_none());
5683 assert!(empty_json.get("remoteSession").is_none());
5684 assert!(
5685 empty_json
5686 .get("enableOnDemandInstructionDiscovery")
5687 .is_none()
5688 );
5689 }
5690
5691 #[test]
5692 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
5693 use std::path::PathBuf;
5694
5695 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5696 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
5697 cfg.large_output = Some(
5698 LargeToolOutputConfig::new()
5699 .with_enabled(false)
5700 .with_max_size_bytes(2048)
5701 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
5702 );
5703
5704 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5705 let wire_json = serde_json::to_value(&wire).unwrap();
5706 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
5707 assert_eq!(wire_json["largeOutput"]["enabled"], false);
5708 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
5709 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
5710
5711 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5712 .into_wire()
5713 .expect("default resume has no duplicate handlers");
5714 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5715 assert!(empty_json.get("pluginDirectories").is_none());
5716 assert!(empty_json.get("largeOutput").is_none());
5717 }
5718
5719 #[test]
5720 fn session_config_builder_composes() {
5721 use std::collections::HashMap;
5722
5723 let cfg = SessionConfig::default()
5724 .with_session_id(SessionId::from("sess-1"))
5725 .with_model("claude-sonnet-4")
5726 .with_client_name("test-app")
5727 .with_reasoning_effort("medium")
5728 .with_reasoning_summary(ReasoningSummary::Concise)
5729 .with_context_tier("long_context")
5730 .with_streaming(true)
5731 .with_tools([Tool::new("greet")])
5732 .with_available_tools(["bash", "view"])
5733 .with_excluded_tools(["dangerous"])
5734 .with_mcp_servers(HashMap::new())
5735 .with_mcp_oauth_token_storage("persistent")
5736 .with_enable_config_discovery(true)
5737 .with_enable_on_demand_instruction_discovery(true)
5738 .with_skill_directories([PathBuf::from("/tmp/skills")])
5739 .with_disabled_skills(["broken-skill"])
5740 .with_agent("researcher")
5741 .with_config_directory(PathBuf::from("/tmp/config"))
5742 .with_working_directory(PathBuf::from("/tmp/work"))
5743 .with_github_token("ghp_test")
5744 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5745 .with_enable_session_telemetry(false)
5746 .with_include_sub_agent_streaming_events(false)
5747 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5748
5749 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
5750 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
5751 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5752 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
5753 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
5754 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
5755 assert_eq!(cfg.streaming, Some(true));
5756 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5757 assert_eq!(
5758 cfg.available_tools.as_deref(),
5759 Some(&["bash".to_string(), "view".to_string()][..])
5760 );
5761 assert_eq!(
5762 cfg.excluded_tools.as_deref(),
5763 Some(&["dangerous".to_string()][..])
5764 );
5765 assert!(cfg.mcp_servers.is_some());
5766 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5767 assert_eq!(cfg.enable_config_discovery, Some(true));
5768 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
5769 assert_eq!(
5770 cfg.skill_directories.as_deref(),
5771 Some(&[PathBuf::from("/tmp/skills")][..])
5772 );
5773 assert_eq!(
5774 cfg.disabled_skills.as_deref(),
5775 Some(&["broken-skill".to_string()][..])
5776 );
5777 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5778 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5779 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5780 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5781 assert_eq!(
5782 cfg.capi,
5783 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5784 );
5785 assert_eq!(cfg.enable_session_telemetry, Some(false));
5786 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
5787 assert_eq!(
5788 cfg.extension_info,
5789 Some(ExtensionInfo::new("github-app", "counter"))
5790 );
5791 }
5792
5793 #[test]
5794 fn resume_session_config_builder_composes() {
5795 use std::collections::HashMap;
5796
5797 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
5798 .with_client_name("test-app")
5799 .with_reasoning_summary(ReasoningSummary::None)
5800 .with_context_tier("default")
5801 .with_streaming(true)
5802 .with_tools([Tool::new("greet")])
5803 .with_available_tools(["bash", "view"])
5804 .with_excluded_tools(["dangerous"])
5805 .with_mcp_servers(HashMap::new())
5806 .with_mcp_oauth_token_storage("persistent")
5807 .with_enable_config_discovery(true)
5808 .with_enable_on_demand_instruction_discovery(false)
5809 .with_skill_directories([PathBuf::from("/tmp/skills")])
5810 .with_disabled_skills(["broken-skill"])
5811 .with_agent("researcher")
5812 .with_config_directory(PathBuf::from("/tmp/config"))
5813 .with_working_directory(PathBuf::from("/tmp/work"))
5814 .with_github_token("ghp_test")
5815 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5816 .with_enable_session_telemetry(false)
5817 .with_include_sub_agent_streaming_events(true)
5818 .with_suppress_resume_event(true)
5819 .with_continue_pending_work(true)
5820 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5821
5822 assert_eq!(cfg.session_id.as_str(), "sess-2");
5823 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5824 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
5825 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
5826 assert_eq!(cfg.streaming, Some(true));
5827 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5828 assert_eq!(
5829 cfg.available_tools.as_deref(),
5830 Some(&["bash".to_string(), "view".to_string()][..])
5831 );
5832 assert_eq!(
5833 cfg.excluded_tools.as_deref(),
5834 Some(&["dangerous".to_string()][..])
5835 );
5836 assert!(cfg.mcp_servers.is_some());
5837 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5838 assert_eq!(cfg.enable_config_discovery, Some(true));
5839 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
5840 assert_eq!(
5841 cfg.skill_directories.as_deref(),
5842 Some(&[PathBuf::from("/tmp/skills")][..])
5843 );
5844 assert_eq!(
5845 cfg.disabled_skills.as_deref(),
5846 Some(&["broken-skill".to_string()][..])
5847 );
5848 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5849 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5850 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5851 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5852 assert_eq!(
5853 cfg.capi,
5854 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5855 );
5856 assert_eq!(cfg.enable_session_telemetry, Some(false));
5857 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
5858 assert_eq!(cfg.suppress_resume_event, Some(true));
5859 assert_eq!(cfg.continue_pending_work, Some(true));
5860 assert_eq!(
5861 cfg.extension_info,
5862 Some(ExtensionInfo::new("github-app", "counter"))
5863 );
5864 }
5865
5866 #[test]
5870 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
5871 let cfg =
5872 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
5873 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5874 let json = serde_json::to_value(&wire).unwrap();
5875 assert_eq!(json["continuePendingWork"], true);
5876
5877 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5879 .into_wire()
5880 .expect("no duplicate handlers");
5881 let json = serde_json::to_value(&wire).unwrap();
5882 assert!(json.get("continuePendingWork").is_none());
5883 }
5884
5885 #[test]
5889 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
5890 let cfg =
5891 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
5892 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5893 let json = serde_json::to_value(&wire).unwrap();
5894 assert_eq!(json["disableResume"], true);
5895 assert!(json.get("suppressResumeEvent").is_none());
5896 }
5897
5898 #[test]
5901 fn session_config_serializes_instruction_directories_to_camel_case() {
5902 let cfg =
5903 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
5904 let (wire, _) = cfg
5905 .into_wire(Some(SessionId::from("instr-on")))
5906 .expect("no duplicate handlers");
5907 let json = serde_json::to_value(&wire).unwrap();
5908 assert_eq!(
5909 json["instructionDirectories"],
5910 serde_json::json!(["/tmp/instr"])
5911 );
5912
5913 let (wire, _) = SessionConfig::default()
5915 .into_wire(Some(SessionId::from("instr-off")))
5916 .expect("no duplicate handlers");
5917 let json = serde_json::to_value(&wire).unwrap();
5918 assert!(json.get("instructionDirectories").is_none());
5919 }
5920
5921 #[test]
5924 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
5925 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
5926 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
5927 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5928 let json = serde_json::to_value(&wire).unwrap();
5929 assert_eq!(
5930 json["instructionDirectories"],
5931 serde_json::json!(["/tmp/instr"])
5932 );
5933
5934 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5935 .into_wire()
5936 .expect("no duplicate handlers");
5937 let json = serde_json::to_value(&wire).unwrap();
5938 assert!(json.get("instructionDirectories").is_none());
5939 }
5940
5941 #[test]
5942 fn custom_agent_config_builder_composes() {
5943 use std::collections::HashMap;
5944
5945 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
5946 .with_display_name("Research Assistant")
5947 .with_description("Investigates technical questions.")
5948 .with_tools(["bash", "view"])
5949 .with_mcp_servers(HashMap::new())
5950 .with_infer(true)
5951 .with_skills(["rust-coding-skill"]);
5952
5953 assert_eq!(cfg.name, "researcher");
5954 assert_eq!(cfg.prompt, "You are a research assistant.");
5955 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
5956 assert_eq!(
5957 cfg.description.as_deref(),
5958 Some("Investigates technical questions.")
5959 );
5960 assert_eq!(
5961 cfg.tools.as_deref(),
5962 Some(&["bash".to_string(), "view".to_string()][..])
5963 );
5964 assert!(cfg.mcp_servers.is_some());
5965 assert_eq!(cfg.infer, Some(true));
5966 assert_eq!(
5967 cfg.skills.as_deref(),
5968 Some(&["rust-coding-skill".to_string()][..])
5969 );
5970 }
5971
5972 #[test]
5973 fn infinite_session_config_builder_composes() {
5974 let cfg = InfiniteSessionConfig::new()
5975 .with_enabled(true)
5976 .with_background_compaction_threshold(0.75)
5977 .with_buffer_exhaustion_threshold(0.92);
5978
5979 assert_eq!(cfg.enabled, Some(true));
5980 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
5981 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
5982 }
5983
5984 #[test]
5985 fn provider_config_builder_composes() {
5986 use std::collections::HashMap;
5987
5988 let mut headers = HashMap::new();
5989 headers.insert("X-Custom".to_string(), "value".to_string());
5990
5991 let cfg = ProviderConfig::new("https://api.example.com")
5992 .with_provider_type("openai")
5993 .with_wire_api("completions")
5994 .with_transport("websockets")
5995 .with_api_key("sk-test")
5996 .with_bearer_token("bearer-test")
5997 .with_headers(headers)
5998 .with_model_id("gpt-4")
5999 .with_wire_model("azure-gpt-4-deployment")
6000 .with_max_prompt_tokens(8192)
6001 .with_max_output_tokens(2048);
6002
6003 assert_eq!(cfg.base_url, "https://api.example.com");
6004 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
6005 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
6006 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
6007 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
6008 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
6009 assert_eq!(
6010 cfg.headers
6011 .as_ref()
6012 .and_then(|h| h.get("X-Custom"))
6013 .map(String::as_str),
6014 Some("value"),
6015 );
6016 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
6017 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
6018 assert_eq!(cfg.max_prompt_tokens, Some(8192));
6019 assert_eq!(cfg.max_output_tokens, Some(2048));
6020
6021 let wire = serde_json::to_value(&cfg).unwrap();
6023 assert_eq!(wire["modelId"], "gpt-4");
6024 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
6025 assert_eq!(wire["maxPromptTokens"], 8192);
6026 assert_eq!(wire["maxOutputTokens"], 2048);
6027
6028 let unset = ProviderConfig::new("https://api.example.com");
6029 let wire_unset = serde_json::to_value(&unset).unwrap();
6030 assert!(wire_unset.get("modelId").is_none());
6031 assert!(wire_unset.get("wireModel").is_none());
6032 assert!(wire_unset.get("maxPromptTokens").is_none());
6033 assert!(wire_unset.get("maxOutputTokens").is_none());
6034 }
6035
6036 #[test]
6037 fn capi_session_options_builder_composes_and_serializes() {
6038 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
6039
6040 assert_eq!(cfg.enable_web_socket_responses, Some(false));
6041
6042 let wire = serde_json::to_value(&cfg).unwrap();
6043 assert_eq!(
6044 wire,
6045 serde_json::json!({ "enableWebSocketResponses": false })
6046 );
6047
6048 let unset = CapiSessionOptions::new();
6049 let wire_unset = serde_json::to_value(&unset).unwrap();
6050 assert!(wire_unset.get("enableWebSocketResponses").is_none());
6051 }
6052
6053 #[test]
6054 fn session_config_with_capi_serializes() {
6055 let (wire, _) = SessionConfig::default()
6056 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6057 .into_wire(Some(SessionId::from("capi-create")))
6058 .expect("no duplicate handlers");
6059 let json = serde_json::to_value(&wire).unwrap();
6060 assert_eq!(
6061 json["capi"],
6062 serde_json::json!({ "enableWebSocketResponses": false })
6063 );
6064
6065 let (empty_wire, _) = SessionConfig::default()
6066 .into_wire(Some(SessionId::from("capi-create-unset")))
6067 .expect("no duplicate handlers");
6068 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6069 assert!(empty_json.get("capi").is_none());
6070 }
6071
6072 #[test]
6073 fn resume_session_config_with_capi_serializes() {
6074 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
6075 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6076 .into_wire()
6077 .expect("no duplicate handlers");
6078 let json = serde_json::to_value(&wire).unwrap();
6079 assert_eq!(
6080 json["capi"],
6081 serde_json::json!({ "enableWebSocketResponses": false })
6082 );
6083
6084 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
6085 .into_wire()
6086 .expect("no duplicate handlers");
6087 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6088 assert!(empty_json.get("capi").is_none());
6089 }
6090
6091 #[test]
6092 fn system_message_config_builder_composes() {
6093 use std::collections::HashMap;
6094
6095 let cfg = SystemMessageConfig::new()
6096 .with_mode("replace")
6097 .with_content("Custom system message.")
6098 .with_sections(HashMap::new());
6099
6100 assert_eq!(cfg.mode.as_deref(), Some("replace"));
6101 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
6102 assert!(cfg.sections.is_some());
6103 }
6104
6105 #[test]
6106 fn delivery_mode_serializes_to_kebab_case_strings() {
6107 assert_eq!(
6108 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
6109 "\"enqueue\""
6110 );
6111 assert_eq!(
6112 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
6113 "\"immediate\""
6114 );
6115 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
6116 assert_eq!(parsed, DeliveryMode::Immediate);
6117 }
6118
6119 #[test]
6120 fn agent_mode_serializes_to_kebab_case_strings() {
6121 assert_eq!(
6122 serde_json::to_string(&AgentMode::Interactive).unwrap(),
6123 "\"interactive\""
6124 );
6125 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
6126 assert_eq!(
6127 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
6128 "\"autopilot\""
6129 );
6130 assert_eq!(
6131 serde_json::to_string(&AgentMode::Shell).unwrap(),
6132 "\"shell\""
6133 );
6134 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
6135 assert_eq!(parsed, AgentMode::Plan);
6136 }
6137
6138 #[test]
6139 fn connection_state_distinguishes_variants() {
6140 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
6143 }
6144
6145 #[test]
6151 fn session_event_round_trips_agent_id_on_envelope() {
6152 let wire = json!({
6153 "id": "evt-1",
6154 "timestamp": "2026-04-30T12:00:00Z",
6155 "parentId": null,
6156 "agentId": "sub-agent-42",
6157 "type": "assistant.message",
6158 "data": { "message": "hi" }
6159 });
6160
6161 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
6162 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6163
6164 let roundtripped = serde_json::to_value(&event).unwrap();
6166 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6167
6168 let main_agent_event: SessionEvent = serde_json::from_value(json!({
6170 "id": "evt-2",
6171 "timestamp": "2026-04-30T12:00:01Z",
6172 "parentId": null,
6173 "type": "session.idle",
6174 "data": {}
6175 }))
6176 .unwrap();
6177 assert!(main_agent_event.agent_id.is_none());
6178 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
6179 assert!(roundtripped.get("agentId").is_none());
6180 }
6181
6182 #[test]
6184 fn typed_session_event_round_trips_agent_id_on_envelope() {
6185 let wire = json!({
6186 "id": "evt-1",
6187 "timestamp": "2026-04-30T12:00:00Z",
6188 "parentId": null,
6189 "agentId": "sub-agent-42",
6190 "type": "session.idle",
6191 "data": {}
6192 });
6193
6194 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
6195 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6196
6197 let roundtripped = serde_json::to_value(&event).unwrap();
6198 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6199 }
6200
6201 #[test]
6202 fn connection_state_variants_compile() {
6203 let _ = ConnectionState::Disconnected;
6207 let _ = ConnectionState::Connecting;
6208 let _ = ConnectionState::Connected;
6209 let _ = ConnectionState::Error;
6210 }
6211
6212 #[test]
6213 fn deserializes_runtime_attachment_variants() {
6214 let attachments: Vec<Attachment> = serde_json::from_value(json!([
6215 {
6216 "type": "file",
6217 "path": "/tmp/file.rs",
6218 "displayName": "file.rs",
6219 "lineRange": { "start": 7, "end": 12 }
6220 },
6221 {
6222 "type": "directory",
6223 "path": "/tmp/project",
6224 "displayName": "project"
6225 },
6226 {
6227 "type": "selection",
6228 "filePath": "/tmp/lib.rs",
6229 "displayName": "lib.rs",
6230 "text": "fn main() {}",
6231 "selection": {
6232 "start": { "line": 1, "character": 2 },
6233 "end": { "line": 3, "character": 4 }
6234 }
6235 },
6236 {
6237 "type": "blob",
6238 "data": "Zm9v",
6239 "mimeType": "image/png",
6240 "displayName": "image.png"
6241 },
6242 {
6243 "type": "github_reference",
6244 "number": 42,
6245 "title": "Fix rendering",
6246 "referenceType": "issue",
6247 "state": "open",
6248 "url": "https://github.com/example/repo/issues/42"
6249 }
6250 ]))
6251 .expect("attachments should deserialize");
6252
6253 assert_eq!(attachments.len(), 5);
6254 assert!(matches!(
6255 &attachments[0],
6256 Attachment::File {
6257 path,
6258 display_name,
6259 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
6260 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
6261 ));
6262 assert!(matches!(
6263 &attachments[1],
6264 Attachment::Directory { path, display_name }
6265 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
6266 ));
6267 assert!(matches!(
6268 &attachments[2],
6269 Attachment::Selection {
6270 file_path,
6271 display_name,
6272 selection:
6273 AttachmentSelectionRange {
6274 start: AttachmentSelectionPosition { line: 1, character: 2 },
6275 end: AttachmentSelectionPosition { line: 3, character: 4 },
6276 },
6277 ..
6278 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
6279 ));
6280 assert!(matches!(
6281 &attachments[3],
6282 Attachment::Blob {
6283 data,
6284 mime_type,
6285 display_name,
6286 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
6287 ));
6288 assert!(matches!(
6289 &attachments[4],
6290 Attachment::GitHubReference {
6291 number: 42,
6292 title,
6293 reference_type: GitHubReferenceType::Issue,
6294 state,
6295 url,
6296 } if title == "Fix rendering"
6297 && state == "open"
6298 && url == "https://github.com/example/repo/issues/42"
6299 ));
6300 }
6301
6302 #[test]
6303 fn ensures_display_names_for_variants_that_support_them() {
6304 let mut attachments = vec![
6305 Attachment::File {
6306 path: PathBuf::from("/tmp/file.rs"),
6307 display_name: None,
6308 line_range: None,
6309 },
6310 Attachment::Selection {
6311 file_path: PathBuf::from("/tmp/src/lib.rs"),
6312 display_name: None,
6313 text: "fn main() {}".to_string(),
6314 selection: AttachmentSelectionRange {
6315 start: AttachmentSelectionPosition {
6316 line: 0,
6317 character: 0,
6318 },
6319 end: AttachmentSelectionPosition {
6320 line: 0,
6321 character: 10,
6322 },
6323 },
6324 },
6325 Attachment::Blob {
6326 data: "Zm9v".to_string(),
6327 mime_type: "image/png".to_string(),
6328 display_name: None,
6329 },
6330 Attachment::GitHubReference {
6331 number: 7,
6332 title: "Track regressions".to_string(),
6333 reference_type: GitHubReferenceType::Issue,
6334 state: "open".to_string(),
6335 url: "https://example.com/issues/7".to_string(),
6336 },
6337 ];
6338
6339 ensure_attachment_display_names(&mut attachments);
6340
6341 assert_eq!(attachments[0].display_name(), Some("file.rs"));
6342 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
6343 assert_eq!(attachments[2].display_name(), Some("attachment"));
6344 assert_eq!(attachments[3].display_name(), None);
6345 assert_eq!(
6346 attachments[3].label(),
6347 Some("Track regressions".to_string())
6348 );
6349 }
6350
6351 #[test]
6352 fn github_anchored_attachment_variants_round_trip() {
6353 let cases = vec![
6354 (
6355 "github_commit",
6356 json!({
6357 "type": "github_commit",
6358 "message": "Fix the thing",
6359 "oid": "abc123",
6360 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
6361 "url": "https://github.com/octocat/repo/commit/abc123"
6362 }),
6363 ),
6364 (
6365 "github_release",
6366 json!({
6367 "type": "github_release",
6368 "name": "v1.2.3",
6369 "repo": { "name": "repo", "owner": "octocat" },
6370 "tagName": "v1.2.3",
6371 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
6372 }),
6373 ),
6374 (
6375 "github_actions_job",
6376 json!({
6377 "type": "github_actions_job",
6378 "conclusion": "failure",
6379 "jobId": 99,
6380 "jobName": "build",
6381 "repo": { "name": "repo", "owner": "octocat" },
6382 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
6383 "workflowName": "CI"
6384 }),
6385 ),
6386 (
6387 "github_repository",
6388 json!({
6389 "type": "github_repository",
6390 "description": "An example repository",
6391 "ref": "main",
6392 "repo": { "name": "repo", "owner": "octocat" },
6393 "url": "https://github.com/octocat/repo"
6394 }),
6395 ),
6396 (
6397 "github_file_diff",
6398 json!({
6399 "type": "github_file_diff",
6400 "base": {
6401 "path": "src/lib.rs",
6402 "ref": "main",
6403 "repo": { "name": "repo", "owner": "octocat" }
6404 },
6405 "head": {
6406 "path": "src/lib.rs",
6407 "ref": "feature",
6408 "repo": { "name": "repo", "owner": "octocat" }
6409 },
6410 "url": "https://github.com/octocat/repo/compare/main...feature"
6411 }),
6412 ),
6413 (
6414 "github_tree_comparison",
6415 json!({
6416 "type": "github_tree_comparison",
6417 "base": {
6418 "repo": { "name": "repo", "owner": "octocat" },
6419 "revision": "main"
6420 },
6421 "head": {
6422 "repo": { "name": "repo", "owner": "octocat" },
6423 "revision": "feature"
6424 },
6425 "url": "https://github.com/octocat/repo/compare/main...feature"
6426 }),
6427 ),
6428 (
6429 "github_url",
6430 json!({
6431 "type": "github_url",
6432 "url": "https://github.com/octocat/repo/wiki"
6433 }),
6434 ),
6435 (
6436 "github_file",
6437 json!({
6438 "type": "github_file",
6439 "path": "src/main.rs",
6440 "ref": "main",
6441 "repo": { "name": "repo", "owner": "octocat" },
6442 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
6443 }),
6444 ),
6445 (
6446 "github_snippet",
6447 json!({
6448 "type": "github_snippet",
6449 "lineRange": { "start": 10, "end": 20 },
6450 "path": "src/main.rs",
6451 "ref": "main",
6452 "repo": { "name": "repo", "owner": "octocat" },
6453 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
6454 }),
6455 ),
6456 ];
6457
6458 for (expected_type, input) in cases {
6459 let attachment: Attachment = serde_json::from_value(input.clone())
6460 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
6461
6462 let serialized_string = serde_json::to_string(&attachment)
6467 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
6468
6469 assert_eq!(
6471 serialized_string.matches("\"type\":").count(),
6472 1,
6473 "{expected_type} must serialize a single `type` key"
6474 );
6475
6476 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
6477 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
6478 assert_eq!(
6479 serialized.get("type").and_then(|value| value.as_str()),
6480 Some(expected_type),
6481 "{expected_type} must serialize the correct discriminator"
6482 );
6483
6484 assert_eq!(
6486 serialized, input,
6487 "{expected_type} should round-trip without data loss"
6488 );
6489 let reparsed: Attachment = serde_json::from_value(serialized)
6490 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
6491 assert_eq!(
6492 reparsed, attachment,
6493 "{expected_type} should re-deserialize to the same value"
6494 );
6495 }
6496 }
6497}
6498
6499#[cfg(test)]
6500mod permission_builder_tests {
6501 use std::sync::Arc;
6502
6503 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
6504 use crate::permission;
6505 use crate::types::{
6506 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
6507 SessionId,
6508 };
6509
6510 fn data() -> PermissionRequestData {
6511 PermissionRequestData {
6512 extra: serde_json::json!({"tool": "shell"}),
6513 ..Default::default()
6514 }
6515 }
6516
6517 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6520 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6521 }
6522
6523 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6524 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6525 }
6526
6527 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
6528 handler
6529 .handle(SessionId::from("s1"), RequestId::new("1"), data())
6530 .await
6531 }
6532
6533 #[tokio::test]
6534 async fn approve_all_with_handler_present_approves() {
6535 let cfg = SessionConfig::default()
6536 .with_permission_handler(Arc::new(ApproveAllHandler))
6537 .approve_all_permissions();
6538 let h = resolve_create(cfg).expect("policy + handler yields handler");
6539 assert!(matches!(
6540 dispatch(&h).await,
6541 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6542 ));
6543 }
6544
6545 #[tokio::test]
6546 async fn approve_all_standalone_produces_handler() {
6547 let cfg = SessionConfig::default().approve_all_permissions();
6548 let h = resolve_create(cfg).expect("policy alone yields handler");
6549 assert!(matches!(
6550 dispatch(&h).await,
6551 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6552 ));
6553 }
6554
6555 #[tokio::test]
6558 async fn approve_all_is_order_independent() {
6559 let a = SessionConfig::default()
6560 .with_permission_handler(Arc::new(ApproveAllHandler))
6561 .approve_all_permissions();
6562 let b = SessionConfig::default()
6563 .approve_all_permissions()
6564 .with_permission_handler(Arc::new(ApproveAllHandler));
6565 let ha = resolve_create(a).unwrap();
6566 let hb = resolve_create(b).unwrap();
6567 assert!(matches!(
6568 dispatch(&ha).await,
6569 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6570 ));
6571 assert!(matches!(
6572 dispatch(&hb).await,
6573 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6574 ));
6575 }
6576
6577 #[tokio::test]
6578 async fn deny_all_is_order_independent() {
6579 let a = SessionConfig::default()
6580 .with_permission_handler(Arc::new(ApproveAllHandler))
6581 .deny_all_permissions();
6582 let b = SessionConfig::default()
6583 .deny_all_permissions()
6584 .with_permission_handler(Arc::new(ApproveAllHandler));
6585 let ha = resolve_create(a).unwrap();
6586 let hb = resolve_create(b).unwrap();
6587 assert!(matches!(
6588 dispatch(&ha).await,
6589 PermissionResult::Decision(PermissionDecision::Reject(_))
6590 ));
6591 assert!(matches!(
6592 dispatch(&hb).await,
6593 PermissionResult::Decision(PermissionDecision::Reject(_))
6594 ));
6595 }
6596
6597 #[tokio::test]
6598 async fn approve_permissions_if_consults_predicate() {
6599 let cfg = SessionConfig::default().approve_permissions_if(|d| {
6600 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6601 });
6602 let h = resolve_create(cfg).unwrap();
6603 assert!(matches!(
6604 dispatch(&h).await,
6605 PermissionResult::Decision(PermissionDecision::Reject(_))
6606 ));
6607 }
6608
6609 #[tokio::test]
6610 async fn approve_permissions_if_is_order_independent() {
6611 let predicate = |d: &PermissionRequestData| {
6612 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6613 };
6614 let a = SessionConfig::default()
6615 .with_permission_handler(Arc::new(ApproveAllHandler))
6616 .approve_permissions_if(predicate);
6617 let b = SessionConfig::default()
6618 .approve_permissions_if(predicate)
6619 .with_permission_handler(Arc::new(ApproveAllHandler));
6620 let ha = resolve_create(a).unwrap();
6621 let hb = resolve_create(b).unwrap();
6622 assert!(matches!(
6623 dispatch(&ha).await,
6624 PermissionResult::Decision(PermissionDecision::Reject(_))
6625 ));
6626 assert!(matches!(
6627 dispatch(&hb).await,
6628 PermissionResult::Decision(PermissionDecision::Reject(_))
6629 ));
6630 }
6631
6632 #[tokio::test]
6633 async fn resume_session_config_approve_all_works() {
6634 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
6635 .with_permission_handler(Arc::new(ApproveAllHandler))
6636 .approve_all_permissions();
6637 let h = resolve_resume(cfg).unwrap();
6638 assert!(matches!(
6639 dispatch(&h).await,
6640 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6641 ));
6642 }
6643
6644 #[tokio::test]
6645 async fn resume_session_config_approve_all_is_order_independent() {
6646 let a = ResumeSessionConfig::new(SessionId::from("s1"))
6647 .with_permission_handler(Arc::new(ApproveAllHandler))
6648 .approve_all_permissions();
6649 let b = ResumeSessionConfig::new(SessionId::from("s1"))
6650 .approve_all_permissions()
6651 .with_permission_handler(Arc::new(ApproveAllHandler));
6652 let ha = resolve_resume(a).unwrap();
6653 let hb = resolve_resume(b).unwrap();
6654 assert!(matches!(
6655 dispatch(&ha).await,
6656 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6657 ));
6658 assert!(matches!(
6659 dispatch(&hb).await,
6660 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6661 ));
6662 }
6663}