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 enable_github_telemetry_forwarding: None,
2167 commands: wire_commands,
2168 exp_assignments: self.exp_assignments,
2169 };
2170
2171 let runtime = SessionConfigRuntime {
2172 permission_handler: self.permission_handler,
2173 permission_policy: self.permission_policy,
2174 elicitation_handler: self.elicitation_handler,
2175 mcp_auth_handler: self.mcp_auth_handler,
2176 user_input_handler: self.user_input_handler,
2177 exit_plan_mode_handler: self.exit_plan_mode_handler,
2178 auto_mode_switch_handler: self.auto_mode_switch_handler,
2179 hooks_handler: self.hooks_handler,
2180 system_message_transform: self.system_message_transform,
2181 tool_handlers,
2182 canvas_handler,
2183 session_fs_provider: self.session_fs_provider,
2184 bearer_token_providers,
2185 commands: self.commands,
2186 };
2187
2188 Ok((wire, runtime))
2189 }
2190
2191 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2195 self.permission_handler = Some(handler);
2196 self
2197 }
2198
2199 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2202 self.elicitation_handler = Some(handler);
2203 self
2204 }
2205
2206 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2208 self.mcp_auth_handler = Some(handler);
2209 self
2210 }
2211
2212 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2215 self.user_input_handler = Some(handler);
2216 self
2217 }
2218
2219 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2221 self.exit_plan_mode_handler = Some(handler);
2222 self
2223 }
2224
2225 pub fn with_auto_mode_switch_handler(
2227 mut self,
2228 handler: Arc<dyn AutoModeSwitchHandler>,
2229 ) -> Self {
2230 self.auto_mode_switch_handler = Some(handler);
2231 self
2232 }
2233
2234 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2239 self.commands = Some(commands);
2240 self
2241 }
2242
2243 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2247 self.session_fs_provider = Some(provider);
2248 self
2249 }
2250
2251 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2254 self.hooks_handler = Some(hooks);
2255 self
2256 }
2257
2258 pub fn with_system_message_transform(
2262 mut self,
2263 transform: Arc<dyn SystemMessageTransform>,
2264 ) -> Self {
2265 self.system_message_transform = Some(transform);
2266 self
2267 }
2268
2269 pub fn approve_all_permissions(mut self) -> Self {
2275 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2276 self
2277 }
2278
2279 pub fn deny_all_permissions(mut self) -> Self {
2282 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2283 self
2284 }
2285
2286 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2291 where
2292 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2293 {
2294 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2295 self
2296 }
2297
2298 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2300 self.session_id = Some(id.into());
2301 self
2302 }
2303
2304 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2306 self.model = Some(model.into());
2307 self
2308 }
2309
2310 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2312 self.client_name = Some(name.into());
2313 self
2314 }
2315
2316 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2318 self.reasoning_effort = Some(effort.into());
2319 self
2320 }
2321
2322 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2324 self.reasoning_summary = Some(summary);
2325 self
2326 }
2327
2328 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2330 self.context_tier = Some(tier.into());
2331 self
2332 }
2333
2334 pub fn with_streaming(mut self, streaming: bool) -> Self {
2336 self.streaming = Some(streaming);
2337 self
2338 }
2339
2340 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2342 self.system_message = Some(system_message);
2343 self
2344 }
2345
2346 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2348 self.tools = Some(tools.into_iter().collect());
2349 self
2350 }
2351
2352 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2357 self.canvases = Some(canvases.into_iter().collect());
2358 self
2359 }
2360
2361 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2363 self.canvas_handler = Some(handler);
2364 self
2365 }
2366
2367 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2369 self.request_canvas_renderer = Some(request);
2370 self
2371 }
2372
2373 pub fn with_request_extensions(mut self, request: bool) -> Self {
2375 self.request_extensions = Some(request);
2376 self
2377 }
2378
2379 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2383 self.extension_sdk_path = Some(path.into());
2384 self
2385 }
2386
2387 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2389 self.extension_info = Some(extension_info);
2390 self
2391 }
2392
2393 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2395 where
2396 I: IntoIterator<Item = S>,
2397 S: Into<String>,
2398 {
2399 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2400 self
2401 }
2402
2403 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2405 where
2406 I: IntoIterator<Item = S>,
2407 S: Into<String>,
2408 {
2409 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2410 self
2411 }
2412
2413 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
2415 where
2416 I: IntoIterator<Item = S>,
2417 S: Into<String>,
2418 {
2419 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
2420 self
2421 }
2422
2423 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
2425 self.mcp_servers = Some(servers);
2426 self
2427 }
2428
2429 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2437 self.mcp_oauth_token_storage = Some(mode.into());
2438 self
2439 }
2440
2441 pub fn with_embedding_cache_storage(
2443 mut self,
2444 embedding_cache_storage: impl Into<String>,
2445 ) -> Self {
2446 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2447 self
2448 }
2449
2450 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2452 self.enable_config_discovery = Some(enable);
2453 self
2454 }
2455
2456 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2458 self.skip_embedding_retrieval = Some(value);
2459 self
2460 }
2461
2462 pub fn with_organization_custom_instructions(
2464 mut self,
2465 instructions: impl Into<String>,
2466 ) -> Self {
2467 self.organization_custom_instructions = Some(instructions.into());
2468 self
2469 }
2470
2471 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2473 self.enable_on_demand_instruction_discovery = Some(value);
2474 self
2475 }
2476
2477 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2479 self.enable_file_hooks = Some(value);
2480 self
2481 }
2482
2483 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2485 self.enable_host_git_operations = Some(value);
2486 self
2487 }
2488
2489 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2491 self.enable_session_store = Some(value);
2492 self
2493 }
2494
2495 pub fn with_enable_skills(mut self, value: bool) -> Self {
2497 self.enable_skills = Some(value);
2498 self
2499 }
2500
2501 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2507 self.enable_mcp_apps = Some(enable);
2508 self
2509 }
2510
2511 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2513 where
2514 I: IntoIterator<Item = P>,
2515 P: Into<PathBuf>,
2516 {
2517 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2518 self
2519 }
2520
2521 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2525 where
2526 I: IntoIterator<Item = P>,
2527 P: Into<PathBuf>,
2528 {
2529 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2530 self
2531 }
2532
2533 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2535 where
2536 I: IntoIterator<Item = P>,
2537 P: Into<PathBuf>,
2538 {
2539 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2540 self
2541 }
2542
2543 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2545 self.large_output = Some(config);
2546 self
2547 }
2548
2549 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2551 where
2552 I: IntoIterator<Item = S>,
2553 S: Into<String>,
2554 {
2555 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2556 self
2557 }
2558
2559 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2561 mut self,
2562 agents: I,
2563 ) -> Self {
2564 self.custom_agents = Some(agents.into_iter().collect());
2565 self
2566 }
2567
2568 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2570 self.default_agent = Some(agent);
2571 self
2572 }
2573
2574 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2577 self.agent = Some(name.into());
2578 self
2579 }
2580
2581 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2584 self.infinite_sessions = Some(config);
2585 self
2586 }
2587
2588 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2590 self.provider = Some(provider);
2591 self
2592 }
2593
2594 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
2596 self.capi = Some(capi);
2597 self
2598 }
2599
2600 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
2606 self.providers = Some(providers);
2607 self
2608 }
2609
2610 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
2616 self.models = Some(models);
2617 self
2618 }
2619
2620 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2624 self.enable_session_telemetry = Some(enable);
2625 self
2626 }
2627
2628 pub fn with_enable_citations(mut self, enable: bool) -> Self {
2630 self.enable_citations = Some(enable);
2631 self
2632 }
2633
2634 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
2636 self.session_limits = Some(limits);
2637 self
2638 }
2639
2640 pub fn with_model_capabilities(
2642 mut self,
2643 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2644 ) -> Self {
2645 self.model_capabilities = Some(capabilities);
2646 self
2647 }
2648
2649 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
2651 self.memory = Some(memory);
2652 self
2653 }
2654
2655 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2657 self.config_directory = Some(dir.into());
2658 self
2659 }
2660
2661 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2664 self.working_directory = Some(dir.into());
2665 self
2666 }
2667
2668 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2673 self.github_token = Some(token.into());
2674 self
2675 }
2676
2677 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2680 self.include_sub_agent_streaming_events = Some(include);
2681 self
2682 }
2683
2684 pub fn with_remote_session(
2686 mut self,
2687 mode: crate::generated::api_types::RemoteSessionMode,
2688 ) -> Self {
2689 self.remote_session = Some(mode);
2690 self
2691 }
2692
2693 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
2695 self.cloud = Some(cloud);
2696 self
2697 }
2698
2699 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
2701 self.skip_custom_instructions = Some(value);
2702 self
2703 }
2704
2705 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
2707 self.custom_agents_local_only = Some(value);
2708 self
2709 }
2710
2711 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
2713 self.coauthor_enabled = Some(value);
2714 self
2715 }
2716
2717 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
2719 self.manage_schedule_enabled = Some(value);
2720 self
2721 }
2722
2723 #[doc(hidden)]
2731 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
2732 self.exp_assignments = Some(assignments);
2733 self
2734 }
2735}
2736#[derive(Clone)]
2743#[non_exhaustive]
2744pub struct ResumeSessionConfig {
2745 pub session_id: SessionId,
2747 pub model: Option<String>,
2750 pub client_name: Option<String>,
2752 pub reasoning_effort: Option<String>,
2754 pub reasoning_summary: Option<ReasoningSummary>,
2758 pub context_tier: Option<String>,
2761 pub streaming: Option<bool>,
2763 pub system_message: Option<SystemMessageConfig>,
2766 pub tools: Option<Vec<Tool>>,
2768 pub canvases: Option<Vec<CanvasDeclaration>>,
2770 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2773 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
2775 pub request_canvas_renderer: Option<bool>,
2777 pub request_extensions: Option<bool>,
2779 pub extension_sdk_path: Option<String>,
2783 pub extension_info: Option<ExtensionInfo>,
2785 pub available_tools: Option<Vec<String>>,
2787 pub excluded_tools: Option<Vec<String>>,
2789 pub excluded_builtin_agents: Option<Vec<String>>,
2795 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
2797 pub mcp_oauth_token_storage: Option<String>,
2800 pub enable_config_discovery: Option<bool>,
2802 pub skip_embedding_retrieval: Option<bool>,
2804 pub embedding_cache_storage: Option<String>,
2806 pub organization_custom_instructions: Option<String>,
2808 pub enable_on_demand_instruction_discovery: Option<bool>,
2810 pub enable_file_hooks: Option<bool>,
2812 pub enable_host_git_operations: Option<bool>,
2814 pub enable_session_store: Option<bool>,
2816 pub enable_skills: Option<bool>,
2818 pub enable_mcp_apps: Option<bool>,
2824 pub skill_directories: Option<Vec<PathBuf>>,
2826 pub instruction_directories: Option<Vec<PathBuf>>,
2829 pub plugin_directories: Option<Vec<PathBuf>>,
2831 pub large_output: Option<LargeToolOutputConfig>,
2833 pub disabled_skills: Option<Vec<String>>,
2835 pub hooks: Option<bool>,
2837 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2839 pub default_agent: Option<DefaultAgentConfig>,
2841 pub agent: Option<String>,
2843 pub infinite_sessions: Option<InfiniteSessionConfig>,
2845 pub provider: Option<ProviderConfig>,
2847 pub capi: Option<CapiSessionOptions>,
2853 pub providers: Option<Vec<NamedProviderConfig>>,
2859 pub models: Option<Vec<ProviderModelConfig>>,
2865 pub enable_session_telemetry: Option<bool>,
2873 pub enable_citations: Option<bool>,
2875 pub session_limits: Option<SessionLimitsConfig>,
2877 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2879 pub memory: Option<MemoryConfiguration>,
2881 pub config_directory: Option<PathBuf>,
2883 pub working_directory: Option<PathBuf>,
2885 pub github_token: Option<String>,
2888 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2891 pub include_sub_agent_streaming_events: Option<bool>,
2893 pub commands: Option<Vec<CommandDefinition>>,
2897 #[doc(hidden)]
2902 pub exp_assignments: Option<Value>,
2903 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2908 pub suppress_resume_event: Option<bool>,
2911 pub continue_pending_work: Option<bool>,
2919 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2922 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2925 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2927 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2930 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2933 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2936 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2938 pub(crate) permission_policy: Option<crate::permission::Policy>,
2940 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2942 pub skip_custom_instructions: Option<bool>,
2944 pub custom_agents_local_only: Option<bool>,
2946 pub coauthor_enabled: Option<bool>,
2948 pub manage_schedule_enabled: Option<bool>,
2950}
2951
2952impl std::fmt::Debug for ResumeSessionConfig {
2953 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2954 f.debug_struct("ResumeSessionConfig")
2955 .field("session_id", &self.session_id)
2956 .field("model", &self.model)
2957 .field("client_name", &self.client_name)
2958 .field("reasoning_effort", &self.reasoning_effort)
2959 .field("reasoning_summary", &self.reasoning_summary)
2960 .field("context_tier", &self.context_tier)
2961 .field("streaming", &self.streaming)
2962 .field("system_message", &self.system_message)
2963 .field("tools", &self.tools)
2964 .field("canvases", &self.canvases)
2965 .field(
2966 "canvas_handler",
2967 &self.canvas_handler.as_ref().map(|_| "<set>"),
2968 )
2969 .field("open_canvases", &self.open_canvases)
2970 .field("request_canvas_renderer", &self.request_canvas_renderer)
2971 .field("request_extensions", &self.request_extensions)
2972 .field("extension_sdk_path", &self.extension_sdk_path)
2973 .field("extension_info", &self.extension_info)
2974 .field("available_tools", &self.available_tools)
2975 .field("excluded_tools", &self.excluded_tools)
2976 .field("excluded_builtin_agents", &self.excluded_builtin_agents)
2977 .field("mcp_servers", &self.mcp_servers)
2978 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2979 .field("embedding_cache_storage", &self.embedding_cache_storage)
2980 .field("enable_config_discovery", &self.enable_config_discovery)
2981 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2982 .field(
2983 "organization_custom_instructions",
2984 &self
2985 .organization_custom_instructions
2986 .as_ref()
2987 .map(|_| "<redacted>"),
2988 )
2989 .field(
2990 "enable_on_demand_instruction_discovery",
2991 &self.enable_on_demand_instruction_discovery,
2992 )
2993 .field("enable_file_hooks", &self.enable_file_hooks)
2994 .field(
2995 "enable_host_git_operations",
2996 &self.enable_host_git_operations,
2997 )
2998 .field("enable_session_store", &self.enable_session_store)
2999 .field("enable_skills", &self.enable_skills)
3000 .field("enable_mcp_apps", &self.enable_mcp_apps)
3001 .field("skill_directories", &self.skill_directories)
3002 .field("instruction_directories", &self.instruction_directories)
3003 .field("plugin_directories", &self.plugin_directories)
3004 .field("large_output", &self.large_output)
3005 .field("disabled_skills", &self.disabled_skills)
3006 .field("hooks", &self.hooks)
3007 .field("custom_agents", &self.custom_agents)
3008 .field("default_agent", &self.default_agent)
3009 .field("agent", &self.agent)
3010 .field("infinite_sessions", &self.infinite_sessions)
3011 .field("provider", &self.provider)
3012 .field("capi", &self.capi)
3013 .field("enable_session_telemetry", &self.enable_session_telemetry)
3014 .field("enable_citations", &self.enable_citations)
3015 .field("session_limits", &self.session_limits)
3016 .field("model_capabilities", &self.model_capabilities)
3017 .field("memory", &self.memory)
3018 .field("config_directory", &self.config_directory)
3019 .field("working_directory", &self.working_directory)
3020 .field(
3021 "github_token",
3022 &self.github_token.as_ref().map(|_| "<redacted>"),
3023 )
3024 .field("remote_session", &self.remote_session)
3025 .field(
3026 "include_sub_agent_streaming_events",
3027 &self.include_sub_agent_streaming_events,
3028 )
3029 .field("commands", &self.commands)
3030 .field("exp_assignments", &self.exp_assignments)
3031 .field(
3032 "session_fs_provider",
3033 &self.session_fs_provider.as_ref().map(|_| "<set>"),
3034 )
3035 .field(
3036 "permission_handler",
3037 &self.permission_handler.as_ref().map(|_| "<set>"),
3038 )
3039 .field(
3040 "elicitation_handler",
3041 &self.elicitation_handler.as_ref().map(|_| "<set>"),
3042 )
3043 .field(
3044 "user_input_handler",
3045 &self.user_input_handler.as_ref().map(|_| "<set>"),
3046 )
3047 .field(
3048 "exit_plan_mode_handler",
3049 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
3050 )
3051 .field(
3052 "auto_mode_switch_handler",
3053 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
3054 )
3055 .field(
3056 "hooks_handler",
3057 &self.hooks_handler.as_ref().map(|_| "<set>"),
3058 )
3059 .field(
3060 "system_message_transform",
3061 &self.system_message_transform.as_ref().map(|_| "<set>"),
3062 )
3063 .field("suppress_resume_event", &self.suppress_resume_event)
3064 .field("continue_pending_work", &self.continue_pending_work)
3065 .finish()
3066 }
3067}
3068
3069impl ResumeSessionConfig {
3070 pub(crate) fn into_wire(
3078 mut self,
3079 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3080 let permission_active =
3081 self.permission_handler.is_some() || self.permission_policy.is_some();
3082 let request_user_input = self.user_input_handler.is_some();
3083 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3084 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3085 let request_elicitation = self.elicitation_handler.is_some();
3086 let hooks_flag = self.hooks_handler.is_some();
3087
3088 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3089 if let Some(tools) = self.tools.as_mut() {
3090 for tool in tools.iter_mut() {
3091 if let Some(handler) = tool.handler.take()
3092 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3093 {
3094 return Err(crate::Error::with_message(
3095 crate::ErrorKind::InvalidConfig,
3096 format!("duplicate tool handler registered for name {:?}", tool.name),
3097 ));
3098 }
3099 }
3100 }
3101
3102 let wire_commands = self.commands.as_ref().map(|cmds| {
3103 cmds.iter()
3104 .map(|c| crate::wire::CommandWireDefinition {
3105 name: c.name.clone(),
3106 description: c.description.clone(),
3107 })
3108 .collect()
3109 });
3110 let wire_canvases = self.canvases.clone();
3111 let canvas_handler = self.canvas_handler.clone();
3112 let bearer_token_providers =
3113 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3114
3115 let wire = crate::wire::SessionResumeWire {
3116 session_id: self.session_id,
3117 model: self.model,
3118 client_name: self.client_name,
3119 reasoning_effort: self.reasoning_effort,
3120 reasoning_summary: self.reasoning_summary,
3121 context_tier: self.context_tier,
3122 streaming: self.streaming,
3123 system_message: self.system_message,
3124 tools: self.tools,
3125 canvases: wire_canvases,
3126 open_canvases: self.open_canvases,
3127 request_canvas_renderer: self.request_canvas_renderer,
3128 request_extensions: self.request_extensions,
3129 extension_sdk_path: self.extension_sdk_path,
3130 extension_info: self.extension_info,
3131 available_tools: self.available_tools,
3132 excluded_tools: self.excluded_tools,
3133 excluded_builtin_agents: self.excluded_builtin_agents,
3134 tool_filter_precedence: "excluded",
3135 mcp_servers: self.mcp_servers,
3136 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3137 embedding_cache_storage: self.embedding_cache_storage,
3138 env_value_mode: "direct",
3139 enable_config_discovery: self.enable_config_discovery,
3140 skip_embedding_retrieval: self.skip_embedding_retrieval,
3141 organization_custom_instructions: self.organization_custom_instructions,
3142 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3143 enable_file_hooks: self.enable_file_hooks,
3144 enable_host_git_operations: self.enable_host_git_operations,
3145 enable_session_store: self.enable_session_store,
3146 enable_skills: self.enable_skills,
3147 request_user_input,
3148 request_permission: permission_active,
3149 request_exit_plan_mode,
3150 request_auto_mode_switch,
3151 request_elicitation,
3152 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3153 hooks: hooks_flag,
3154 skill_directories: self.skill_directories,
3155 instruction_directories: self.instruction_directories,
3156 plugin_directories: self.plugin_directories,
3157 large_output: self.large_output,
3158 disabled_skills: self.disabled_skills,
3159 custom_agents: self.custom_agents,
3160 default_agent: self.default_agent,
3161 agent: self.agent,
3162 infinite_sessions: self.infinite_sessions,
3163 provider: self.provider,
3164 capi: self.capi,
3165 providers: self.providers,
3166 models: self.models,
3167 enable_session_telemetry: self.enable_session_telemetry,
3168 enable_citations: self.enable_citations,
3169 session_limits: self.session_limits,
3170 model_capabilities: self.model_capabilities,
3171 memory: self.memory,
3172 config_dir: self.config_directory,
3173 working_directory: self.working_directory,
3174 github_token: self.github_token,
3175 remote_session: self.remote_session,
3176 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3177 enable_github_telemetry_forwarding: None,
3178 commands: wire_commands,
3179 exp_assignments: self.exp_assignments,
3180 suppress_resume_event: self.suppress_resume_event,
3181 continue_pending_work: self.continue_pending_work,
3182 };
3183
3184 let runtime = SessionConfigRuntime {
3185 permission_handler: self.permission_handler,
3186 permission_policy: self.permission_policy,
3187 elicitation_handler: self.elicitation_handler,
3188 mcp_auth_handler: self.mcp_auth_handler,
3189 user_input_handler: self.user_input_handler,
3190 exit_plan_mode_handler: self.exit_plan_mode_handler,
3191 auto_mode_switch_handler: self.auto_mode_switch_handler,
3192 hooks_handler: self.hooks_handler,
3193 system_message_transform: self.system_message_transform,
3194 tool_handlers,
3195 canvas_handler,
3196 session_fs_provider: self.session_fs_provider,
3197 bearer_token_providers,
3198 commands: self.commands,
3199 };
3200
3201 Ok((wire, runtime))
3202 }
3203
3204 pub fn new(session_id: SessionId) -> Self {
3209 Self {
3210 session_id,
3211 model: None,
3212 client_name: None,
3213 reasoning_effort: None,
3214 reasoning_summary: None,
3215 context_tier: None,
3216 streaming: None,
3217 system_message: None,
3218 tools: None,
3219 canvases: None,
3220 canvas_handler: None,
3221 open_canvases: None,
3222 request_canvas_renderer: None,
3223 request_extensions: None,
3224 extension_sdk_path: None,
3225 extension_info: None,
3226 available_tools: None,
3227 excluded_tools: None,
3228 excluded_builtin_agents: None,
3229 mcp_servers: None,
3230 mcp_oauth_token_storage: None,
3231 enable_config_discovery: None,
3232 skip_embedding_retrieval: None,
3233 organization_custom_instructions: None,
3234 enable_on_demand_instruction_discovery: None,
3235 enable_file_hooks: None,
3236 enable_host_git_operations: None,
3237 enable_session_store: None,
3238 enable_skills: None,
3239 embedding_cache_storage: None,
3240 enable_mcp_apps: None,
3241 skill_directories: None,
3242 instruction_directories: None,
3243 plugin_directories: None,
3244 large_output: None,
3245 disabled_skills: None,
3246 hooks: None,
3247 custom_agents: None,
3248 default_agent: None,
3249 agent: None,
3250 infinite_sessions: None,
3251 provider: None,
3252 capi: None,
3253 providers: None,
3254 models: None,
3255 enable_session_telemetry: None,
3256 enable_citations: None,
3257 session_limits: None,
3258 model_capabilities: None,
3259 memory: None,
3260 config_directory: None,
3261 working_directory: None,
3262 github_token: None,
3263 remote_session: None,
3264 include_sub_agent_streaming_events: None,
3265 commands: None,
3266 exp_assignments: None,
3267 session_fs_provider: None,
3268 suppress_resume_event: None,
3269 continue_pending_work: None,
3270 permission_handler: None,
3271 elicitation_handler: None,
3272 mcp_auth_handler: None,
3273 user_input_handler: None,
3274 exit_plan_mode_handler: None,
3275 auto_mode_switch_handler: None,
3276 hooks_handler: None,
3277 permission_policy: None,
3278 system_message_transform: None,
3279 skip_custom_instructions: None,
3280 custom_agents_local_only: None,
3281 coauthor_enabled: None,
3282 manage_schedule_enabled: None,
3283 }
3284 }
3285
3286 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3288 self.permission_handler = Some(handler);
3289 self
3290 }
3291
3292 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3294 self.elicitation_handler = Some(handler);
3295 self
3296 }
3297
3298 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3300 self.mcp_auth_handler = Some(handler);
3301 self
3302 }
3303
3304 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3306 self.user_input_handler = Some(handler);
3307 self
3308 }
3309
3310 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3312 self.exit_plan_mode_handler = Some(handler);
3313 self
3314 }
3315
3316 pub fn with_auto_mode_switch_handler(
3318 mut self,
3319 handler: Arc<dyn AutoModeSwitchHandler>,
3320 ) -> Self {
3321 self.auto_mode_switch_handler = Some(handler);
3322 self
3323 }
3324
3325 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3328 self.hooks_handler = Some(hooks);
3329 self
3330 }
3331
3332 pub fn with_system_message_transform(
3334 mut self,
3335 transform: Arc<dyn SystemMessageTransform>,
3336 ) -> Self {
3337 self.system_message_transform = Some(transform);
3338 self
3339 }
3340
3341 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3345 self.commands = Some(commands);
3346 self
3347 }
3348
3349 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3352 self.session_fs_provider = Some(provider);
3353 self
3354 }
3355
3356 pub fn approve_all_permissions(mut self) -> Self {
3359 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3360 self
3361 }
3362
3363 pub fn deny_all_permissions(mut self) -> Self {
3366 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3367 self
3368 }
3369
3370 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3373 where
3374 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3375 {
3376 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3377 self
3378 }
3379
3380 pub fn with_model(mut self, model: impl Into<String>) -> Self {
3382 self.model = Some(model.into());
3383 self
3384 }
3385
3386 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3388 self.client_name = Some(name.into());
3389 self
3390 }
3391
3392 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3394 self.reasoning_effort = Some(effort.into());
3395 self
3396 }
3397
3398 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3400 self.reasoning_summary = Some(summary);
3401 self
3402 }
3403
3404 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3407 self.context_tier = Some(tier.into());
3408 self
3409 }
3410
3411 pub fn with_streaming(mut self, streaming: bool) -> Self {
3413 self.streaming = Some(streaming);
3414 self
3415 }
3416
3417 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3420 self.system_message = Some(system_message);
3421 self
3422 }
3423
3424 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3426 self.tools = Some(tools.into_iter().collect());
3427 self
3428 }
3429
3430 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3432 self.canvases = Some(canvases.into_iter().collect());
3433 self
3434 }
3435
3436 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
3438 self.canvas_handler = Some(handler);
3439 self
3440 }
3441
3442 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
3444 mut self,
3445 open_canvases: I,
3446 ) -> Self {
3447 self.open_canvases = Some(open_canvases.into_iter().collect());
3448 self
3449 }
3450
3451 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
3453 self.request_canvas_renderer = Some(request);
3454 self
3455 }
3456
3457 pub fn with_request_extensions(mut self, request: bool) -> Self {
3459 self.request_extensions = Some(request);
3460 self
3461 }
3462
3463 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
3467 self.extension_sdk_path = Some(path.into());
3468 self
3469 }
3470
3471 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
3473 self.extension_info = Some(extension_info);
3474 self
3475 }
3476
3477 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
3479 where
3480 I: IntoIterator<Item = S>,
3481 S: Into<String>,
3482 {
3483 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
3484 self
3485 }
3486
3487 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
3489 where
3490 I: IntoIterator<Item = S>,
3491 S: Into<String>,
3492 {
3493 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
3494 self
3495 }
3496
3497 pub fn with_excluded_builtin_agents<I, S>(mut self, agents: I) -> Self
3499 where
3500 I: IntoIterator<Item = S>,
3501 S: Into<String>,
3502 {
3503 self.excluded_builtin_agents = Some(agents.into_iter().map(Into::into).collect());
3504 self
3505 }
3506
3507 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
3509 self.mcp_servers = Some(servers);
3510 self
3511 }
3512
3513 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3516 self.mcp_oauth_token_storage = Some(mode.into());
3517 self
3518 }
3519
3520 pub fn with_embedding_cache_storage(
3522 mut self,
3523 embedding_cache_storage: impl Into<String>,
3524 ) -> Self {
3525 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3526 self
3527 }
3528
3529 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3531 self.enable_config_discovery = Some(enable);
3532 self
3533 }
3534
3535 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3537 self.skip_embedding_retrieval = Some(value);
3538 self
3539 }
3540
3541 pub fn with_organization_custom_instructions(
3543 mut self,
3544 instructions: impl Into<String>,
3545 ) -> Self {
3546 self.organization_custom_instructions = Some(instructions.into());
3547 self
3548 }
3549
3550 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3552 self.enable_on_demand_instruction_discovery = Some(value);
3553 self
3554 }
3555
3556 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3558 self.enable_file_hooks = Some(value);
3559 self
3560 }
3561
3562 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3564 self.enable_host_git_operations = Some(value);
3565 self
3566 }
3567
3568 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3570 self.enable_session_store = Some(value);
3571 self
3572 }
3573
3574 pub fn with_enable_skills(mut self, value: bool) -> Self {
3576 self.enable_skills = Some(value);
3577 self
3578 }
3579
3580 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3586 self.enable_mcp_apps = Some(enable);
3587 self
3588 }
3589
3590 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3592 where
3593 I: IntoIterator<Item = P>,
3594 P: Into<PathBuf>,
3595 {
3596 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3597 self
3598 }
3599
3600 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3604 where
3605 I: IntoIterator<Item = P>,
3606 P: Into<PathBuf>,
3607 {
3608 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3609 self
3610 }
3611
3612 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3614 where
3615 I: IntoIterator<Item = P>,
3616 P: Into<PathBuf>,
3617 {
3618 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3619 self
3620 }
3621
3622 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3624 self.large_output = Some(config);
3625 self
3626 }
3627
3628 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3630 where
3631 I: IntoIterator<Item = S>,
3632 S: Into<String>,
3633 {
3634 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3635 self
3636 }
3637
3638 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3640 mut self,
3641 agents: I,
3642 ) -> Self {
3643 self.custom_agents = Some(agents.into_iter().collect());
3644 self
3645 }
3646
3647 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3649 self.default_agent = Some(agent);
3650 self
3651 }
3652
3653 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3655 self.agent = Some(name.into());
3656 self
3657 }
3658
3659 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3661 self.infinite_sessions = Some(config);
3662 self
3663 }
3664
3665 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3667 self.provider = Some(provider);
3668 self
3669 }
3670
3671 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3673 self.capi = Some(capi);
3674 self
3675 }
3676
3677 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3683 self.providers = Some(providers);
3684 self
3685 }
3686
3687 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3693 self.models = Some(models);
3694 self
3695 }
3696
3697 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3701 self.enable_session_telemetry = Some(enable);
3702 self
3703 }
3704
3705 pub fn with_enable_citations(mut self, enable: bool) -> Self {
3707 self.enable_citations = Some(enable);
3708 self
3709 }
3710
3711 pub fn with_session_limits(mut self, limits: SessionLimitsConfig) -> Self {
3713 self.session_limits = Some(limits);
3714 self
3715 }
3716
3717 pub fn with_model_capabilities(
3719 mut self,
3720 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3721 ) -> Self {
3722 self.model_capabilities = Some(capabilities);
3723 self
3724 }
3725
3726 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3728 self.memory = Some(memory);
3729 self
3730 }
3731
3732 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3734 self.config_directory = Some(dir.into());
3735 self
3736 }
3737
3738 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3740 self.working_directory = Some(dir.into());
3741 self
3742 }
3743
3744 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3748 self.github_token = Some(token.into());
3749 self
3750 }
3751
3752 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3754 self.include_sub_agent_streaming_events = Some(include);
3755 self
3756 }
3757
3758 pub fn with_remote_session(
3760 mut self,
3761 mode: crate::generated::api_types::RemoteSessionMode,
3762 ) -> Self {
3763 self.remote_session = Some(mode);
3764 self
3765 }
3766
3767 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
3770 self.suppress_resume_event = Some(suppress);
3771 self
3772 }
3773
3774 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
3780 self.continue_pending_work = Some(continue_pending);
3781 self
3782 }
3783
3784 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3786 self.skip_custom_instructions = Some(value);
3787 self
3788 }
3789
3790 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3792 self.custom_agents_local_only = Some(value);
3793 self
3794 }
3795
3796 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3798 self.coauthor_enabled = Some(value);
3799 self
3800 }
3801
3802 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3804 self.manage_schedule_enabled = Some(value);
3805 self
3806 }
3807
3808 #[doc(hidden)]
3812 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
3813 self.exp_assignments = Some(assignments);
3814 self
3815 }
3816}
3817
3818#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3824#[serde(rename_all = "camelCase")]
3825#[non_exhaustive]
3826pub struct SystemMessageConfig {
3827 #[serde(skip_serializing_if = "Option::is_none")]
3829 pub mode: Option<String>,
3830 #[serde(skip_serializing_if = "Option::is_none")]
3832 pub content: Option<String>,
3833 #[serde(skip_serializing_if = "Option::is_none")]
3835 pub sections: Option<HashMap<String, SectionOverride>>,
3836}
3837
3838impl SystemMessageConfig {
3839 pub fn new() -> Self {
3842 Self::default()
3843 }
3844
3845 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
3848 self.mode = Some(mode.into());
3849 self
3850 }
3851
3852 pub fn with_content(mut self, content: impl Into<String>) -> Self {
3855 self.content = Some(content.into());
3856 self
3857 }
3858
3859 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
3861 self.sections = Some(sections);
3862 self
3863 }
3864}
3865
3866#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3872#[serde(rename_all = "camelCase")]
3873pub struct SectionOverride {
3874 #[serde(skip_serializing_if = "Option::is_none")]
3877 pub action: Option<String>,
3878 #[serde(skip_serializing_if = "Option::is_none")]
3880 pub content: Option<String>,
3881}
3882
3883#[derive(Debug, Clone, Serialize, Deserialize)]
3885#[serde(rename_all = "camelCase")]
3886pub struct CreateSessionResult {
3887 pub session_id: SessionId,
3889 #[serde(skip_serializing_if = "Option::is_none")]
3891 pub workspace_path: Option<PathBuf>,
3892 #[serde(default, alias = "remote_url")]
3894 pub remote_url: Option<String>,
3895 #[serde(skip_serializing_if = "Option::is_none")]
3897 pub capabilities: Option<SessionCapabilities>,
3898}
3899
3900#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3902#[serde(rename_all = "camelCase")]
3903pub(crate) struct ResumeSessionResult {
3904 #[serde(default)]
3906 pub session_id: Option<SessionId>,
3907 #[serde(default, skip_serializing_if = "Option::is_none")]
3909 pub workspace_path: Option<PathBuf>,
3910 #[serde(default, alias = "remote_url")]
3912 pub remote_url: Option<String>,
3913 #[serde(default, skip_serializing_if = "Option::is_none")]
3915 pub capabilities: Option<SessionCapabilities>,
3916 #[serde(
3918 default,
3919 alias = "openCanvasInstances",
3920 skip_serializing_if = "Option::is_none"
3921 )]
3922 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3923}
3924
3925#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3927#[serde(rename_all = "lowercase")]
3928pub enum LogLevel {
3929 #[default]
3931 Info,
3932 Warning,
3934 Error,
3936}
3937
3938#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
3943#[serde(rename_all = "camelCase")]
3944pub struct LogOptions {
3945 #[serde(skip_serializing_if = "Option::is_none")]
3947 pub level: Option<LogLevel>,
3948 #[serde(skip_serializing_if = "Option::is_none")]
3951 pub ephemeral: Option<bool>,
3952}
3953
3954impl LogOptions {
3955 pub fn with_level(mut self, level: LogLevel) -> Self {
3957 self.level = Some(level);
3958 self
3959 }
3960
3961 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
3963 self.ephemeral = Some(ephemeral);
3964 self
3965 }
3966}
3967
3968#[derive(Debug, Clone, Default)]
3972pub struct SetModelOptions {
3973 pub reasoning_effort: Option<String>,
3976 pub reasoning_summary: Option<ReasoningSummary>,
3980 pub context_tier: Option<ContextTier>,
3983 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3987}
3988
3989impl SetModelOptions {
3990 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3992 self.reasoning_effort = Some(effort.into());
3993 self
3994 }
3995
3996 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3998 self.reasoning_summary = Some(summary);
3999 self
4000 }
4001
4002 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
4004 self.context_tier = Some(tier);
4005 self
4006 }
4007
4008 pub fn with_model_capabilities(
4010 mut self,
4011 caps: crate::generated::api_types::ModelCapabilitiesOverride,
4012 ) -> Self {
4013 self.model_capabilities = Some(caps);
4014 self
4015 }
4016}
4017
4018#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
4025#[serde(rename_all = "camelCase")]
4026pub struct PingResponse {
4027 #[serde(default)]
4029 pub message: String,
4030 #[serde(default)]
4032 pub timestamp: String,
4033 #[serde(skip_serializing_if = "Option::is_none")]
4035 pub protocol_version: Option<u32>,
4036}
4037
4038#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4040#[serde(rename_all = "camelCase")]
4041pub struct AttachmentLineRange {
4042 pub start: u32,
4044 pub end: u32,
4046}
4047
4048#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4050#[serde(rename_all = "camelCase")]
4051pub struct AttachmentSelectionPosition {
4052 pub line: u32,
4054 pub character: u32,
4056}
4057
4058#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4060#[serde(rename_all = "camelCase")]
4061pub struct AttachmentSelectionRange {
4062 pub start: AttachmentSelectionPosition,
4064 pub end: AttachmentSelectionPosition,
4066}
4067
4068#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4070#[serde(rename_all = "snake_case")]
4071#[non_exhaustive]
4072pub enum GitHubReferenceType {
4073 Issue,
4075 Pr,
4077 Discussion,
4079}
4080
4081#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4087#[serde(rename_all = "camelCase")]
4088pub struct GitHubRepoPointer {
4089 #[serde(skip_serializing_if = "Option::is_none")]
4091 pub id: Option<i64>,
4092 pub name: String,
4094 pub owner: String,
4096}
4097
4098#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4100#[serde(rename_all = "camelCase")]
4101pub struct GitHubFileDiffSide {
4102 pub path: String,
4104 pub r#ref: String,
4106 pub repo: GitHubRepoPointer,
4108}
4109
4110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4112#[serde(rename_all = "camelCase")]
4113pub struct GitHubTreeComparisonSide {
4114 pub repo: GitHubRepoPointer,
4116 pub revision: String,
4118}
4119
4120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4122#[serde(rename_all = "camelCase")]
4123pub struct GitHubSnippetLineRange {
4124 pub start: i64,
4126 pub end: i64,
4128}
4129
4130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4132#[serde(
4133 tag = "type",
4134 rename_all = "camelCase",
4135 rename_all_fields = "camelCase"
4136)]
4137#[non_exhaustive]
4138pub enum Attachment {
4139 File {
4141 path: PathBuf,
4143 #[serde(skip_serializing_if = "Option::is_none")]
4145 display_name: Option<String>,
4146 #[serde(skip_serializing_if = "Option::is_none")]
4148 line_range: Option<AttachmentLineRange>,
4149 },
4150 Directory {
4152 path: PathBuf,
4154 #[serde(skip_serializing_if = "Option::is_none")]
4156 display_name: Option<String>,
4157 },
4158 Selection {
4160 file_path: PathBuf,
4162 text: String,
4164 #[serde(skip_serializing_if = "Option::is_none")]
4166 display_name: Option<String>,
4167 selection: AttachmentSelectionRange,
4169 },
4170 Blob {
4172 data: String,
4174 mime_type: String,
4176 #[serde(skip_serializing_if = "Option::is_none")]
4178 display_name: Option<String>,
4179 },
4180 #[serde(rename = "github_reference")]
4182 GitHubReference {
4183 number: u64,
4185 title: String,
4187 reference_type: GitHubReferenceType,
4189 state: String,
4191 url: String,
4193 },
4194 #[serde(rename = "github_commit")]
4196 GitHubCommit {
4197 message: String,
4199 oid: String,
4201 repo: GitHubRepoPointer,
4203 url: String,
4205 },
4206 #[serde(rename = "github_release")]
4208 GitHubRelease {
4209 name: String,
4211 repo: GitHubRepoPointer,
4213 tag_name: String,
4215 url: String,
4217 },
4218 #[serde(rename = "github_actions_job")]
4220 GitHubActionsJob {
4221 #[serde(skip_serializing_if = "Option::is_none")]
4224 conclusion: Option<String>,
4225 job_id: i64,
4227 job_name: String,
4229 repo: GitHubRepoPointer,
4231 url: String,
4233 workflow_name: String,
4235 },
4236 #[serde(rename = "github_repository")]
4238 GitHubRepository {
4239 #[serde(skip_serializing_if = "Option::is_none")]
4241 description: Option<String>,
4242 #[serde(skip_serializing_if = "Option::is_none")]
4245 r#ref: Option<String>,
4246 repo: GitHubRepoPointer,
4248 url: String,
4250 },
4251 #[serde(rename = "github_file_diff")]
4253 GitHubFileDiff {
4254 #[serde(skip_serializing_if = "Option::is_none")]
4256 base: Option<GitHubFileDiffSide>,
4257 #[serde(skip_serializing_if = "Option::is_none")]
4259 head: Option<GitHubFileDiffSide>,
4260 url: String,
4262 },
4263 #[serde(rename = "github_tree_comparison")]
4265 GitHubTreeComparison {
4266 base: GitHubTreeComparisonSide,
4268 head: GitHubTreeComparisonSide,
4270 url: String,
4272 },
4273 #[serde(rename = "github_url")]
4275 GitHubUrl {
4276 url: String,
4278 },
4279 #[serde(rename = "github_file")]
4281 GitHubFile {
4282 path: String,
4284 r#ref: String,
4286 repo: GitHubRepoPointer,
4288 url: String,
4290 },
4291 #[serde(rename = "github_snippet")]
4293 GitHubSnippet {
4294 line_range: GitHubSnippetLineRange,
4296 path: String,
4298 r#ref: String,
4300 repo: GitHubRepoPointer,
4302 url: String,
4304 },
4305}
4306
4307impl Attachment {
4308 pub fn display_name(&self) -> Option<&str> {
4310 match self {
4311 Self::File { display_name, .. }
4312 | Self::Directory { display_name, .. }
4313 | Self::Selection { display_name, .. }
4314 | Self::Blob { display_name, .. } => display_name.as_deref(),
4315 Self::GitHubReference { .. }
4316 | Self::GitHubCommit { .. }
4317 | Self::GitHubRelease { .. }
4318 | Self::GitHubActionsJob { .. }
4319 | Self::GitHubRepository { .. }
4320 | Self::GitHubFileDiff { .. }
4321 | Self::GitHubTreeComparison { .. }
4322 | Self::GitHubUrl { .. }
4323 | Self::GitHubFile { .. }
4324 | Self::GitHubSnippet { .. } => None,
4325 }
4326 }
4327
4328 pub fn label(&self) -> Option<String> {
4330 if let Some(display_name) = self
4331 .display_name()
4332 .map(str::trim)
4333 .filter(|name| !name.is_empty())
4334 {
4335 return Some(display_name.to_string());
4336 }
4337
4338 match self {
4339 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4340 format!("#{}", number)
4341 } else {
4342 title.trim().to_string()
4343 }),
4344 _ => self.derived_display_name(),
4345 }
4346 }
4347
4348 pub fn ensure_display_name(&mut self) {
4350 if self
4351 .display_name()
4352 .map(str::trim)
4353 .is_some_and(|name| !name.is_empty())
4354 {
4355 return;
4356 }
4357
4358 let Some(derived_display_name) = self.derived_display_name() else {
4359 return;
4360 };
4361
4362 match self {
4363 Self::File { display_name, .. }
4364 | Self::Directory { display_name, .. }
4365 | Self::Selection { display_name, .. }
4366 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4367 Self::GitHubReference { .. }
4368 | Self::GitHubCommit { .. }
4369 | Self::GitHubRelease { .. }
4370 | Self::GitHubActionsJob { .. }
4371 | Self::GitHubRepository { .. }
4372 | Self::GitHubFileDiff { .. }
4373 | Self::GitHubTreeComparison { .. }
4374 | Self::GitHubUrl { .. }
4375 | Self::GitHubFile { .. }
4376 | Self::GitHubSnippet { .. } => {}
4377 }
4378 }
4379
4380 fn derived_display_name(&self) -> Option<String> {
4381 match self {
4382 Self::File { path, .. } | Self::Directory { path, .. } => {
4383 Some(attachment_name_from_path(path))
4384 }
4385 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4386 Self::Blob { .. } => Some("attachment".to_string()),
4387 Self::GitHubReference { .. }
4388 | Self::GitHubCommit { .. }
4389 | Self::GitHubRelease { .. }
4390 | Self::GitHubActionsJob { .. }
4391 | Self::GitHubRepository { .. }
4392 | Self::GitHubFileDiff { .. }
4393 | Self::GitHubTreeComparison { .. }
4394 | Self::GitHubUrl { .. }
4395 | Self::GitHubFile { .. }
4396 | Self::GitHubSnippet { .. } => None,
4397 }
4398 }
4399}
4400
4401fn attachment_name_from_path(path: &Path) -> String {
4402 path.file_name()
4403 .map(|name| name.to_string_lossy().into_owned())
4404 .filter(|name| !name.is_empty())
4405 .unwrap_or_else(|| {
4406 let full = path.to_string_lossy();
4407 if full.is_empty() {
4408 "attachment".to_string()
4409 } else {
4410 full.into_owned()
4411 }
4412 })
4413}
4414
4415pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
4417 for attachment in attachments {
4418 attachment.ensure_display_name();
4419 }
4420}
4421
4422#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4427#[serde(rename_all = "lowercase")]
4428#[non_exhaustive]
4429pub enum DeliveryMode {
4430 Enqueue,
4432 Immediate,
4434}
4435
4436#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4441#[serde(rename_all = "lowercase")]
4442#[non_exhaustive]
4443pub enum AgentMode {
4444 Interactive,
4446 Plan,
4448 Autopilot,
4450 Shell,
4452}
4453
4454#[derive(Debug, Clone)]
4483#[non_exhaustive]
4484pub struct MessageOptions {
4485 pub prompt: String,
4487 pub mode: Option<DeliveryMode>,
4493 pub agent_mode: Option<AgentMode>,
4497 pub attachments: Option<Vec<Attachment>>,
4499 pub wait_timeout: Option<Duration>,
4502 pub request_headers: Option<HashMap<String, String>>,
4506 pub traceparent: Option<String>,
4513 pub tracestate: Option<String>,
4517 pub display_prompt: Option<String>,
4519}
4520
4521impl MessageOptions {
4522 pub fn new(prompt: impl Into<String>) -> Self {
4524 Self {
4525 prompt: prompt.into(),
4526 mode: None,
4527 agent_mode: None,
4528 attachments: None,
4529 wait_timeout: None,
4530 request_headers: None,
4531 traceparent: None,
4532 tracestate: None,
4533 display_prompt: None,
4534 }
4535 }
4536
4537 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
4543 self.mode = Some(mode);
4544 self
4545 }
4546
4547 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
4551 self.agent_mode = Some(agent_mode);
4552 self
4553 }
4554
4555 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
4557 self.attachments = Some(attachments);
4558 self
4559 }
4560
4561 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
4563 self.wait_timeout = Some(timeout);
4564 self
4565 }
4566
4567 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
4569 self.request_headers = Some(headers);
4570 self
4571 }
4572
4573 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
4578 self.traceparent = ctx.traceparent;
4579 self.tracestate = ctx.tracestate;
4580 self
4581 }
4582
4583 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
4585 self.traceparent = Some(traceparent.into());
4586 self
4587 }
4588
4589 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
4591 self.tracestate = Some(tracestate.into());
4592 self
4593 }
4594
4595 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
4597 self.display_prompt = Some(display_prompt.into());
4598 self
4599 }
4600}
4601
4602impl From<&str> for MessageOptions {
4603 fn from(prompt: &str) -> Self {
4604 Self::new(prompt)
4605 }
4606}
4607
4608impl From<String> for MessageOptions {
4609 fn from(prompt: String) -> Self {
4610 Self::new(prompt)
4611 }
4612}
4613
4614impl From<&String> for MessageOptions {
4615 fn from(prompt: &String) -> Self {
4616 Self::new(prompt.clone())
4617 }
4618}
4619
4620#[derive(Debug, Clone, Serialize, Deserialize)]
4622#[serde(rename_all = "camelCase")]
4623#[non_exhaustive]
4624pub struct GetStatusResponse {
4625 pub version: String,
4627 pub protocol_version: u32,
4629}
4630
4631#[derive(Debug, Clone, Serialize, Deserialize)]
4633#[serde(rename_all = "camelCase")]
4634#[non_exhaustive]
4635pub struct GetAuthStatusResponse {
4636 pub is_authenticated: bool,
4638 #[serde(skip_serializing_if = "Option::is_none")]
4641 pub auth_type: Option<String>,
4642 #[serde(skip_serializing_if = "Option::is_none")]
4644 pub host: Option<String>,
4645 #[serde(skip_serializing_if = "Option::is_none")]
4647 pub login: Option<String>,
4648 #[serde(skip_serializing_if = "Option::is_none")]
4650 pub status_message: Option<String>,
4651}
4652
4653#[derive(Debug, Clone, Serialize, Deserialize)]
4657#[serde(rename_all = "camelCase")]
4658pub struct SessionEventNotification {
4659 pub session_id: SessionId,
4661 pub event: SessionEvent,
4663}
4664
4665#[derive(Debug, Clone, Serialize, Deserialize)]
4672#[serde(rename_all = "camelCase")]
4673pub struct SessionEvent {
4674 pub id: String,
4676 pub timestamp: String,
4678 pub parent_id: Option<String>,
4680 #[serde(skip_serializing_if = "Option::is_none")]
4682 pub ephemeral: Option<bool>,
4683 #[serde(skip_serializing_if = "Option::is_none")]
4686 pub agent_id: Option<String>,
4687 #[serde(skip_serializing_if = "Option::is_none")]
4689 pub debug_cli_received_at_ms: Option<i64>,
4690 #[serde(skip_serializing_if = "Option::is_none")]
4692 pub debug_ws_forwarded_at_ms: Option<i64>,
4693 #[serde(rename = "type")]
4695 pub event_type: String,
4696 pub data: Value,
4698}
4699
4700impl SessionEvent {
4701 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
4706 use serde::de::IntoDeserializer;
4707 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
4708 self.event_type.as_str().into_deserializer();
4709 crate::generated::SessionEventType::deserialize(deserializer)
4710 .unwrap_or(crate::generated::SessionEventType::Unknown)
4711 }
4712
4713 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
4719 serde_json::from_value(self.data.clone()).ok()
4720 }
4721
4722 pub fn is_transient_error(&self) -> bool {
4726 self.event_type == "session.error"
4727 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
4728 }
4729}
4730
4731#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4736#[serde(rename_all = "camelCase")]
4737#[non_exhaustive]
4738pub struct ToolInvocation {
4739 pub session_id: SessionId,
4741 pub tool_call_id: String,
4743 pub tool_name: String,
4745 pub arguments: Value,
4747 #[serde(default, skip_serializing_if = "Option::is_none")]
4752 pub traceparent: Option<String>,
4753 #[serde(default, skip_serializing_if = "Option::is_none")]
4756 pub tracestate: Option<String>,
4757}
4758
4759impl ToolInvocation {
4760 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
4781 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
4782 }
4783
4784 pub fn trace_context(&self) -> TraceContext {
4787 TraceContext {
4788 traceparent: self.traceparent.clone(),
4789 tracestate: self.tracestate.clone(),
4790 }
4791 }
4792}
4793
4794#[derive(Debug, Clone, Serialize, Deserialize)]
4796#[serde(rename_all = "camelCase")]
4797pub struct ToolBinaryResult {
4798 pub data: String,
4800 pub mime_type: String,
4802 pub r#type: String,
4804 #[serde(default, skip_serializing_if = "Option::is_none")]
4806 pub description: Option<String>,
4807}
4808
4809#[derive(Debug, Clone, Serialize, Deserialize)]
4811#[serde(rename_all = "camelCase")]
4812pub struct ToolResultExpanded {
4813 pub text_result_for_llm: String,
4815 pub result_type: String,
4817 #[serde(default, skip_serializing_if = "Option::is_none")]
4819 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
4820 #[serde(skip_serializing_if = "Option::is_none")]
4822 pub session_log: Option<String>,
4823 #[serde(skip_serializing_if = "Option::is_none")]
4825 pub error: Option<String>,
4826 #[serde(default, skip_serializing_if = "Option::is_none")]
4828 pub tool_telemetry: Option<HashMap<String, Value>>,
4829}
4830
4831#[derive(Debug, Clone, Serialize, Deserialize)]
4833#[serde(untagged)]
4834#[non_exhaustive]
4835pub enum ToolResult {
4836 Text(String),
4838 Expanded(ToolResultExpanded),
4840}
4841
4842#[derive(Debug, Clone, Serialize, Deserialize)]
4844#[serde(rename_all = "camelCase")]
4845pub struct ToolResultResponse {
4846 pub result: ToolResult,
4848}
4849
4850#[derive(Debug, Clone, Serialize, Deserialize)]
4852#[serde(rename_all = "camelCase")]
4853pub struct SessionMetadata {
4854 pub session_id: SessionId,
4856 pub start_time: String,
4858 pub modified_time: String,
4860 #[serde(skip_serializing_if = "Option::is_none")]
4862 pub summary: Option<String>,
4863 pub is_remote: bool,
4865}
4866
4867#[derive(Debug, Clone, Serialize, Deserialize)]
4869#[serde(rename_all = "camelCase")]
4870pub struct ListSessionsResponse {
4871 pub sessions: Vec<SessionMetadata>,
4873}
4874
4875#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4879#[serde(rename_all = "camelCase")]
4880pub struct SessionListFilter {
4881 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
4883 pub working_directory: Option<String>,
4884 #[serde(default, skip_serializing_if = "Option::is_none")]
4886 pub git_root: Option<String>,
4887 #[serde(default, skip_serializing_if = "Option::is_none")]
4889 pub repository: Option<String>,
4890 #[serde(default, skip_serializing_if = "Option::is_none")]
4892 pub branch: Option<String>,
4893}
4894
4895#[derive(Debug, Clone, Serialize, Deserialize)]
4897#[serde(rename_all = "camelCase")]
4898pub struct GetSessionMetadataResponse {
4899 #[serde(skip_serializing_if = "Option::is_none")]
4901 pub session: Option<SessionMetadata>,
4902}
4903
4904#[derive(Debug, Clone, Serialize, Deserialize)]
4906#[serde(rename_all = "camelCase")]
4907pub struct GetLastSessionIdResponse {
4908 #[serde(skip_serializing_if = "Option::is_none")]
4910 pub session_id: Option<SessionId>,
4911}
4912
4913#[derive(Debug, Clone, Serialize, Deserialize)]
4915#[serde(rename_all = "camelCase")]
4916pub struct GetForegroundSessionResponse {
4917 #[serde(skip_serializing_if = "Option::is_none")]
4919 pub session_id: Option<SessionId>,
4920}
4921
4922#[derive(Debug, Clone, Serialize, Deserialize)]
4924#[serde(rename_all = "camelCase")]
4925pub struct GetMessagesResponse {
4926 pub events: Vec<SessionEvent>,
4928}
4929
4930#[derive(Debug, Clone, Serialize, Deserialize)]
4932#[serde(rename_all = "camelCase")]
4933pub struct ElicitationResult {
4934 pub action: String,
4936 #[serde(skip_serializing_if = "Option::is_none")]
4938 pub content: Option<Value>,
4939}
4940
4941#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4947#[serde(rename_all = "camelCase")]
4948#[non_exhaustive]
4949pub enum ElicitationMode {
4950 Form,
4952 Url,
4954 #[serde(other)]
4956 Unknown,
4957}
4958
4959#[derive(Debug, Clone, Serialize, Deserialize)]
4966#[serde(rename_all = "camelCase")]
4967pub struct ElicitationRequest {
4968 pub message: String,
4970 #[serde(skip_serializing_if = "Option::is_none")]
4972 pub requested_schema: Option<Value>,
4973 #[serde(skip_serializing_if = "Option::is_none")]
4975 pub mode: Option<ElicitationMode>,
4976 #[serde(skip_serializing_if = "Option::is_none")]
4978 pub elicitation_source: Option<String>,
4979 #[serde(skip_serializing_if = "Option::is_none")]
4981 pub url: Option<String>,
4982}
4983
4984#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4989#[serde(rename_all = "camelCase")]
4990pub struct SessionCapabilities {
4991 #[serde(skip_serializing_if = "Option::is_none")]
4993 pub ui: Option<UiCapabilities>,
4994}
4995
4996#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4998#[serde(rename_all = "camelCase")]
4999pub struct UiCapabilities {
5000 #[serde(skip_serializing_if = "Option::is_none")]
5002 pub elicitation: Option<bool>,
5003 #[serde(skip_serializing_if = "Option::is_none")]
5014 pub mcp_apps: Option<bool>,
5015 #[serde(skip_serializing_if = "Option::is_none")]
5017 pub canvases: Option<bool>,
5018}
5019
5020#[derive(Debug, Clone, Default)]
5022pub struct UiInputOptions<'a> {
5023 pub title: Option<&'a str>,
5025 pub description: Option<&'a str>,
5027 pub min_length: Option<u64>,
5029 pub max_length: Option<u64>,
5031 pub format: Option<InputFormat>,
5033 pub default: Option<&'a str>,
5035}
5036
5037#[derive(Debug, Clone, Copy)]
5039#[non_exhaustive]
5040pub enum InputFormat {
5041 Email,
5043 Uri,
5045 Date,
5047 DateTime,
5049}
5050
5051impl InputFormat {
5052 pub fn as_str(&self) -> &'static str {
5054 match self {
5055 Self::Email => "email",
5056 Self::Uri => "uri",
5057 Self::Date => "date",
5058 Self::DateTime => "date-time",
5059 }
5060 }
5061}
5062
5063pub use crate::generated::api_types::{
5068 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
5069 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
5070 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
5071 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
5072};
5073
5074#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
5080#[serde(rename_all = "kebab-case")]
5081#[non_exhaustive]
5082pub enum PermissionRequestKind {
5083 Shell,
5085 Write,
5087 Read,
5089 Url,
5091 Mcp,
5093 CustomTool,
5095 Memory,
5097 Hook,
5099 #[serde(other)]
5102 Unknown,
5103}
5104
5105#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5111#[serde(rename_all = "camelCase")]
5112pub struct PermissionRequestData {
5113 #[serde(default, skip_serializing_if = "Option::is_none")]
5117 pub kind: Option<PermissionRequestKind>,
5118 #[serde(default, skip_serializing_if = "Option::is_none")]
5121 pub tool_call_id: Option<String>,
5122 #[serde(flatten)]
5125 pub extra: Value,
5126}
5127
5128#[derive(Debug, Clone, Serialize, Deserialize)]
5130#[serde(rename_all = "camelCase")]
5131pub struct ExitPlanModeData {
5132 #[serde(default)]
5134 pub summary: String,
5135 #[serde(default, skip_serializing_if = "Option::is_none")]
5137 pub plan_content: Option<String>,
5138 #[serde(default)]
5140 pub actions: Vec<String>,
5141 #[serde(default = "default_recommended_action")]
5143 pub recommended_action: String,
5144}
5145
5146fn default_recommended_action() -> String {
5147 "autopilot".to_string()
5148}
5149
5150impl Default for ExitPlanModeData {
5151 fn default() -> Self {
5152 Self {
5153 summary: String::new(),
5154 plan_content: None,
5155 actions: Vec::new(),
5156 recommended_action: default_recommended_action(),
5157 }
5158 }
5159}
5160
5161#[cfg(test)]
5162mod tests {
5163 use std::path::PathBuf;
5164
5165 use serde_json::json;
5166
5167 use super::{
5168 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5169 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5170 CustomAgentConfig, DeliveryMode, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig,
5171 LargeToolOutputConfig, MemoryConfiguration, NamedProviderConfig, ProviderConfig,
5172 ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
5173 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
5174 ToolResultResponse, ensure_attachment_display_names,
5175 };
5176 use crate::generated::session_events::TypedSessionEvent;
5177
5178 #[test]
5179 fn tool_builder_composes() {
5180 let tool = Tool::new("greet")
5181 .with_description("Say hello")
5182 .with_namespaced_name("hello/greet")
5183 .with_instructions("Pass the user's name")
5184 .with_parameters(json!({
5185 "type": "object",
5186 "properties": { "name": { "type": "string" } },
5187 "required": ["name"]
5188 }))
5189 .with_overrides_built_in_tool(true)
5190 .with_skip_permission(true);
5191 assert_eq!(tool.name, "greet");
5192 assert_eq!(tool.description, "Say hello");
5193 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5194 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5195 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5196 assert!(tool.overrides_built_in_tool);
5197 assert!(tool.skip_permission);
5198 }
5199
5200 #[test]
5201 fn tool_defer_serialization() {
5202 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5203 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5204 let value = serde_json::to_value(&tool).unwrap();
5205 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5206
5207 let plain = Tool::new("plain");
5208 let value = serde_json::to_value(&plain).unwrap();
5209 assert!(value.get("defer").is_none());
5210 }
5211
5212 #[test]
5213 fn custom_agent_config_builder_with_model() {
5214 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5215 .with_model("claude-haiku-4.5")
5216 .with_display_name("My Agent");
5217 assert_eq!(agent.name, "my-agent");
5218 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5219 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5220 }
5221
5222 #[test]
5223 fn custom_agent_config_serializes_model() {
5224 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5225 let wire = serde_json::to_value(&agent).unwrap();
5226 assert_eq!(wire["model"], "claude-haiku-4.5");
5227 assert_eq!(wire["name"], "model-agent");
5228 }
5229
5230 #[test]
5231 fn custom_agent_config_omits_model_when_none() {
5232 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5233 let wire = serde_json::to_value(&agent).unwrap();
5234 assert!(wire.get("model").is_none());
5235 }
5236
5237 #[test]
5238 #[should_panic(expected = "tool parameter schema must be a JSON object")]
5239 fn tool_with_parameters_panics_on_non_object_value() {
5240 let _ = Tool::new("noop").with_parameters(json!(null));
5241 }
5242
5243 #[test]
5244 fn tool_result_expanded_serializes_binary_results_for_llm() {
5245 let response = ToolResultResponse {
5246 result: ToolResult::Expanded(ToolResultExpanded {
5247 text_result_for_llm: "rendered chart".to_string(),
5248 result_type: "success".to_string(),
5249 binary_results_for_llm: Some(vec![ToolBinaryResult {
5250 data: "aW1n".to_string(),
5251 mime_type: "image/png".to_string(),
5252 r#type: "image".to_string(),
5253 description: Some("chart preview".to_string()),
5254 }]),
5255 session_log: None,
5256 error: None,
5257 tool_telemetry: None,
5258 }),
5259 };
5260
5261 let wire = serde_json::to_value(&response).unwrap();
5262
5263 assert_eq!(
5264 wire,
5265 json!({
5266 "result": {
5267 "textResultForLlm": "rendered chart",
5268 "resultType": "success",
5269 "binaryResultsForLlm": [
5270 {
5271 "data": "aW1n",
5272 "mimeType": "image/png",
5273 "type": "image",
5274 "description": "chart preview"
5275 }
5276 ]
5277 }
5278 })
5279 );
5280 }
5281
5282 #[test]
5283 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
5284 let response = ToolResultResponse {
5285 result: ToolResult::Expanded(ToolResultExpanded {
5286 text_result_for_llm: "ok".to_string(),
5287 result_type: "success".to_string(),
5288 binary_results_for_llm: None,
5289 session_log: None,
5290 error: None,
5291 tool_telemetry: None,
5292 }),
5293 };
5294
5295 let wire = serde_json::to_value(&response).unwrap();
5296
5297 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5298 assert!(wire["result"].get("binaryResultsForLlm").is_none());
5299 }
5300
5301 #[test]
5302 fn session_config_default_wire_flags_off_without_handlers() {
5303 let cfg = SessionConfig::default();
5304 assert_eq!(cfg.mcp_oauth_token_storage, None);
5305 let (wire, _runtime) = cfg
5309 .into_wire(Some(SessionId::from("default-flags")))
5310 .expect("default config has no duplicate handlers");
5311 assert!(!wire.request_user_input);
5312 assert!(!wire.request_permission);
5313 assert!(!wire.request_elicitation);
5314 assert!(!wire.request_exit_plan_mode);
5315 assert!(!wire.request_auto_mode_switch);
5316 assert!(!wire.hooks);
5317 assert!(!wire.request_mcp_apps);
5318 }
5319
5320 #[test]
5321 fn resume_session_config_new_wire_flags_off_without_handlers() {
5322 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
5323 assert_eq!(cfg.mcp_oauth_token_storage, None);
5324 let (wire, _runtime) = cfg
5325 .into_wire()
5326 .expect("default resume config has no duplicate handlers");
5327 assert!(!wire.request_user_input);
5328 assert!(!wire.request_permission);
5329 assert!(!wire.request_elicitation);
5330 assert!(!wire.request_exit_plan_mode);
5331 assert!(!wire.request_auto_mode_switch);
5332 assert!(!wire.hooks);
5333 assert!(!wire.request_mcp_apps);
5334 }
5335
5336 #[test]
5337 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5338 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
5339 assert_eq!(cfg.enable_mcp_apps, Some(true));
5340
5341 let (wire, _runtime) = cfg
5342 .into_wire(Some(SessionId::from("enable-mcp-apps")))
5343 .expect("enable_mcp_apps config has no duplicate handlers");
5344 assert!(wire.request_mcp_apps);
5345
5346 let json = serde_json::to_value(&wire).unwrap();
5347 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5348 }
5349
5350 #[test]
5351 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5352 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
5353 .with_enable_mcp_apps(true);
5354 assert_eq!(cfg.enable_mcp_apps, Some(true));
5355
5356 let (wire, _runtime) = cfg
5357 .into_wire()
5358 .expect("resume enable_mcp_apps config has no duplicate handlers");
5359 assert!(wire.request_mcp_apps);
5360
5361 let json = serde_json::to_value(&wire).unwrap();
5362 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5363 }
5364
5365 #[test]
5366 fn memory_configuration_constructors_and_serde() {
5367 assert!(MemoryConfiguration::enabled().enabled);
5368 assert!(!MemoryConfiguration::disabled().enabled);
5369 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
5370
5371 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
5372 assert_eq!(json, serde_json::json!({ "enabled": true }));
5373 }
5374
5375 #[test]
5376 fn session_config_with_memory_serializes() {
5377 let (wire, _runtime) = SessionConfig::default()
5378 .with_memory(MemoryConfiguration::enabled())
5379 .into_wire(Some(SessionId::from("memory-on")))
5380 .expect("no duplicate handlers");
5381 let json = serde_json::to_value(&wire).unwrap();
5382 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5383
5384 let (wire_off, _) = SessionConfig::default()
5385 .with_memory(MemoryConfiguration::disabled())
5386 .into_wire(Some(SessionId::from("memory-off")))
5387 .expect("no duplicate handlers");
5388 let json_off = serde_json::to_value(&wire_off).unwrap();
5389 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
5390
5391 let (empty_wire, _) = SessionConfig::default()
5393 .into_wire(Some(SessionId::from("memory-unset")))
5394 .expect("no duplicate handlers");
5395 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5396 assert!(empty_json.get("memory").is_none());
5397 }
5398
5399 #[test]
5400 fn resume_session_config_with_memory_serializes() {
5401 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
5402 .with_memory(MemoryConfiguration::enabled())
5403 .into_wire()
5404 .expect("no duplicate handlers");
5405 let json = serde_json::to_value(&wire).unwrap();
5406 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5407
5408 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
5410 .into_wire()
5411 .expect("no duplicate handlers");
5412 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5413 assert!(empty_json.get("memory").is_none());
5414 }
5415
5416 #[test]
5417 fn session_config_with_exp_assignments_serializes() {
5418 let assignments = serde_json::json!({
5419 "Parameters": { "copilot_exp_flag": "treatment" },
5420 "AssignmentContext": "ctx-123",
5421 });
5422 let (wire, _runtime) = SessionConfig::default()
5423 .with_exp_assignments(assignments.clone())
5424 .into_wire(Some(SessionId::from("exp-on")))
5425 .expect("no duplicate handlers");
5426 let json = serde_json::to_value(&wire).unwrap();
5427 assert_eq!(json["expAssignments"], assignments);
5428
5429 let (empty_wire, _) = SessionConfig::default()
5431 .into_wire(Some(SessionId::from("exp-unset")))
5432 .expect("no duplicate handlers");
5433 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5434 assert!(empty_json.get("expAssignments").is_none());
5435 }
5436
5437 #[test]
5438 fn resume_session_config_with_exp_assignments_serializes() {
5439 let assignments = serde_json::json!({
5440 "Parameters": { "copilot_exp_flag": "treatment" },
5441 "AssignmentContext": "ctx-456",
5442 });
5443 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
5444 .with_exp_assignments(assignments.clone())
5445 .into_wire()
5446 .expect("no duplicate handlers");
5447 let json = serde_json::to_value(&wire).unwrap();
5448 assert_eq!(json["expAssignments"], assignments);
5449
5450 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
5452 .into_wire()
5453 .expect("no duplicate handlers");
5454 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5455 assert!(empty_json.get("expAssignments").is_none());
5456 }
5457
5458 #[test]
5459 fn session_config_clone_preserves_exp_assignments() {
5460 let assignments = serde_json::json!({
5461 "Parameters": { "copilot_exp_flag": "treatment" },
5462 "AssignmentContext": "ctx-clone",
5463 });
5464 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
5465 let cloned = config.clone();
5466
5467 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5468
5469 let (wire, _runtime) = cloned
5470 .into_wire(Some(SessionId::from("exp-clone")))
5471 .expect("no duplicate handlers");
5472 let json = serde_json::to_value(&wire).unwrap();
5473 assert_eq!(json["expAssignments"], assignments);
5474 }
5475
5476 #[test]
5477 fn resume_session_config_clone_preserves_exp_assignments() {
5478 let assignments = serde_json::json!({
5479 "Parameters": { "copilot_exp_flag": "treatment" },
5480 "AssignmentContext": "ctx-clone-resume",
5481 });
5482 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
5483 .with_exp_assignments(assignments.clone());
5484 let cloned = config.clone();
5485
5486 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5487
5488 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
5489 let json = serde_json::to_value(&wire).unwrap();
5490 assert_eq!(json["expAssignments"], assignments);
5491 }
5492
5493 #[test]
5494 #[allow(clippy::field_reassign_with_default)]
5495 fn session_config_into_wire_serializes_bucket_b_fields() {
5496 use std::path::PathBuf;
5497
5498 use super::{CloudSessionOptions, CloudSessionRepository};
5499
5500 let mut cfg = SessionConfig::default();
5501 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5502 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5503 cfg.github_token = Some("ghs_secret".to_string());
5504 cfg.include_sub_agent_streaming_events = Some(false);
5505 cfg.enable_session_telemetry = Some(false);
5506 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
5507 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
5508 cfg.enable_on_demand_instruction_discovery = Some(false);
5509 cfg.cloud = Some(CloudSessionOptions::with_repository(
5510 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
5511 ));
5512
5513 let (wire, _runtime) = cfg
5514 .into_wire(Some(SessionId::from("custom-id")))
5515 .expect("no duplicate handlers");
5516 let wire_json = serde_json::to_value(&wire).unwrap();
5517 assert_eq!(wire_json["sessionId"], "custom-id");
5518 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5519 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5520 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5521 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
5522 assert_eq!(wire_json["enableSessionTelemetry"], false);
5523 assert_eq!(wire_json["reasoningSummary"], "concise");
5524 assert_eq!(wire_json["remoteSession"], "export");
5525 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5526 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
5527 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
5528 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
5529
5530 let (empty_wire, _) = SessionConfig::default()
5532 .into_wire(Some(SessionId::from("empty")))
5533 .expect("default has no duplicate handlers");
5534 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5535 assert!(empty_json.get("gitHubToken").is_none());
5536 assert!(empty_json.get("enableSessionTelemetry").is_none());
5537 assert!(empty_json.get("reasoningSummary").is_none());
5538 assert!(empty_json.get("remoteSession").is_none());
5539 assert!(
5540 empty_json
5541 .get("enableOnDemandInstructionDiscovery")
5542 .is_none()
5543 );
5544 assert!(empty_json.get("cloud").is_none());
5545 }
5546
5547 #[test]
5548 fn session_config_into_wire_serializes_named_providers_and_models() {
5549 let cfg = SessionConfig::default()
5550 .with_providers(vec![
5551 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
5552 .with_provider_type("openai")
5553 .with_wire_api("responses")
5554 .with_api_key("sk-test"),
5555 ])
5556 .with_models(vec![
5557 ProviderModelConfig::new("gpt-x", "my-openai")
5558 .with_wire_model("gpt-x-2025")
5559 .with_max_output_tokens(2048),
5560 ]);
5561
5562 let (wire, _) = cfg
5563 .into_wire(Some(SessionId::from("sess-providers")))
5564 .expect("no duplicate handlers");
5565 let wire_json = serde_json::to_value(&wire).unwrap();
5566 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
5567 assert_eq!(
5568 wire_json["providers"][0]["baseUrl"],
5569 "https://api.example.com/v1"
5570 );
5571 assert_eq!(wire_json["providers"][0]["type"], "openai");
5572 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
5573 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
5574 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
5575 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
5576 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
5577 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
5578
5579 let (empty_wire, _) = SessionConfig::default()
5580 .into_wire(Some(SessionId::from("empty")))
5581 .expect("default has no duplicate handlers");
5582 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5583 assert!(empty_json.get("providers").is_none());
5584 assert!(empty_json.get("models").is_none());
5585 }
5586
5587 #[test]
5588 fn resume_config_into_wire_serializes_named_providers_and_models() {
5589 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
5590 .with_providers(vec![
5591 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
5592 .with_provider_type("azure")
5593 .with_azure(AzureProviderOptions {
5594 api_version: Some("2024-10-21".to_string()),
5595 }),
5596 ])
5597 .with_models(vec![
5598 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
5599 ]);
5600
5601 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5602 let wire_json = serde_json::to_value(&wire).unwrap();
5603 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
5604 assert_eq!(wire_json["providers"][0]["type"], "azure");
5605 assert_eq!(
5606 wire_json["providers"][0]["azure"]["apiVersion"],
5607 "2024-10-21"
5608 );
5609 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
5610 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
5611 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
5612
5613 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
5614 .into_wire()
5615 .expect("default has no duplicate handlers");
5616 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5617 assert!(empty_json.get("providers").is_none());
5618 assert!(empty_json.get("models").is_none());
5619 }
5620
5621 #[test]
5622 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
5623 use std::path::PathBuf;
5624
5625 let cfg = SessionConfig {
5626 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
5627 large_output: Some(
5628 LargeToolOutputConfig::new()
5629 .with_enabled(true)
5630 .with_max_size_bytes(1024)
5631 .with_output_directory(PathBuf::from("/tmp/large-output")),
5632 ),
5633 ..Default::default()
5634 };
5635
5636 let (wire, _) = cfg
5637 .into_wire(Some(SessionId::from("sess-1")))
5638 .expect("no duplicate handlers");
5639 let wire_json = serde_json::to_value(&wire).unwrap();
5640 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
5641 assert_eq!(wire_json["largeOutput"]["enabled"], true);
5642 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
5643 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
5644
5645 let (empty_wire, _) = SessionConfig::default()
5646 .into_wire(Some(SessionId::from("empty")))
5647 .expect("default has no duplicate handlers");
5648 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5649 assert!(empty_json.get("pluginDirectories").is_none());
5650 assert!(empty_json.get("largeOutput").is_none());
5651 }
5652
5653 #[test]
5654 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
5655 use std::path::PathBuf;
5656
5657 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5658 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5659 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5660 cfg.github_token = Some("ghs_secret".to_string());
5661 cfg.include_sub_agent_streaming_events = Some(true);
5662 cfg.enable_session_telemetry = Some(false);
5663 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
5664 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
5665 cfg.enable_on_demand_instruction_discovery = Some(false);
5666
5667 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5668 let wire_json = serde_json::to_value(&wire).unwrap();
5669 assert_eq!(wire_json["sessionId"], "sess-1");
5670 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5671 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5672 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5673 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
5674 assert_eq!(wire_json["enableSessionTelemetry"], false);
5675 assert_eq!(wire_json["reasoningSummary"], "detailed");
5676 assert_eq!(wire_json["remoteSession"], "on");
5677 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5678
5679 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5681 .into_wire()
5682 .expect("default resume has no duplicate handlers");
5683 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5684 assert!(empty_json.get("reasoningSummary").is_none());
5685 assert!(empty_json.get("remoteSession").is_none());
5686 assert!(
5687 empty_json
5688 .get("enableOnDemandInstructionDiscovery")
5689 .is_none()
5690 );
5691 }
5692
5693 #[test]
5694 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
5695 use std::path::PathBuf;
5696
5697 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5698 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
5699 cfg.large_output = Some(
5700 LargeToolOutputConfig::new()
5701 .with_enabled(false)
5702 .with_max_size_bytes(2048)
5703 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
5704 );
5705
5706 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5707 let wire_json = serde_json::to_value(&wire).unwrap();
5708 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
5709 assert_eq!(wire_json["largeOutput"]["enabled"], false);
5710 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
5711 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
5712
5713 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5714 .into_wire()
5715 .expect("default resume has no duplicate handlers");
5716 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5717 assert!(empty_json.get("pluginDirectories").is_none());
5718 assert!(empty_json.get("largeOutput").is_none());
5719 }
5720
5721 #[test]
5722 fn session_config_builder_composes() {
5723 use std::collections::HashMap;
5724
5725 let cfg = SessionConfig::default()
5726 .with_session_id(SessionId::from("sess-1"))
5727 .with_model("claude-sonnet-4")
5728 .with_client_name("test-app")
5729 .with_reasoning_effort("medium")
5730 .with_reasoning_summary(ReasoningSummary::Concise)
5731 .with_context_tier("long_context")
5732 .with_streaming(true)
5733 .with_tools([Tool::new("greet")])
5734 .with_available_tools(["bash", "view"])
5735 .with_excluded_tools(["dangerous"])
5736 .with_mcp_servers(HashMap::new())
5737 .with_mcp_oauth_token_storage("persistent")
5738 .with_enable_config_discovery(true)
5739 .with_enable_on_demand_instruction_discovery(true)
5740 .with_skill_directories([PathBuf::from("/tmp/skills")])
5741 .with_disabled_skills(["broken-skill"])
5742 .with_agent("researcher")
5743 .with_config_directory(PathBuf::from("/tmp/config"))
5744 .with_working_directory(PathBuf::from("/tmp/work"))
5745 .with_github_token("ghp_test")
5746 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5747 .with_enable_session_telemetry(false)
5748 .with_include_sub_agent_streaming_events(false)
5749 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5750
5751 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
5752 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
5753 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5754 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
5755 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
5756 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
5757 assert_eq!(cfg.streaming, Some(true));
5758 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5759 assert_eq!(
5760 cfg.available_tools.as_deref(),
5761 Some(&["bash".to_string(), "view".to_string()][..])
5762 );
5763 assert_eq!(
5764 cfg.excluded_tools.as_deref(),
5765 Some(&["dangerous".to_string()][..])
5766 );
5767 assert!(cfg.mcp_servers.is_some());
5768 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5769 assert_eq!(cfg.enable_config_discovery, Some(true));
5770 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
5771 assert_eq!(
5772 cfg.skill_directories.as_deref(),
5773 Some(&[PathBuf::from("/tmp/skills")][..])
5774 );
5775 assert_eq!(
5776 cfg.disabled_skills.as_deref(),
5777 Some(&["broken-skill".to_string()][..])
5778 );
5779 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5780 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5781 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5782 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5783 assert_eq!(
5784 cfg.capi,
5785 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5786 );
5787 assert_eq!(cfg.enable_session_telemetry, Some(false));
5788 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
5789 assert_eq!(
5790 cfg.extension_info,
5791 Some(ExtensionInfo::new("github-app", "counter"))
5792 );
5793 }
5794
5795 #[test]
5796 fn resume_session_config_builder_composes() {
5797 use std::collections::HashMap;
5798
5799 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
5800 .with_client_name("test-app")
5801 .with_reasoning_summary(ReasoningSummary::None)
5802 .with_context_tier("default")
5803 .with_streaming(true)
5804 .with_tools([Tool::new("greet")])
5805 .with_available_tools(["bash", "view"])
5806 .with_excluded_tools(["dangerous"])
5807 .with_mcp_servers(HashMap::new())
5808 .with_mcp_oauth_token_storage("persistent")
5809 .with_enable_config_discovery(true)
5810 .with_enable_on_demand_instruction_discovery(false)
5811 .with_skill_directories([PathBuf::from("/tmp/skills")])
5812 .with_disabled_skills(["broken-skill"])
5813 .with_agent("researcher")
5814 .with_config_directory(PathBuf::from("/tmp/config"))
5815 .with_working_directory(PathBuf::from("/tmp/work"))
5816 .with_github_token("ghp_test")
5817 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5818 .with_enable_session_telemetry(false)
5819 .with_include_sub_agent_streaming_events(true)
5820 .with_suppress_resume_event(true)
5821 .with_continue_pending_work(true)
5822 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5823
5824 assert_eq!(cfg.session_id.as_str(), "sess-2");
5825 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5826 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
5827 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
5828 assert_eq!(cfg.streaming, Some(true));
5829 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5830 assert_eq!(
5831 cfg.available_tools.as_deref(),
5832 Some(&["bash".to_string(), "view".to_string()][..])
5833 );
5834 assert_eq!(
5835 cfg.excluded_tools.as_deref(),
5836 Some(&["dangerous".to_string()][..])
5837 );
5838 assert!(cfg.mcp_servers.is_some());
5839 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5840 assert_eq!(cfg.enable_config_discovery, Some(true));
5841 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
5842 assert_eq!(
5843 cfg.skill_directories.as_deref(),
5844 Some(&[PathBuf::from("/tmp/skills")][..])
5845 );
5846 assert_eq!(
5847 cfg.disabled_skills.as_deref(),
5848 Some(&["broken-skill".to_string()][..])
5849 );
5850 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5851 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5852 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5853 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5854 assert_eq!(
5855 cfg.capi,
5856 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5857 );
5858 assert_eq!(cfg.enable_session_telemetry, Some(false));
5859 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
5860 assert_eq!(cfg.suppress_resume_event, Some(true));
5861 assert_eq!(cfg.continue_pending_work, Some(true));
5862 assert_eq!(
5863 cfg.extension_info,
5864 Some(ExtensionInfo::new("github-app", "counter"))
5865 );
5866 }
5867
5868 #[test]
5872 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
5873 let cfg =
5874 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
5875 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5876 let json = serde_json::to_value(&wire).unwrap();
5877 assert_eq!(json["continuePendingWork"], true);
5878
5879 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5881 .into_wire()
5882 .expect("no duplicate handlers");
5883 let json = serde_json::to_value(&wire).unwrap();
5884 assert!(json.get("continuePendingWork").is_none());
5885 }
5886
5887 #[test]
5891 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
5892 let cfg =
5893 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
5894 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5895 let json = serde_json::to_value(&wire).unwrap();
5896 assert_eq!(json["disableResume"], true);
5897 assert!(json.get("suppressResumeEvent").is_none());
5898 }
5899
5900 #[test]
5903 fn session_config_serializes_instruction_directories_to_camel_case() {
5904 let cfg =
5905 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
5906 let (wire, _) = cfg
5907 .into_wire(Some(SessionId::from("instr-on")))
5908 .expect("no duplicate handlers");
5909 let json = serde_json::to_value(&wire).unwrap();
5910 assert_eq!(
5911 json["instructionDirectories"],
5912 serde_json::json!(["/tmp/instr"])
5913 );
5914
5915 let (wire, _) = SessionConfig::default()
5917 .into_wire(Some(SessionId::from("instr-off")))
5918 .expect("no duplicate handlers");
5919 let json = serde_json::to_value(&wire).unwrap();
5920 assert!(json.get("instructionDirectories").is_none());
5921 }
5922
5923 #[test]
5926 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
5927 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
5928 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
5929 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5930 let json = serde_json::to_value(&wire).unwrap();
5931 assert_eq!(
5932 json["instructionDirectories"],
5933 serde_json::json!(["/tmp/instr"])
5934 );
5935
5936 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5937 .into_wire()
5938 .expect("no duplicate handlers");
5939 let json = serde_json::to_value(&wire).unwrap();
5940 assert!(json.get("instructionDirectories").is_none());
5941 }
5942
5943 #[test]
5944 fn custom_agent_config_builder_composes() {
5945 use std::collections::HashMap;
5946
5947 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
5948 .with_display_name("Research Assistant")
5949 .with_description("Investigates technical questions.")
5950 .with_tools(["bash", "view"])
5951 .with_mcp_servers(HashMap::new())
5952 .with_infer(true)
5953 .with_skills(["rust-coding-skill"]);
5954
5955 assert_eq!(cfg.name, "researcher");
5956 assert_eq!(cfg.prompt, "You are a research assistant.");
5957 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
5958 assert_eq!(
5959 cfg.description.as_deref(),
5960 Some("Investigates technical questions.")
5961 );
5962 assert_eq!(
5963 cfg.tools.as_deref(),
5964 Some(&["bash".to_string(), "view".to_string()][..])
5965 );
5966 assert!(cfg.mcp_servers.is_some());
5967 assert_eq!(cfg.infer, Some(true));
5968 assert_eq!(
5969 cfg.skills.as_deref(),
5970 Some(&["rust-coding-skill".to_string()][..])
5971 );
5972 }
5973
5974 #[test]
5975 fn infinite_session_config_builder_composes() {
5976 let cfg = InfiniteSessionConfig::new()
5977 .with_enabled(true)
5978 .with_background_compaction_threshold(0.75)
5979 .with_buffer_exhaustion_threshold(0.92);
5980
5981 assert_eq!(cfg.enabled, Some(true));
5982 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
5983 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
5984 }
5985
5986 #[test]
5987 fn provider_config_builder_composes() {
5988 use std::collections::HashMap;
5989
5990 let mut headers = HashMap::new();
5991 headers.insert("X-Custom".to_string(), "value".to_string());
5992
5993 let cfg = ProviderConfig::new("https://api.example.com")
5994 .with_provider_type("openai")
5995 .with_wire_api("completions")
5996 .with_transport("websockets")
5997 .with_api_key("sk-test")
5998 .with_bearer_token("bearer-test")
5999 .with_headers(headers)
6000 .with_model_id("gpt-4")
6001 .with_wire_model("azure-gpt-4-deployment")
6002 .with_max_prompt_tokens(8192)
6003 .with_max_output_tokens(2048);
6004
6005 assert_eq!(cfg.base_url, "https://api.example.com");
6006 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
6007 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
6008 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
6009 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
6010 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
6011 assert_eq!(
6012 cfg.headers
6013 .as_ref()
6014 .and_then(|h| h.get("X-Custom"))
6015 .map(String::as_str),
6016 Some("value"),
6017 );
6018 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
6019 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
6020 assert_eq!(cfg.max_prompt_tokens, Some(8192));
6021 assert_eq!(cfg.max_output_tokens, Some(2048));
6022
6023 let wire = serde_json::to_value(&cfg).unwrap();
6025 assert_eq!(wire["modelId"], "gpt-4");
6026 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
6027 assert_eq!(wire["maxPromptTokens"], 8192);
6028 assert_eq!(wire["maxOutputTokens"], 2048);
6029
6030 let unset = ProviderConfig::new("https://api.example.com");
6031 let wire_unset = serde_json::to_value(&unset).unwrap();
6032 assert!(wire_unset.get("modelId").is_none());
6033 assert!(wire_unset.get("wireModel").is_none());
6034 assert!(wire_unset.get("maxPromptTokens").is_none());
6035 assert!(wire_unset.get("maxOutputTokens").is_none());
6036 }
6037
6038 #[test]
6039 fn capi_session_options_builder_composes_and_serializes() {
6040 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
6041
6042 assert_eq!(cfg.enable_web_socket_responses, Some(false));
6043
6044 let wire = serde_json::to_value(&cfg).unwrap();
6045 assert_eq!(
6046 wire,
6047 serde_json::json!({ "enableWebSocketResponses": false })
6048 );
6049
6050 let unset = CapiSessionOptions::new();
6051 let wire_unset = serde_json::to_value(&unset).unwrap();
6052 assert!(wire_unset.get("enableWebSocketResponses").is_none());
6053 }
6054
6055 #[test]
6056 fn session_config_with_capi_serializes() {
6057 let (wire, _) = SessionConfig::default()
6058 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6059 .into_wire(Some(SessionId::from("capi-create")))
6060 .expect("no duplicate handlers");
6061 let json = serde_json::to_value(&wire).unwrap();
6062 assert_eq!(
6063 json["capi"],
6064 serde_json::json!({ "enableWebSocketResponses": false })
6065 );
6066
6067 let (empty_wire, _) = SessionConfig::default()
6068 .into_wire(Some(SessionId::from("capi-create-unset")))
6069 .expect("no duplicate handlers");
6070 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6071 assert!(empty_json.get("capi").is_none());
6072 }
6073
6074 #[test]
6075 fn resume_session_config_with_capi_serializes() {
6076 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
6077 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
6078 .into_wire()
6079 .expect("no duplicate handlers");
6080 let json = serde_json::to_value(&wire).unwrap();
6081 assert_eq!(
6082 json["capi"],
6083 serde_json::json!({ "enableWebSocketResponses": false })
6084 );
6085
6086 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
6087 .into_wire()
6088 .expect("no duplicate handlers");
6089 let empty_json = serde_json::to_value(&empty_wire).unwrap();
6090 assert!(empty_json.get("capi").is_none());
6091 }
6092
6093 #[test]
6094 fn system_message_config_builder_composes() {
6095 use std::collections::HashMap;
6096
6097 let cfg = SystemMessageConfig::new()
6098 .with_mode("replace")
6099 .with_content("Custom system message.")
6100 .with_sections(HashMap::new());
6101
6102 assert_eq!(cfg.mode.as_deref(), Some("replace"));
6103 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
6104 assert!(cfg.sections.is_some());
6105 }
6106
6107 #[test]
6108 fn delivery_mode_serializes_to_kebab_case_strings() {
6109 assert_eq!(
6110 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
6111 "\"enqueue\""
6112 );
6113 assert_eq!(
6114 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
6115 "\"immediate\""
6116 );
6117 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
6118 assert_eq!(parsed, DeliveryMode::Immediate);
6119 }
6120
6121 #[test]
6122 fn agent_mode_serializes_to_kebab_case_strings() {
6123 assert_eq!(
6124 serde_json::to_string(&AgentMode::Interactive).unwrap(),
6125 "\"interactive\""
6126 );
6127 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
6128 assert_eq!(
6129 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
6130 "\"autopilot\""
6131 );
6132 assert_eq!(
6133 serde_json::to_string(&AgentMode::Shell).unwrap(),
6134 "\"shell\""
6135 );
6136 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
6137 assert_eq!(parsed, AgentMode::Plan);
6138 }
6139
6140 #[test]
6141 fn connection_state_distinguishes_variants() {
6142 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
6145 }
6146
6147 #[test]
6153 fn session_event_round_trips_agent_id_on_envelope() {
6154 let wire = json!({
6155 "id": "evt-1",
6156 "timestamp": "2026-04-30T12:00:00Z",
6157 "parentId": null,
6158 "agentId": "sub-agent-42",
6159 "type": "assistant.message",
6160 "data": { "message": "hi" }
6161 });
6162
6163 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
6164 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6165
6166 let roundtripped = serde_json::to_value(&event).unwrap();
6168 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6169
6170 let main_agent_event: SessionEvent = serde_json::from_value(json!({
6172 "id": "evt-2",
6173 "timestamp": "2026-04-30T12:00:01Z",
6174 "parentId": null,
6175 "type": "session.idle",
6176 "data": {}
6177 }))
6178 .unwrap();
6179 assert!(main_agent_event.agent_id.is_none());
6180 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
6181 assert!(roundtripped.get("agentId").is_none());
6182 }
6183
6184 #[test]
6186 fn typed_session_event_round_trips_agent_id_on_envelope() {
6187 let wire = json!({
6188 "id": "evt-1",
6189 "timestamp": "2026-04-30T12:00:00Z",
6190 "parentId": null,
6191 "agentId": "sub-agent-42",
6192 "type": "session.idle",
6193 "data": {}
6194 });
6195
6196 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
6197 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6198
6199 let roundtripped = serde_json::to_value(&event).unwrap();
6200 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6201 }
6202
6203 #[test]
6204 fn connection_state_variants_compile() {
6205 let _ = ConnectionState::Disconnected;
6209 let _ = ConnectionState::Connecting;
6210 let _ = ConnectionState::Connected;
6211 let _ = ConnectionState::Error;
6212 }
6213
6214 #[test]
6215 fn deserializes_runtime_attachment_variants() {
6216 let attachments: Vec<Attachment> = serde_json::from_value(json!([
6217 {
6218 "type": "file",
6219 "path": "/tmp/file.rs",
6220 "displayName": "file.rs",
6221 "lineRange": { "start": 7, "end": 12 }
6222 },
6223 {
6224 "type": "directory",
6225 "path": "/tmp/project",
6226 "displayName": "project"
6227 },
6228 {
6229 "type": "selection",
6230 "filePath": "/tmp/lib.rs",
6231 "displayName": "lib.rs",
6232 "text": "fn main() {}",
6233 "selection": {
6234 "start": { "line": 1, "character": 2 },
6235 "end": { "line": 3, "character": 4 }
6236 }
6237 },
6238 {
6239 "type": "blob",
6240 "data": "Zm9v",
6241 "mimeType": "image/png",
6242 "displayName": "image.png"
6243 },
6244 {
6245 "type": "github_reference",
6246 "number": 42,
6247 "title": "Fix rendering",
6248 "referenceType": "issue",
6249 "state": "open",
6250 "url": "https://github.com/example/repo/issues/42"
6251 }
6252 ]))
6253 .expect("attachments should deserialize");
6254
6255 assert_eq!(attachments.len(), 5);
6256 assert!(matches!(
6257 &attachments[0],
6258 Attachment::File {
6259 path,
6260 display_name,
6261 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
6262 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
6263 ));
6264 assert!(matches!(
6265 &attachments[1],
6266 Attachment::Directory { path, display_name }
6267 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
6268 ));
6269 assert!(matches!(
6270 &attachments[2],
6271 Attachment::Selection {
6272 file_path,
6273 display_name,
6274 selection:
6275 AttachmentSelectionRange {
6276 start: AttachmentSelectionPosition { line: 1, character: 2 },
6277 end: AttachmentSelectionPosition { line: 3, character: 4 },
6278 },
6279 ..
6280 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
6281 ));
6282 assert!(matches!(
6283 &attachments[3],
6284 Attachment::Blob {
6285 data,
6286 mime_type,
6287 display_name,
6288 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
6289 ));
6290 assert!(matches!(
6291 &attachments[4],
6292 Attachment::GitHubReference {
6293 number: 42,
6294 title,
6295 reference_type: GitHubReferenceType::Issue,
6296 state,
6297 url,
6298 } if title == "Fix rendering"
6299 && state == "open"
6300 && url == "https://github.com/example/repo/issues/42"
6301 ));
6302 }
6303
6304 #[test]
6305 fn ensures_display_names_for_variants_that_support_them() {
6306 let mut attachments = vec![
6307 Attachment::File {
6308 path: PathBuf::from("/tmp/file.rs"),
6309 display_name: None,
6310 line_range: None,
6311 },
6312 Attachment::Selection {
6313 file_path: PathBuf::from("/tmp/src/lib.rs"),
6314 display_name: None,
6315 text: "fn main() {}".to_string(),
6316 selection: AttachmentSelectionRange {
6317 start: AttachmentSelectionPosition {
6318 line: 0,
6319 character: 0,
6320 },
6321 end: AttachmentSelectionPosition {
6322 line: 0,
6323 character: 10,
6324 },
6325 },
6326 },
6327 Attachment::Blob {
6328 data: "Zm9v".to_string(),
6329 mime_type: "image/png".to_string(),
6330 display_name: None,
6331 },
6332 Attachment::GitHubReference {
6333 number: 7,
6334 title: "Track regressions".to_string(),
6335 reference_type: GitHubReferenceType::Issue,
6336 state: "open".to_string(),
6337 url: "https://example.com/issues/7".to_string(),
6338 },
6339 ];
6340
6341 ensure_attachment_display_names(&mut attachments);
6342
6343 assert_eq!(attachments[0].display_name(), Some("file.rs"));
6344 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
6345 assert_eq!(attachments[2].display_name(), Some("attachment"));
6346 assert_eq!(attachments[3].display_name(), None);
6347 assert_eq!(
6348 attachments[3].label(),
6349 Some("Track regressions".to_string())
6350 );
6351 }
6352
6353 #[test]
6354 fn github_anchored_attachment_variants_round_trip() {
6355 let cases = vec![
6356 (
6357 "github_commit",
6358 json!({
6359 "type": "github_commit",
6360 "message": "Fix the thing",
6361 "oid": "abc123",
6362 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
6363 "url": "https://github.com/octocat/repo/commit/abc123"
6364 }),
6365 ),
6366 (
6367 "github_release",
6368 json!({
6369 "type": "github_release",
6370 "name": "v1.2.3",
6371 "repo": { "name": "repo", "owner": "octocat" },
6372 "tagName": "v1.2.3",
6373 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
6374 }),
6375 ),
6376 (
6377 "github_actions_job",
6378 json!({
6379 "type": "github_actions_job",
6380 "conclusion": "failure",
6381 "jobId": 99,
6382 "jobName": "build",
6383 "repo": { "name": "repo", "owner": "octocat" },
6384 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
6385 "workflowName": "CI"
6386 }),
6387 ),
6388 (
6389 "github_repository",
6390 json!({
6391 "type": "github_repository",
6392 "description": "An example repository",
6393 "ref": "main",
6394 "repo": { "name": "repo", "owner": "octocat" },
6395 "url": "https://github.com/octocat/repo"
6396 }),
6397 ),
6398 (
6399 "github_file_diff",
6400 json!({
6401 "type": "github_file_diff",
6402 "base": {
6403 "path": "src/lib.rs",
6404 "ref": "main",
6405 "repo": { "name": "repo", "owner": "octocat" }
6406 },
6407 "head": {
6408 "path": "src/lib.rs",
6409 "ref": "feature",
6410 "repo": { "name": "repo", "owner": "octocat" }
6411 },
6412 "url": "https://github.com/octocat/repo/compare/main...feature"
6413 }),
6414 ),
6415 (
6416 "github_tree_comparison",
6417 json!({
6418 "type": "github_tree_comparison",
6419 "base": {
6420 "repo": { "name": "repo", "owner": "octocat" },
6421 "revision": "main"
6422 },
6423 "head": {
6424 "repo": { "name": "repo", "owner": "octocat" },
6425 "revision": "feature"
6426 },
6427 "url": "https://github.com/octocat/repo/compare/main...feature"
6428 }),
6429 ),
6430 (
6431 "github_url",
6432 json!({
6433 "type": "github_url",
6434 "url": "https://github.com/octocat/repo/wiki"
6435 }),
6436 ),
6437 (
6438 "github_file",
6439 json!({
6440 "type": "github_file",
6441 "path": "src/main.rs",
6442 "ref": "main",
6443 "repo": { "name": "repo", "owner": "octocat" },
6444 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
6445 }),
6446 ),
6447 (
6448 "github_snippet",
6449 json!({
6450 "type": "github_snippet",
6451 "lineRange": { "start": 10, "end": 20 },
6452 "path": "src/main.rs",
6453 "ref": "main",
6454 "repo": { "name": "repo", "owner": "octocat" },
6455 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
6456 }),
6457 ),
6458 ];
6459
6460 for (expected_type, input) in cases {
6461 let attachment: Attachment = serde_json::from_value(input.clone())
6462 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
6463
6464 let serialized_string = serde_json::to_string(&attachment)
6469 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
6470
6471 assert_eq!(
6473 serialized_string.matches("\"type\":").count(),
6474 1,
6475 "{expected_type} must serialize a single `type` key"
6476 );
6477
6478 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
6479 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
6480 assert_eq!(
6481 serialized.get("type").and_then(|value| value.as_str()),
6482 Some(expected_type),
6483 "{expected_type} must serialize the correct discriminator"
6484 );
6485
6486 assert_eq!(
6488 serialized, input,
6489 "{expected_type} should round-trip without data loss"
6490 );
6491 let reparsed: Attachment = serde_json::from_value(serialized)
6492 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
6493 assert_eq!(
6494 reparsed, attachment,
6495 "{expected_type} should re-deserialize to the same value"
6496 );
6497 }
6498 }
6499}
6500
6501#[cfg(test)]
6502mod permission_builder_tests {
6503 use std::sync::Arc;
6504
6505 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
6506 use crate::permission;
6507 use crate::types::{
6508 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
6509 SessionId,
6510 };
6511
6512 fn data() -> PermissionRequestData {
6513 PermissionRequestData {
6514 extra: serde_json::json!({"tool": "shell"}),
6515 ..Default::default()
6516 }
6517 }
6518
6519 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6522 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6523 }
6524
6525 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6526 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6527 }
6528
6529 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
6530 handler
6531 .handle(SessionId::from("s1"), RequestId::new("1"), data())
6532 .await
6533 }
6534
6535 #[tokio::test]
6536 async fn approve_all_with_handler_present_approves() {
6537 let cfg = SessionConfig::default()
6538 .with_permission_handler(Arc::new(ApproveAllHandler))
6539 .approve_all_permissions();
6540 let h = resolve_create(cfg).expect("policy + handler yields handler");
6541 assert!(matches!(
6542 dispatch(&h).await,
6543 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6544 ));
6545 }
6546
6547 #[tokio::test]
6548 async fn approve_all_standalone_produces_handler() {
6549 let cfg = SessionConfig::default().approve_all_permissions();
6550 let h = resolve_create(cfg).expect("policy alone yields handler");
6551 assert!(matches!(
6552 dispatch(&h).await,
6553 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6554 ));
6555 }
6556
6557 #[tokio::test]
6560 async fn approve_all_is_order_independent() {
6561 let a = SessionConfig::default()
6562 .with_permission_handler(Arc::new(ApproveAllHandler))
6563 .approve_all_permissions();
6564 let b = SessionConfig::default()
6565 .approve_all_permissions()
6566 .with_permission_handler(Arc::new(ApproveAllHandler));
6567 let ha = resolve_create(a).unwrap();
6568 let hb = resolve_create(b).unwrap();
6569 assert!(matches!(
6570 dispatch(&ha).await,
6571 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6572 ));
6573 assert!(matches!(
6574 dispatch(&hb).await,
6575 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6576 ));
6577 }
6578
6579 #[tokio::test]
6580 async fn deny_all_is_order_independent() {
6581 let a = SessionConfig::default()
6582 .with_permission_handler(Arc::new(ApproveAllHandler))
6583 .deny_all_permissions();
6584 let b = SessionConfig::default()
6585 .deny_all_permissions()
6586 .with_permission_handler(Arc::new(ApproveAllHandler));
6587 let ha = resolve_create(a).unwrap();
6588 let hb = resolve_create(b).unwrap();
6589 assert!(matches!(
6590 dispatch(&ha).await,
6591 PermissionResult::Decision(PermissionDecision::Reject(_))
6592 ));
6593 assert!(matches!(
6594 dispatch(&hb).await,
6595 PermissionResult::Decision(PermissionDecision::Reject(_))
6596 ));
6597 }
6598
6599 #[tokio::test]
6600 async fn approve_permissions_if_consults_predicate() {
6601 let cfg = SessionConfig::default().approve_permissions_if(|d| {
6602 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6603 });
6604 let h = resolve_create(cfg).unwrap();
6605 assert!(matches!(
6606 dispatch(&h).await,
6607 PermissionResult::Decision(PermissionDecision::Reject(_))
6608 ));
6609 }
6610
6611 #[tokio::test]
6612 async fn approve_permissions_if_is_order_independent() {
6613 let predicate = |d: &PermissionRequestData| {
6614 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6615 };
6616 let a = SessionConfig::default()
6617 .with_permission_handler(Arc::new(ApproveAllHandler))
6618 .approve_permissions_if(predicate);
6619 let b = SessionConfig::default()
6620 .approve_permissions_if(predicate)
6621 .with_permission_handler(Arc::new(ApproveAllHandler));
6622 let ha = resolve_create(a).unwrap();
6623 let hb = resolve_create(b).unwrap();
6624 assert!(matches!(
6625 dispatch(&ha).await,
6626 PermissionResult::Decision(PermissionDecision::Reject(_))
6627 ));
6628 assert!(matches!(
6629 dispatch(&hb).await,
6630 PermissionResult::Decision(PermissionDecision::Reject(_))
6631 ));
6632 }
6633
6634 #[tokio::test]
6635 async fn resume_session_config_approve_all_works() {
6636 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
6637 .with_permission_handler(Arc::new(ApproveAllHandler))
6638 .approve_all_permissions();
6639 let h = resolve_resume(cfg).unwrap();
6640 assert!(matches!(
6641 dispatch(&h).await,
6642 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6643 ));
6644 }
6645
6646 #[tokio::test]
6647 async fn resume_session_config_approve_all_is_order_independent() {
6648 let a = ResumeSessionConfig::new(SessionId::from("s1"))
6649 .with_permission_handler(Arc::new(ApproveAllHandler))
6650 .approve_all_permissions();
6651 let b = ResumeSessionConfig::new(SessionId::from("s1"))
6652 .approve_all_permissions()
6653 .with_permission_handler(Arc::new(ApproveAllHandler));
6654 let ha = resolve_resume(a).unwrap();
6655 let hb = resolve_resume(b).unwrap();
6656 assert!(matches!(
6657 dispatch(&ha).await,
6658 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6659 ));
6660 assert!(matches!(
6661 dispatch(&hb).await,
6662 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6663 ));
6664 }
6665}