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;
23pub use crate::generated::session_events::ContextTier;
25use crate::generated::session_events::ReasoningSummary;
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 mcp_servers: Option<HashMap<String, McpServerConfig>>,
1607 pub mcp_oauth_token_storage: Option<String>,
1616 pub enable_config_discovery: Option<bool>,
1618 pub skip_embedding_retrieval: Option<bool>,
1620 pub embedding_cache_storage: Option<String>,
1623 pub organization_custom_instructions: Option<String>,
1625 pub enable_on_demand_instruction_discovery: Option<bool>,
1627 pub enable_file_hooks: Option<bool>,
1629 pub enable_host_git_operations: Option<bool>,
1631 pub enable_session_store: Option<bool>,
1633 pub enable_skills: Option<bool>,
1635 pub enable_mcp_apps: Option<bool>,
1662 pub skill_directories: Option<Vec<PathBuf>>,
1664 pub instruction_directories: Option<Vec<PathBuf>>,
1667 pub plugin_directories: Option<Vec<PathBuf>>,
1669 pub large_output: Option<LargeToolOutputConfig>,
1671 pub disabled_skills: Option<Vec<String>>,
1674 pub hooks: Option<bool>,
1678 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1680 pub default_agent: Option<DefaultAgentConfig>,
1684 pub agent: Option<String>,
1687 pub infinite_sessions: Option<InfiniteSessionConfig>,
1690 pub provider: Option<ProviderConfig>,
1694 pub capi: Option<CapiSessionOptions>,
1700 pub providers: Option<Vec<NamedProviderConfig>>,
1707 pub models: Option<Vec<ProviderModelConfig>>,
1713 pub enable_session_telemetry: Option<bool>,
1721 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1724 pub memory: Option<MemoryConfiguration>,
1726 pub config_directory: Option<PathBuf>,
1729 pub working_directory: Option<PathBuf>,
1732 pub github_token: Option<String>,
1738 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
1744 pub cloud: Option<CloudSessionOptions>,
1747 pub include_sub_agent_streaming_events: Option<bool>,
1751 pub commands: Option<Vec<CommandDefinition>>,
1755 #[doc(hidden)]
1762 pub exp_assignments: Option<Value>,
1763 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1768 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
1772 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
1775 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
1778 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
1782 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
1785 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
1788 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1792 pub(crate) permission_policy: Option<crate::permission::Policy>,
1796 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
1801 pub skip_custom_instructions: Option<bool>,
1805 pub custom_agents_local_only: Option<bool>,
1809 pub coauthor_enabled: Option<bool>,
1813 pub manage_schedule_enabled: Option<bool>,
1817}
1818
1819impl std::fmt::Debug for SessionConfig {
1820 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1821 f.debug_struct("SessionConfig")
1822 .field("session_id", &self.session_id)
1823 .field("model", &self.model)
1824 .field("client_name", &self.client_name)
1825 .field("reasoning_effort", &self.reasoning_effort)
1826 .field("reasoning_summary", &self.reasoning_summary)
1827 .field("context_tier", &self.context_tier)
1828 .field("streaming", &self.streaming)
1829 .field("system_message", &self.system_message)
1830 .field("tools", &self.tools)
1831 .field("canvases", &self.canvases)
1832 .field(
1833 "canvas_handler",
1834 &self.canvas_handler.as_ref().map(|_| "<set>"),
1835 )
1836 .field("request_canvas_renderer", &self.request_canvas_renderer)
1837 .field("request_extensions", &self.request_extensions)
1838 .field("extension_sdk_path", &self.extension_sdk_path)
1839 .field("extension_info", &self.extension_info)
1840 .field("available_tools", &self.available_tools)
1841 .field("excluded_tools", &self.excluded_tools)
1842 .field("mcp_servers", &self.mcp_servers)
1843 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
1844 .field("embedding_cache_storage", &self.embedding_cache_storage)
1845 .field("enable_config_discovery", &self.enable_config_discovery)
1846 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
1847 .field(
1848 "organization_custom_instructions",
1849 &self
1850 .organization_custom_instructions
1851 .as_ref()
1852 .map(|_| "<redacted>"),
1853 )
1854 .field(
1855 "enable_on_demand_instruction_discovery",
1856 &self.enable_on_demand_instruction_discovery,
1857 )
1858 .field("enable_file_hooks", &self.enable_file_hooks)
1859 .field(
1860 "enable_host_git_operations",
1861 &self.enable_host_git_operations,
1862 )
1863 .field("enable_session_store", &self.enable_session_store)
1864 .field("enable_skills", &self.enable_skills)
1865 .field("enable_mcp_apps", &self.enable_mcp_apps)
1866 .field("skill_directories", &self.skill_directories)
1867 .field("instruction_directories", &self.instruction_directories)
1868 .field("plugin_directories", &self.plugin_directories)
1869 .field("large_output", &self.large_output)
1870 .field("disabled_skills", &self.disabled_skills)
1871 .field("hooks", &self.hooks)
1872 .field("custom_agents", &self.custom_agents)
1873 .field("default_agent", &self.default_agent)
1874 .field("agent", &self.agent)
1875 .field("infinite_sessions", &self.infinite_sessions)
1876 .field("provider", &self.provider)
1877 .field("capi", &self.capi)
1878 .field("enable_session_telemetry", &self.enable_session_telemetry)
1879 .field("model_capabilities", &self.model_capabilities)
1880 .field("memory", &self.memory)
1881 .field("config_directory", &self.config_directory)
1882 .field("working_directory", &self.working_directory)
1883 .field(
1884 "github_token",
1885 &self.github_token.as_ref().map(|_| "<redacted>"),
1886 )
1887 .field("remote_session", &self.remote_session)
1888 .field("cloud", &self.cloud)
1889 .field(
1890 "include_sub_agent_streaming_events",
1891 &self.include_sub_agent_streaming_events,
1892 )
1893 .field("commands", &self.commands)
1894 .field("exp_assignments", &self.exp_assignments)
1895 .field(
1896 "session_fs_provider",
1897 &self.session_fs_provider.as_ref().map(|_| "<set>"),
1898 )
1899 .field(
1900 "permission_handler",
1901 &self.permission_handler.as_ref().map(|_| "<set>"),
1902 )
1903 .field(
1904 "elicitation_handler",
1905 &self.elicitation_handler.as_ref().map(|_| "<set>"),
1906 )
1907 .field(
1908 "mcp_auth_handler",
1909 &self.mcp_auth_handler.as_ref().map(|_| "<set>"),
1910 )
1911 .field(
1912 "user_input_handler",
1913 &self.user_input_handler.as_ref().map(|_| "<set>"),
1914 )
1915 .field(
1916 "exit_plan_mode_handler",
1917 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
1918 )
1919 .field(
1920 "auto_mode_switch_handler",
1921 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
1922 )
1923 .field(
1924 "hooks_handler",
1925 &self.hooks_handler.as_ref().map(|_| "<set>"),
1926 )
1927 .field(
1928 "system_message_transform",
1929 &self.system_message_transform.as_ref().map(|_| "<set>"),
1930 )
1931 .finish()
1932 }
1933}
1934
1935impl Default for SessionConfig {
1936 fn default() -> Self {
1942 Self {
1943 session_id: None,
1944 model: None,
1945 client_name: None,
1946 reasoning_effort: None,
1947 reasoning_summary: None,
1948 context_tier: None,
1949 streaming: None,
1950 system_message: None,
1951 tools: None,
1952 canvases: None,
1953 canvas_handler: None,
1954 request_canvas_renderer: None,
1955 request_extensions: None,
1956 extension_sdk_path: None,
1957 extension_info: None,
1958 available_tools: None,
1959 excluded_tools: None,
1960 mcp_servers: None,
1961 mcp_oauth_token_storage: None,
1962 enable_config_discovery: None,
1963 skip_embedding_retrieval: None,
1964 organization_custom_instructions: None,
1965 enable_on_demand_instruction_discovery: None,
1966 enable_file_hooks: None,
1967 enable_host_git_operations: None,
1968 enable_session_store: None,
1969 enable_skills: None,
1970 embedding_cache_storage: None,
1971 enable_mcp_apps: None,
1972 skill_directories: None,
1973 instruction_directories: None,
1974 plugin_directories: None,
1975 large_output: None,
1976 disabled_skills: None,
1977 hooks: None,
1978 custom_agents: None,
1979 default_agent: None,
1980 agent: None,
1981 infinite_sessions: None,
1982 provider: None,
1983 capi: None,
1984 providers: None,
1985 models: None,
1986 enable_session_telemetry: None,
1987 model_capabilities: None,
1988 memory: None,
1989 config_directory: None,
1990 working_directory: None,
1991 github_token: None,
1992 remote_session: None,
1993 cloud: None,
1994 include_sub_agent_streaming_events: None,
1995 commands: None,
1996 exp_assignments: None,
1997 session_fs_provider: None,
1998 permission_handler: None,
1999 elicitation_handler: None,
2000 mcp_auth_handler: None,
2001 user_input_handler: None,
2002 exit_plan_mode_handler: None,
2003 auto_mode_switch_handler: None,
2004 hooks_handler: None,
2005 permission_policy: None,
2006 system_message_transform: None,
2007 skip_custom_instructions: None,
2008 custom_agents_local_only: None,
2009 coauthor_enabled: None,
2010 manage_schedule_enabled: None,
2011 }
2012 }
2013}
2014
2015pub(crate) struct SessionConfigRuntime {
2021 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2022 pub permission_policy: Option<crate::permission::Policy>,
2023 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2024 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2025 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2026 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2027 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2028 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2029 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2030 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2031 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2032 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2033 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2034 pub commands: Option<Vec<CommandDefinition>>,
2035}
2036
2037impl SessionConfig {
2038 pub(crate) fn into_wire(
2050 mut self,
2051 session_id: Option<SessionId>,
2052 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2053 let permission_active =
2054 self.permission_handler.is_some() || self.permission_policy.is_some();
2055 let request_user_input = self.user_input_handler.is_some();
2056 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2057 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2058 let request_elicitation = self.elicitation_handler.is_some();
2059 let hooks_flag = self.hooks_handler.is_some();
2060
2061 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2062 if let Some(tools) = self.tools.as_mut() {
2063 for tool in tools.iter_mut() {
2064 if let Some(handler) = tool.handler.take()
2065 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2066 {
2067 return Err(crate::Error::with_message(
2068 crate::ErrorKind::InvalidConfig,
2069 format!("duplicate tool handler registered for name {:?}", tool.name),
2070 ));
2071 }
2072 }
2073 }
2074
2075 let wire_commands = self.commands.as_ref().map(|cmds| {
2076 cmds.iter()
2077 .map(|c| crate::wire::CommandWireDefinition {
2078 name: c.name.clone(),
2079 description: c.description.clone(),
2080 })
2081 .collect()
2082 });
2083 let wire_canvases = self.canvases.clone();
2084 let canvas_handler = self.canvas_handler.clone();
2085 let bearer_token_providers =
2086 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2087
2088 let wire = crate::wire::SessionCreateWire {
2089 session_id,
2090 model: self.model,
2091 client_name: self.client_name,
2092 reasoning_effort: self.reasoning_effort,
2093 reasoning_summary: self.reasoning_summary,
2094 context_tier: self.context_tier,
2095 streaming: self.streaming,
2096 system_message: self.system_message,
2097 tools: self.tools,
2098 canvases: wire_canvases,
2099 request_canvas_renderer: self.request_canvas_renderer,
2100 request_extensions: self.request_extensions,
2101 extension_sdk_path: self.extension_sdk_path,
2102 extension_info: self.extension_info,
2103 available_tools: self.available_tools,
2104 excluded_tools: self.excluded_tools,
2105 tool_filter_precedence: "excluded",
2106 mcp_servers: self.mcp_servers,
2107 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2108 embedding_cache_storage: self.embedding_cache_storage,
2109 env_value_mode: "direct",
2110 enable_config_discovery: self.enable_config_discovery,
2111 skip_embedding_retrieval: self.skip_embedding_retrieval,
2112 organization_custom_instructions: self.organization_custom_instructions,
2113 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2114 enable_file_hooks: self.enable_file_hooks,
2115 enable_host_git_operations: self.enable_host_git_operations,
2116 enable_session_store: self.enable_session_store,
2117 enable_skills: self.enable_skills,
2118 request_user_input,
2119 request_permission: permission_active,
2120 request_exit_plan_mode,
2121 request_auto_mode_switch,
2122 request_elicitation,
2123 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2124 hooks: hooks_flag,
2125 skill_directories: self.skill_directories,
2126 instruction_directories: self.instruction_directories,
2127 plugin_directories: self.plugin_directories,
2128 large_output: self.large_output,
2129 disabled_skills: self.disabled_skills,
2130 custom_agents: self.custom_agents,
2131 default_agent: self.default_agent,
2132 agent: self.agent,
2133 infinite_sessions: self.infinite_sessions,
2134 provider: self.provider,
2135 capi: self.capi,
2136 providers: self.providers,
2137 models: self.models,
2138 enable_session_telemetry: self.enable_session_telemetry,
2139 model_capabilities: self.model_capabilities,
2140 memory: self.memory,
2141 config_dir: self.config_directory,
2142 working_directory: self.working_directory,
2143 github_token: self.github_token,
2144 remote_session: self.remote_session,
2145 cloud: self.cloud,
2146 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2147 commands: wire_commands,
2148 exp_assignments: self.exp_assignments,
2149 };
2150
2151 let runtime = SessionConfigRuntime {
2152 permission_handler: self.permission_handler,
2153 permission_policy: self.permission_policy,
2154 elicitation_handler: self.elicitation_handler,
2155 mcp_auth_handler: self.mcp_auth_handler,
2156 user_input_handler: self.user_input_handler,
2157 exit_plan_mode_handler: self.exit_plan_mode_handler,
2158 auto_mode_switch_handler: self.auto_mode_switch_handler,
2159 hooks_handler: self.hooks_handler,
2160 system_message_transform: self.system_message_transform,
2161 tool_handlers,
2162 canvas_handler,
2163 session_fs_provider: self.session_fs_provider,
2164 bearer_token_providers,
2165 commands: self.commands,
2166 };
2167
2168 Ok((wire, runtime))
2169 }
2170
2171 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2175 self.permission_handler = Some(handler);
2176 self
2177 }
2178
2179 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2182 self.elicitation_handler = Some(handler);
2183 self
2184 }
2185
2186 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
2188 self.mcp_auth_handler = Some(handler);
2189 self
2190 }
2191
2192 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2195 self.user_input_handler = Some(handler);
2196 self
2197 }
2198
2199 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2201 self.exit_plan_mode_handler = Some(handler);
2202 self
2203 }
2204
2205 pub fn with_auto_mode_switch_handler(
2207 mut self,
2208 handler: Arc<dyn AutoModeSwitchHandler>,
2209 ) -> Self {
2210 self.auto_mode_switch_handler = Some(handler);
2211 self
2212 }
2213
2214 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2219 self.commands = Some(commands);
2220 self
2221 }
2222
2223 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2227 self.session_fs_provider = Some(provider);
2228 self
2229 }
2230
2231 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2234 self.hooks_handler = Some(hooks);
2235 self
2236 }
2237
2238 pub fn with_system_message_transform(
2242 mut self,
2243 transform: Arc<dyn SystemMessageTransform>,
2244 ) -> Self {
2245 self.system_message_transform = Some(transform);
2246 self
2247 }
2248
2249 pub fn approve_all_permissions(mut self) -> Self {
2255 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2256 self
2257 }
2258
2259 pub fn deny_all_permissions(mut self) -> Self {
2262 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2263 self
2264 }
2265
2266 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2271 where
2272 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2273 {
2274 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2275 self
2276 }
2277
2278 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2280 self.session_id = Some(id.into());
2281 self
2282 }
2283
2284 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2286 self.model = Some(model.into());
2287 self
2288 }
2289
2290 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2292 self.client_name = Some(name.into());
2293 self
2294 }
2295
2296 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2298 self.reasoning_effort = Some(effort.into());
2299 self
2300 }
2301
2302 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2304 self.reasoning_summary = Some(summary);
2305 self
2306 }
2307
2308 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2310 self.context_tier = Some(tier.into());
2311 self
2312 }
2313
2314 pub fn with_streaming(mut self, streaming: bool) -> Self {
2316 self.streaming = Some(streaming);
2317 self
2318 }
2319
2320 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2322 self.system_message = Some(system_message);
2323 self
2324 }
2325
2326 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2328 self.tools = Some(tools.into_iter().collect());
2329 self
2330 }
2331
2332 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2337 self.canvases = Some(canvases.into_iter().collect());
2338 self
2339 }
2340
2341 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2343 self.canvas_handler = Some(handler);
2344 self
2345 }
2346
2347 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2349 self.request_canvas_renderer = Some(request);
2350 self
2351 }
2352
2353 pub fn with_request_extensions(mut self, request: bool) -> Self {
2355 self.request_extensions = Some(request);
2356 self
2357 }
2358
2359 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2363 self.extension_sdk_path = Some(path.into());
2364 self
2365 }
2366
2367 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2369 self.extension_info = Some(extension_info);
2370 self
2371 }
2372
2373 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2375 where
2376 I: IntoIterator<Item = S>,
2377 S: Into<String>,
2378 {
2379 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2380 self
2381 }
2382
2383 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2385 where
2386 I: IntoIterator<Item = S>,
2387 S: Into<String>,
2388 {
2389 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2390 self
2391 }
2392
2393 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
2395 self.mcp_servers = Some(servers);
2396 self
2397 }
2398
2399 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2407 self.mcp_oauth_token_storage = Some(mode.into());
2408 self
2409 }
2410
2411 pub fn with_embedding_cache_storage(
2413 mut self,
2414 embedding_cache_storage: impl Into<String>,
2415 ) -> Self {
2416 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2417 self
2418 }
2419
2420 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2422 self.enable_config_discovery = Some(enable);
2423 self
2424 }
2425
2426 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2428 self.skip_embedding_retrieval = Some(value);
2429 self
2430 }
2431
2432 pub fn with_organization_custom_instructions(
2434 mut self,
2435 instructions: impl Into<String>,
2436 ) -> Self {
2437 self.organization_custom_instructions = Some(instructions.into());
2438 self
2439 }
2440
2441 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2443 self.enable_on_demand_instruction_discovery = Some(value);
2444 self
2445 }
2446
2447 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2449 self.enable_file_hooks = Some(value);
2450 self
2451 }
2452
2453 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2455 self.enable_host_git_operations = Some(value);
2456 self
2457 }
2458
2459 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2461 self.enable_session_store = Some(value);
2462 self
2463 }
2464
2465 pub fn with_enable_skills(mut self, value: bool) -> Self {
2467 self.enable_skills = Some(value);
2468 self
2469 }
2470
2471 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2477 self.enable_mcp_apps = Some(enable);
2478 self
2479 }
2480
2481 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2483 where
2484 I: IntoIterator<Item = P>,
2485 P: Into<PathBuf>,
2486 {
2487 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2488 self
2489 }
2490
2491 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2495 where
2496 I: IntoIterator<Item = P>,
2497 P: Into<PathBuf>,
2498 {
2499 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2500 self
2501 }
2502
2503 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2505 where
2506 I: IntoIterator<Item = P>,
2507 P: Into<PathBuf>,
2508 {
2509 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2510 self
2511 }
2512
2513 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2515 self.large_output = Some(config);
2516 self
2517 }
2518
2519 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2521 where
2522 I: IntoIterator<Item = S>,
2523 S: Into<String>,
2524 {
2525 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2526 self
2527 }
2528
2529 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2531 mut self,
2532 agents: I,
2533 ) -> Self {
2534 self.custom_agents = Some(agents.into_iter().collect());
2535 self
2536 }
2537
2538 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2540 self.default_agent = Some(agent);
2541 self
2542 }
2543
2544 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2547 self.agent = Some(name.into());
2548 self
2549 }
2550
2551 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2554 self.infinite_sessions = Some(config);
2555 self
2556 }
2557
2558 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2560 self.provider = Some(provider);
2561 self
2562 }
2563
2564 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
2566 self.capi = Some(capi);
2567 self
2568 }
2569
2570 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
2576 self.providers = Some(providers);
2577 self
2578 }
2579
2580 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
2586 self.models = Some(models);
2587 self
2588 }
2589
2590 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2594 self.enable_session_telemetry = Some(enable);
2595 self
2596 }
2597
2598 pub fn with_model_capabilities(
2600 mut self,
2601 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2602 ) -> Self {
2603 self.model_capabilities = Some(capabilities);
2604 self
2605 }
2606
2607 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
2609 self.memory = Some(memory);
2610 self
2611 }
2612
2613 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2615 self.config_directory = Some(dir.into());
2616 self
2617 }
2618
2619 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2622 self.working_directory = Some(dir.into());
2623 self
2624 }
2625
2626 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2631 self.github_token = Some(token.into());
2632 self
2633 }
2634
2635 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2638 self.include_sub_agent_streaming_events = Some(include);
2639 self
2640 }
2641
2642 pub fn with_remote_session(
2644 mut self,
2645 mode: crate::generated::api_types::RemoteSessionMode,
2646 ) -> Self {
2647 self.remote_session = Some(mode);
2648 self
2649 }
2650
2651 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
2653 self.cloud = Some(cloud);
2654 self
2655 }
2656
2657 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
2659 self.skip_custom_instructions = Some(value);
2660 self
2661 }
2662
2663 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
2665 self.custom_agents_local_only = Some(value);
2666 self
2667 }
2668
2669 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
2671 self.coauthor_enabled = Some(value);
2672 self
2673 }
2674
2675 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
2677 self.manage_schedule_enabled = Some(value);
2678 self
2679 }
2680
2681 #[doc(hidden)]
2689 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
2690 self.exp_assignments = Some(assignments);
2691 self
2692 }
2693}
2694#[derive(Clone)]
2701#[non_exhaustive]
2702pub struct ResumeSessionConfig {
2703 pub session_id: SessionId,
2705 pub client_name: Option<String>,
2707 pub reasoning_effort: Option<String>,
2709 pub reasoning_summary: Option<ReasoningSummary>,
2713 pub context_tier: Option<String>,
2716 pub streaming: Option<bool>,
2718 pub system_message: Option<SystemMessageConfig>,
2721 pub tools: Option<Vec<Tool>>,
2723 pub canvases: Option<Vec<CanvasDeclaration>>,
2725 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2728 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
2730 pub request_canvas_renderer: Option<bool>,
2732 pub request_extensions: Option<bool>,
2734 pub extension_sdk_path: Option<String>,
2738 pub extension_info: Option<ExtensionInfo>,
2740 pub available_tools: Option<Vec<String>>,
2742 pub excluded_tools: Option<Vec<String>>,
2744 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
2746 pub mcp_oauth_token_storage: Option<String>,
2749 pub enable_config_discovery: Option<bool>,
2751 pub skip_embedding_retrieval: Option<bool>,
2753 pub embedding_cache_storage: Option<String>,
2755 pub organization_custom_instructions: Option<String>,
2757 pub enable_on_demand_instruction_discovery: Option<bool>,
2759 pub enable_file_hooks: Option<bool>,
2761 pub enable_host_git_operations: Option<bool>,
2763 pub enable_session_store: Option<bool>,
2765 pub enable_skills: Option<bool>,
2767 pub enable_mcp_apps: Option<bool>,
2773 pub skill_directories: Option<Vec<PathBuf>>,
2775 pub instruction_directories: Option<Vec<PathBuf>>,
2778 pub plugin_directories: Option<Vec<PathBuf>>,
2780 pub large_output: Option<LargeToolOutputConfig>,
2782 pub disabled_skills: Option<Vec<String>>,
2784 pub hooks: Option<bool>,
2786 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2788 pub default_agent: Option<DefaultAgentConfig>,
2790 pub agent: Option<String>,
2792 pub infinite_sessions: Option<InfiniteSessionConfig>,
2794 pub provider: Option<ProviderConfig>,
2796 pub capi: Option<CapiSessionOptions>,
2802 pub providers: Option<Vec<NamedProviderConfig>>,
2808 pub models: Option<Vec<ProviderModelConfig>>,
2814 pub enable_session_telemetry: Option<bool>,
2822 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2824 pub memory: Option<MemoryConfiguration>,
2826 pub config_directory: Option<PathBuf>,
2828 pub working_directory: Option<PathBuf>,
2830 pub github_token: Option<String>,
2833 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2836 pub include_sub_agent_streaming_events: Option<bool>,
2838 pub commands: Option<Vec<CommandDefinition>>,
2842 #[doc(hidden)]
2847 pub exp_assignments: Option<Value>,
2848 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2853 pub suppress_resume_event: Option<bool>,
2856 pub continue_pending_work: Option<bool>,
2864 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2867 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2870 pub mcp_auth_handler: Option<Arc<dyn McpAuthHandler>>,
2872 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2875 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2878 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2881 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2883 pub(crate) permission_policy: Option<crate::permission::Policy>,
2885 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2887 pub skip_custom_instructions: Option<bool>,
2889 pub custom_agents_local_only: Option<bool>,
2891 pub coauthor_enabled: Option<bool>,
2893 pub manage_schedule_enabled: Option<bool>,
2895}
2896
2897impl std::fmt::Debug for ResumeSessionConfig {
2898 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2899 f.debug_struct("ResumeSessionConfig")
2900 .field("session_id", &self.session_id)
2901 .field("client_name", &self.client_name)
2902 .field("reasoning_effort", &self.reasoning_effort)
2903 .field("reasoning_summary", &self.reasoning_summary)
2904 .field("context_tier", &self.context_tier)
2905 .field("streaming", &self.streaming)
2906 .field("system_message", &self.system_message)
2907 .field("tools", &self.tools)
2908 .field("canvases", &self.canvases)
2909 .field(
2910 "canvas_handler",
2911 &self.canvas_handler.as_ref().map(|_| "<set>"),
2912 )
2913 .field("open_canvases", &self.open_canvases)
2914 .field("request_canvas_renderer", &self.request_canvas_renderer)
2915 .field("request_extensions", &self.request_extensions)
2916 .field("extension_sdk_path", &self.extension_sdk_path)
2917 .field("extension_info", &self.extension_info)
2918 .field("available_tools", &self.available_tools)
2919 .field("excluded_tools", &self.excluded_tools)
2920 .field("mcp_servers", &self.mcp_servers)
2921 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2922 .field("embedding_cache_storage", &self.embedding_cache_storage)
2923 .field("enable_config_discovery", &self.enable_config_discovery)
2924 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2925 .field(
2926 "organization_custom_instructions",
2927 &self
2928 .organization_custom_instructions
2929 .as_ref()
2930 .map(|_| "<redacted>"),
2931 )
2932 .field(
2933 "enable_on_demand_instruction_discovery",
2934 &self.enable_on_demand_instruction_discovery,
2935 )
2936 .field("enable_file_hooks", &self.enable_file_hooks)
2937 .field(
2938 "enable_host_git_operations",
2939 &self.enable_host_git_operations,
2940 )
2941 .field("enable_session_store", &self.enable_session_store)
2942 .field("enable_skills", &self.enable_skills)
2943 .field("enable_mcp_apps", &self.enable_mcp_apps)
2944 .field("skill_directories", &self.skill_directories)
2945 .field("instruction_directories", &self.instruction_directories)
2946 .field("plugin_directories", &self.plugin_directories)
2947 .field("large_output", &self.large_output)
2948 .field("disabled_skills", &self.disabled_skills)
2949 .field("hooks", &self.hooks)
2950 .field("custom_agents", &self.custom_agents)
2951 .field("default_agent", &self.default_agent)
2952 .field("agent", &self.agent)
2953 .field("infinite_sessions", &self.infinite_sessions)
2954 .field("provider", &self.provider)
2955 .field("capi", &self.capi)
2956 .field("enable_session_telemetry", &self.enable_session_telemetry)
2957 .field("model_capabilities", &self.model_capabilities)
2958 .field("memory", &self.memory)
2959 .field("config_directory", &self.config_directory)
2960 .field("working_directory", &self.working_directory)
2961 .field(
2962 "github_token",
2963 &self.github_token.as_ref().map(|_| "<redacted>"),
2964 )
2965 .field("remote_session", &self.remote_session)
2966 .field(
2967 "include_sub_agent_streaming_events",
2968 &self.include_sub_agent_streaming_events,
2969 )
2970 .field("commands", &self.commands)
2971 .field("exp_assignments", &self.exp_assignments)
2972 .field(
2973 "session_fs_provider",
2974 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2975 )
2976 .field(
2977 "permission_handler",
2978 &self.permission_handler.as_ref().map(|_| "<set>"),
2979 )
2980 .field(
2981 "elicitation_handler",
2982 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2983 )
2984 .field(
2985 "user_input_handler",
2986 &self.user_input_handler.as_ref().map(|_| "<set>"),
2987 )
2988 .field(
2989 "exit_plan_mode_handler",
2990 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2991 )
2992 .field(
2993 "auto_mode_switch_handler",
2994 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2995 )
2996 .field(
2997 "hooks_handler",
2998 &self.hooks_handler.as_ref().map(|_| "<set>"),
2999 )
3000 .field(
3001 "system_message_transform",
3002 &self.system_message_transform.as_ref().map(|_| "<set>"),
3003 )
3004 .field("suppress_resume_event", &self.suppress_resume_event)
3005 .field("continue_pending_work", &self.continue_pending_work)
3006 .finish()
3007 }
3008}
3009
3010impl ResumeSessionConfig {
3011 pub(crate) fn into_wire(
3019 mut self,
3020 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3021 let permission_active =
3022 self.permission_handler.is_some() || self.permission_policy.is_some();
3023 let request_user_input = self.user_input_handler.is_some();
3024 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3025 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3026 let request_elicitation = self.elicitation_handler.is_some();
3027 let hooks_flag = self.hooks_handler.is_some();
3028
3029 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3030 if let Some(tools) = self.tools.as_mut() {
3031 for tool in tools.iter_mut() {
3032 if let Some(handler) = tool.handler.take()
3033 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3034 {
3035 return Err(crate::Error::with_message(
3036 crate::ErrorKind::InvalidConfig,
3037 format!("duplicate tool handler registered for name {:?}", tool.name),
3038 ));
3039 }
3040 }
3041 }
3042
3043 let wire_commands = self.commands.as_ref().map(|cmds| {
3044 cmds.iter()
3045 .map(|c| crate::wire::CommandWireDefinition {
3046 name: c.name.clone(),
3047 description: c.description.clone(),
3048 })
3049 .collect()
3050 });
3051 let wire_canvases = self.canvases.clone();
3052 let canvas_handler = self.canvas_handler.clone();
3053 let bearer_token_providers =
3054 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3055
3056 let wire = crate::wire::SessionResumeWire {
3057 session_id: self.session_id,
3058 client_name: self.client_name,
3059 reasoning_effort: self.reasoning_effort,
3060 reasoning_summary: self.reasoning_summary,
3061 context_tier: self.context_tier,
3062 streaming: self.streaming,
3063 system_message: self.system_message,
3064 tools: self.tools,
3065 canvases: wire_canvases,
3066 open_canvases: self.open_canvases,
3067 request_canvas_renderer: self.request_canvas_renderer,
3068 request_extensions: self.request_extensions,
3069 extension_sdk_path: self.extension_sdk_path,
3070 extension_info: self.extension_info,
3071 available_tools: self.available_tools,
3072 excluded_tools: self.excluded_tools,
3073 tool_filter_precedence: "excluded",
3074 mcp_servers: self.mcp_servers,
3075 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3076 embedding_cache_storage: self.embedding_cache_storage,
3077 env_value_mode: "direct",
3078 enable_config_discovery: self.enable_config_discovery,
3079 skip_embedding_retrieval: self.skip_embedding_retrieval,
3080 organization_custom_instructions: self.organization_custom_instructions,
3081 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3082 enable_file_hooks: self.enable_file_hooks,
3083 enable_host_git_operations: self.enable_host_git_operations,
3084 enable_session_store: self.enable_session_store,
3085 enable_skills: self.enable_skills,
3086 request_user_input,
3087 request_permission: permission_active,
3088 request_exit_plan_mode,
3089 request_auto_mode_switch,
3090 request_elicitation,
3091 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3092 hooks: hooks_flag,
3093 skill_directories: self.skill_directories,
3094 instruction_directories: self.instruction_directories,
3095 plugin_directories: self.plugin_directories,
3096 large_output: self.large_output,
3097 disabled_skills: self.disabled_skills,
3098 custom_agents: self.custom_agents,
3099 default_agent: self.default_agent,
3100 agent: self.agent,
3101 infinite_sessions: self.infinite_sessions,
3102 provider: self.provider,
3103 capi: self.capi,
3104 providers: self.providers,
3105 models: self.models,
3106 enable_session_telemetry: self.enable_session_telemetry,
3107 model_capabilities: self.model_capabilities,
3108 memory: self.memory,
3109 config_dir: self.config_directory,
3110 working_directory: self.working_directory,
3111 github_token: self.github_token,
3112 remote_session: self.remote_session,
3113 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3114 commands: wire_commands,
3115 exp_assignments: self.exp_assignments,
3116 suppress_resume_event: self.suppress_resume_event,
3117 continue_pending_work: self.continue_pending_work,
3118 };
3119
3120 let runtime = SessionConfigRuntime {
3121 permission_handler: self.permission_handler,
3122 permission_policy: self.permission_policy,
3123 elicitation_handler: self.elicitation_handler,
3124 mcp_auth_handler: self.mcp_auth_handler,
3125 user_input_handler: self.user_input_handler,
3126 exit_plan_mode_handler: self.exit_plan_mode_handler,
3127 auto_mode_switch_handler: self.auto_mode_switch_handler,
3128 hooks_handler: self.hooks_handler,
3129 system_message_transform: self.system_message_transform,
3130 tool_handlers,
3131 canvas_handler,
3132 session_fs_provider: self.session_fs_provider,
3133 bearer_token_providers,
3134 commands: self.commands,
3135 };
3136
3137 Ok((wire, runtime))
3138 }
3139
3140 pub fn new(session_id: SessionId) -> Self {
3145 Self {
3146 session_id,
3147 client_name: None,
3148 reasoning_effort: None,
3149 reasoning_summary: None,
3150 context_tier: None,
3151 streaming: None,
3152 system_message: None,
3153 tools: None,
3154 canvases: None,
3155 canvas_handler: None,
3156 open_canvases: None,
3157 request_canvas_renderer: None,
3158 request_extensions: None,
3159 extension_sdk_path: None,
3160 extension_info: None,
3161 available_tools: None,
3162 excluded_tools: None,
3163 mcp_servers: None,
3164 mcp_oauth_token_storage: None,
3165 enable_config_discovery: None,
3166 skip_embedding_retrieval: None,
3167 organization_custom_instructions: None,
3168 enable_on_demand_instruction_discovery: None,
3169 enable_file_hooks: None,
3170 enable_host_git_operations: None,
3171 enable_session_store: None,
3172 enable_skills: None,
3173 embedding_cache_storage: None,
3174 enable_mcp_apps: None,
3175 skill_directories: None,
3176 instruction_directories: None,
3177 plugin_directories: None,
3178 large_output: None,
3179 disabled_skills: None,
3180 hooks: None,
3181 custom_agents: None,
3182 default_agent: None,
3183 agent: None,
3184 infinite_sessions: None,
3185 provider: None,
3186 capi: None,
3187 providers: None,
3188 models: None,
3189 enable_session_telemetry: None,
3190 model_capabilities: None,
3191 memory: None,
3192 config_directory: None,
3193 working_directory: None,
3194 github_token: None,
3195 remote_session: None,
3196 include_sub_agent_streaming_events: None,
3197 commands: None,
3198 exp_assignments: None,
3199 session_fs_provider: None,
3200 suppress_resume_event: None,
3201 continue_pending_work: None,
3202 permission_handler: None,
3203 elicitation_handler: None,
3204 mcp_auth_handler: None,
3205 user_input_handler: None,
3206 exit_plan_mode_handler: None,
3207 auto_mode_switch_handler: None,
3208 hooks_handler: None,
3209 permission_policy: None,
3210 system_message_transform: None,
3211 skip_custom_instructions: None,
3212 custom_agents_local_only: None,
3213 coauthor_enabled: None,
3214 manage_schedule_enabled: None,
3215 }
3216 }
3217
3218 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3220 self.permission_handler = Some(handler);
3221 self
3222 }
3223
3224 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3226 self.elicitation_handler = Some(handler);
3227 self
3228 }
3229
3230 pub fn with_mcp_auth_handler(mut self, handler: Arc<dyn McpAuthHandler>) -> Self {
3232 self.mcp_auth_handler = Some(handler);
3233 self
3234 }
3235
3236 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3238 self.user_input_handler = Some(handler);
3239 self
3240 }
3241
3242 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3244 self.exit_plan_mode_handler = Some(handler);
3245 self
3246 }
3247
3248 pub fn with_auto_mode_switch_handler(
3250 mut self,
3251 handler: Arc<dyn AutoModeSwitchHandler>,
3252 ) -> Self {
3253 self.auto_mode_switch_handler = Some(handler);
3254 self
3255 }
3256
3257 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3260 self.hooks_handler = Some(hooks);
3261 self
3262 }
3263
3264 pub fn with_system_message_transform(
3266 mut self,
3267 transform: Arc<dyn SystemMessageTransform>,
3268 ) -> Self {
3269 self.system_message_transform = Some(transform);
3270 self
3271 }
3272
3273 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3277 self.commands = Some(commands);
3278 self
3279 }
3280
3281 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3284 self.session_fs_provider = Some(provider);
3285 self
3286 }
3287
3288 pub fn approve_all_permissions(mut self) -> Self {
3291 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3292 self
3293 }
3294
3295 pub fn deny_all_permissions(mut self) -> Self {
3298 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3299 self
3300 }
3301
3302 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3305 where
3306 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3307 {
3308 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3309 self
3310 }
3311
3312 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3314 self.client_name = Some(name.into());
3315 self
3316 }
3317
3318 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3320 self.reasoning_effort = Some(effort.into());
3321 self
3322 }
3323
3324 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3326 self.reasoning_summary = Some(summary);
3327 self
3328 }
3329
3330 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3333 self.context_tier = Some(tier.into());
3334 self
3335 }
3336
3337 pub fn with_streaming(mut self, streaming: bool) -> Self {
3339 self.streaming = Some(streaming);
3340 self
3341 }
3342
3343 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3346 self.system_message = Some(system_message);
3347 self
3348 }
3349
3350 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3352 self.tools = Some(tools.into_iter().collect());
3353 self
3354 }
3355
3356 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3358 self.canvases = Some(canvases.into_iter().collect());
3359 self
3360 }
3361
3362 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
3364 self.canvas_handler = Some(handler);
3365 self
3366 }
3367
3368 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
3370 mut self,
3371 open_canvases: I,
3372 ) -> Self {
3373 self.open_canvases = Some(open_canvases.into_iter().collect());
3374 self
3375 }
3376
3377 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
3379 self.request_canvas_renderer = Some(request);
3380 self
3381 }
3382
3383 pub fn with_request_extensions(mut self, request: bool) -> Self {
3385 self.request_extensions = Some(request);
3386 self
3387 }
3388
3389 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
3393 self.extension_sdk_path = Some(path.into());
3394 self
3395 }
3396
3397 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
3399 self.extension_info = Some(extension_info);
3400 self
3401 }
3402
3403 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
3405 where
3406 I: IntoIterator<Item = S>,
3407 S: Into<String>,
3408 {
3409 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
3410 self
3411 }
3412
3413 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
3415 where
3416 I: IntoIterator<Item = S>,
3417 S: Into<String>,
3418 {
3419 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
3420 self
3421 }
3422
3423 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
3425 self.mcp_servers = Some(servers);
3426 self
3427 }
3428
3429 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3432 self.mcp_oauth_token_storage = Some(mode.into());
3433 self
3434 }
3435
3436 pub fn with_embedding_cache_storage(
3438 mut self,
3439 embedding_cache_storage: impl Into<String>,
3440 ) -> Self {
3441 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3442 self
3443 }
3444
3445 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3447 self.enable_config_discovery = Some(enable);
3448 self
3449 }
3450
3451 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3453 self.skip_embedding_retrieval = Some(value);
3454 self
3455 }
3456
3457 pub fn with_organization_custom_instructions(
3459 mut self,
3460 instructions: impl Into<String>,
3461 ) -> Self {
3462 self.organization_custom_instructions = Some(instructions.into());
3463 self
3464 }
3465
3466 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3468 self.enable_on_demand_instruction_discovery = Some(value);
3469 self
3470 }
3471
3472 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3474 self.enable_file_hooks = Some(value);
3475 self
3476 }
3477
3478 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3480 self.enable_host_git_operations = Some(value);
3481 self
3482 }
3483
3484 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3486 self.enable_session_store = Some(value);
3487 self
3488 }
3489
3490 pub fn with_enable_skills(mut self, value: bool) -> Self {
3492 self.enable_skills = Some(value);
3493 self
3494 }
3495
3496 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3502 self.enable_mcp_apps = Some(enable);
3503 self
3504 }
3505
3506 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3508 where
3509 I: IntoIterator<Item = P>,
3510 P: Into<PathBuf>,
3511 {
3512 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3513 self
3514 }
3515
3516 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3520 where
3521 I: IntoIterator<Item = P>,
3522 P: Into<PathBuf>,
3523 {
3524 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3525 self
3526 }
3527
3528 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3530 where
3531 I: IntoIterator<Item = P>,
3532 P: Into<PathBuf>,
3533 {
3534 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3535 self
3536 }
3537
3538 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3540 self.large_output = Some(config);
3541 self
3542 }
3543
3544 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3546 where
3547 I: IntoIterator<Item = S>,
3548 S: Into<String>,
3549 {
3550 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3551 self
3552 }
3553
3554 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3556 mut self,
3557 agents: I,
3558 ) -> Self {
3559 self.custom_agents = Some(agents.into_iter().collect());
3560 self
3561 }
3562
3563 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3565 self.default_agent = Some(agent);
3566 self
3567 }
3568
3569 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3571 self.agent = Some(name.into());
3572 self
3573 }
3574
3575 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3577 self.infinite_sessions = Some(config);
3578 self
3579 }
3580
3581 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3583 self.provider = Some(provider);
3584 self
3585 }
3586
3587 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3589 self.capi = Some(capi);
3590 self
3591 }
3592
3593 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3599 self.providers = Some(providers);
3600 self
3601 }
3602
3603 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3609 self.models = Some(models);
3610 self
3611 }
3612
3613 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3617 self.enable_session_telemetry = Some(enable);
3618 self
3619 }
3620
3621 pub fn with_model_capabilities(
3623 mut self,
3624 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3625 ) -> Self {
3626 self.model_capabilities = Some(capabilities);
3627 self
3628 }
3629
3630 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3632 self.memory = Some(memory);
3633 self
3634 }
3635
3636 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3638 self.config_directory = Some(dir.into());
3639 self
3640 }
3641
3642 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3644 self.working_directory = Some(dir.into());
3645 self
3646 }
3647
3648 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3652 self.github_token = Some(token.into());
3653 self
3654 }
3655
3656 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3658 self.include_sub_agent_streaming_events = Some(include);
3659 self
3660 }
3661
3662 pub fn with_remote_session(
3664 mut self,
3665 mode: crate::generated::api_types::RemoteSessionMode,
3666 ) -> Self {
3667 self.remote_session = Some(mode);
3668 self
3669 }
3670
3671 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
3674 self.suppress_resume_event = Some(suppress);
3675 self
3676 }
3677
3678 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
3684 self.continue_pending_work = Some(continue_pending);
3685 self
3686 }
3687
3688 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3690 self.skip_custom_instructions = Some(value);
3691 self
3692 }
3693
3694 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3696 self.custom_agents_local_only = Some(value);
3697 self
3698 }
3699
3700 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3702 self.coauthor_enabled = Some(value);
3703 self
3704 }
3705
3706 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3708 self.manage_schedule_enabled = Some(value);
3709 self
3710 }
3711
3712 #[doc(hidden)]
3716 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
3717 self.exp_assignments = Some(assignments);
3718 self
3719 }
3720}
3721
3722#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3728#[serde(rename_all = "camelCase")]
3729#[non_exhaustive]
3730pub struct SystemMessageConfig {
3731 #[serde(skip_serializing_if = "Option::is_none")]
3733 pub mode: Option<String>,
3734 #[serde(skip_serializing_if = "Option::is_none")]
3736 pub content: Option<String>,
3737 #[serde(skip_serializing_if = "Option::is_none")]
3739 pub sections: Option<HashMap<String, SectionOverride>>,
3740}
3741
3742impl SystemMessageConfig {
3743 pub fn new() -> Self {
3746 Self::default()
3747 }
3748
3749 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
3752 self.mode = Some(mode.into());
3753 self
3754 }
3755
3756 pub fn with_content(mut self, content: impl Into<String>) -> Self {
3759 self.content = Some(content.into());
3760 self
3761 }
3762
3763 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
3765 self.sections = Some(sections);
3766 self
3767 }
3768}
3769
3770#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3776#[serde(rename_all = "camelCase")]
3777pub struct SectionOverride {
3778 #[serde(skip_serializing_if = "Option::is_none")]
3781 pub action: Option<String>,
3782 #[serde(skip_serializing_if = "Option::is_none")]
3784 pub content: Option<String>,
3785}
3786
3787#[derive(Debug, Clone, Serialize, Deserialize)]
3789#[serde(rename_all = "camelCase")]
3790pub struct CreateSessionResult {
3791 pub session_id: SessionId,
3793 #[serde(skip_serializing_if = "Option::is_none")]
3795 pub workspace_path: Option<PathBuf>,
3796 #[serde(default, alias = "remote_url")]
3798 pub remote_url: Option<String>,
3799 #[serde(skip_serializing_if = "Option::is_none")]
3801 pub capabilities: Option<SessionCapabilities>,
3802}
3803
3804#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3806#[serde(rename_all = "camelCase")]
3807pub(crate) struct ResumeSessionResult {
3808 #[serde(default)]
3810 pub session_id: Option<SessionId>,
3811 #[serde(default, skip_serializing_if = "Option::is_none")]
3813 pub workspace_path: Option<PathBuf>,
3814 #[serde(default, alias = "remote_url")]
3816 pub remote_url: Option<String>,
3817 #[serde(default, skip_serializing_if = "Option::is_none")]
3819 pub capabilities: Option<SessionCapabilities>,
3820 #[serde(
3822 default,
3823 alias = "openCanvasInstances",
3824 skip_serializing_if = "Option::is_none"
3825 )]
3826 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3827}
3828
3829#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3831#[serde(rename_all = "lowercase")]
3832pub enum LogLevel {
3833 #[default]
3835 Info,
3836 Warning,
3838 Error,
3840}
3841
3842#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
3847#[serde(rename_all = "camelCase")]
3848pub struct LogOptions {
3849 #[serde(skip_serializing_if = "Option::is_none")]
3851 pub level: Option<LogLevel>,
3852 #[serde(skip_serializing_if = "Option::is_none")]
3855 pub ephemeral: Option<bool>,
3856}
3857
3858impl LogOptions {
3859 pub fn with_level(mut self, level: LogLevel) -> Self {
3861 self.level = Some(level);
3862 self
3863 }
3864
3865 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
3867 self.ephemeral = Some(ephemeral);
3868 self
3869 }
3870}
3871
3872#[derive(Debug, Clone, Default)]
3876pub struct SetModelOptions {
3877 pub reasoning_effort: Option<String>,
3880 pub reasoning_summary: Option<ReasoningSummary>,
3884 pub context_tier: Option<ContextTier>,
3887 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3891}
3892
3893impl SetModelOptions {
3894 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3896 self.reasoning_effort = Some(effort.into());
3897 self
3898 }
3899
3900 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3902 self.reasoning_summary = Some(summary);
3903 self
3904 }
3905
3906 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
3908 self.context_tier = Some(tier);
3909 self
3910 }
3911
3912 pub fn with_model_capabilities(
3914 mut self,
3915 caps: crate::generated::api_types::ModelCapabilitiesOverride,
3916 ) -> Self {
3917 self.model_capabilities = Some(caps);
3918 self
3919 }
3920}
3921
3922#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
3929#[serde(rename_all = "camelCase")]
3930pub struct PingResponse {
3931 #[serde(default)]
3933 pub message: String,
3934 #[serde(default)]
3936 pub timestamp: String,
3937 #[serde(skip_serializing_if = "Option::is_none")]
3939 pub protocol_version: Option<u32>,
3940}
3941
3942#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3944#[serde(rename_all = "camelCase")]
3945pub struct AttachmentLineRange {
3946 pub start: u32,
3948 pub end: u32,
3950}
3951
3952#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3954#[serde(rename_all = "camelCase")]
3955pub struct AttachmentSelectionPosition {
3956 pub line: u32,
3958 pub character: u32,
3960}
3961
3962#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3964#[serde(rename_all = "camelCase")]
3965pub struct AttachmentSelectionRange {
3966 pub start: AttachmentSelectionPosition,
3968 pub end: AttachmentSelectionPosition,
3970}
3971
3972#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3974#[serde(rename_all = "snake_case")]
3975#[non_exhaustive]
3976pub enum GitHubReferenceType {
3977 Issue,
3979 Pr,
3981 Discussion,
3983}
3984
3985#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3991#[serde(rename_all = "camelCase")]
3992pub struct GitHubRepoPointer {
3993 #[serde(skip_serializing_if = "Option::is_none")]
3995 pub id: Option<i64>,
3996 pub name: String,
3998 pub owner: String,
4000}
4001
4002#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4004#[serde(rename_all = "camelCase")]
4005pub struct GitHubFileDiffSide {
4006 pub path: String,
4008 pub r#ref: String,
4010 pub repo: GitHubRepoPointer,
4012}
4013
4014#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4016#[serde(rename_all = "camelCase")]
4017pub struct GitHubTreeComparisonSide {
4018 pub repo: GitHubRepoPointer,
4020 pub revision: String,
4022}
4023
4024#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4026#[serde(rename_all = "camelCase")]
4027pub struct GitHubSnippetLineRange {
4028 pub start: i64,
4030 pub end: i64,
4032}
4033
4034#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4036#[serde(
4037 tag = "type",
4038 rename_all = "camelCase",
4039 rename_all_fields = "camelCase"
4040)]
4041#[non_exhaustive]
4042pub enum Attachment {
4043 File {
4045 path: PathBuf,
4047 #[serde(skip_serializing_if = "Option::is_none")]
4049 display_name: Option<String>,
4050 #[serde(skip_serializing_if = "Option::is_none")]
4052 line_range: Option<AttachmentLineRange>,
4053 },
4054 Directory {
4056 path: PathBuf,
4058 #[serde(skip_serializing_if = "Option::is_none")]
4060 display_name: Option<String>,
4061 },
4062 Selection {
4064 file_path: PathBuf,
4066 text: String,
4068 #[serde(skip_serializing_if = "Option::is_none")]
4070 display_name: Option<String>,
4071 selection: AttachmentSelectionRange,
4073 },
4074 Blob {
4076 data: String,
4078 mime_type: String,
4080 #[serde(skip_serializing_if = "Option::is_none")]
4082 display_name: Option<String>,
4083 },
4084 #[serde(rename = "github_reference")]
4086 GitHubReference {
4087 number: u64,
4089 title: String,
4091 reference_type: GitHubReferenceType,
4093 state: String,
4095 url: String,
4097 },
4098 #[serde(rename = "github_commit")]
4100 GitHubCommit {
4101 message: String,
4103 oid: String,
4105 repo: GitHubRepoPointer,
4107 url: String,
4109 },
4110 #[serde(rename = "github_release")]
4112 GitHubRelease {
4113 name: String,
4115 repo: GitHubRepoPointer,
4117 tag_name: String,
4119 url: String,
4121 },
4122 #[serde(rename = "github_actions_job")]
4124 GitHubActionsJob {
4125 #[serde(skip_serializing_if = "Option::is_none")]
4128 conclusion: Option<String>,
4129 job_id: i64,
4131 job_name: String,
4133 repo: GitHubRepoPointer,
4135 url: String,
4137 workflow_name: String,
4139 },
4140 #[serde(rename = "github_repository")]
4142 GitHubRepository {
4143 #[serde(skip_serializing_if = "Option::is_none")]
4145 description: Option<String>,
4146 #[serde(skip_serializing_if = "Option::is_none")]
4149 r#ref: Option<String>,
4150 repo: GitHubRepoPointer,
4152 url: String,
4154 },
4155 #[serde(rename = "github_file_diff")]
4157 GitHubFileDiff {
4158 #[serde(skip_serializing_if = "Option::is_none")]
4160 base: Option<GitHubFileDiffSide>,
4161 #[serde(skip_serializing_if = "Option::is_none")]
4163 head: Option<GitHubFileDiffSide>,
4164 url: String,
4166 },
4167 #[serde(rename = "github_tree_comparison")]
4169 GitHubTreeComparison {
4170 base: GitHubTreeComparisonSide,
4172 head: GitHubTreeComparisonSide,
4174 url: String,
4176 },
4177 #[serde(rename = "github_url")]
4179 GitHubUrl {
4180 url: String,
4182 },
4183 #[serde(rename = "github_file")]
4185 GitHubFile {
4186 path: String,
4188 r#ref: String,
4190 repo: GitHubRepoPointer,
4192 url: String,
4194 },
4195 #[serde(rename = "github_snippet")]
4197 GitHubSnippet {
4198 line_range: GitHubSnippetLineRange,
4200 path: String,
4202 r#ref: String,
4204 repo: GitHubRepoPointer,
4206 url: String,
4208 },
4209}
4210
4211impl Attachment {
4212 pub fn display_name(&self) -> Option<&str> {
4214 match self {
4215 Self::File { display_name, .. }
4216 | Self::Directory { display_name, .. }
4217 | Self::Selection { display_name, .. }
4218 | Self::Blob { display_name, .. } => display_name.as_deref(),
4219 Self::GitHubReference { .. }
4220 | Self::GitHubCommit { .. }
4221 | Self::GitHubRelease { .. }
4222 | Self::GitHubActionsJob { .. }
4223 | Self::GitHubRepository { .. }
4224 | Self::GitHubFileDiff { .. }
4225 | Self::GitHubTreeComparison { .. }
4226 | Self::GitHubUrl { .. }
4227 | Self::GitHubFile { .. }
4228 | Self::GitHubSnippet { .. } => None,
4229 }
4230 }
4231
4232 pub fn label(&self) -> Option<String> {
4234 if let Some(display_name) = self
4235 .display_name()
4236 .map(str::trim)
4237 .filter(|name| !name.is_empty())
4238 {
4239 return Some(display_name.to_string());
4240 }
4241
4242 match self {
4243 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4244 format!("#{}", number)
4245 } else {
4246 title.trim().to_string()
4247 }),
4248 _ => self.derived_display_name(),
4249 }
4250 }
4251
4252 pub fn ensure_display_name(&mut self) {
4254 if self
4255 .display_name()
4256 .map(str::trim)
4257 .is_some_and(|name| !name.is_empty())
4258 {
4259 return;
4260 }
4261
4262 let Some(derived_display_name) = self.derived_display_name() else {
4263 return;
4264 };
4265
4266 match self {
4267 Self::File { display_name, .. }
4268 | Self::Directory { display_name, .. }
4269 | Self::Selection { display_name, .. }
4270 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4271 Self::GitHubReference { .. }
4272 | Self::GitHubCommit { .. }
4273 | Self::GitHubRelease { .. }
4274 | Self::GitHubActionsJob { .. }
4275 | Self::GitHubRepository { .. }
4276 | Self::GitHubFileDiff { .. }
4277 | Self::GitHubTreeComparison { .. }
4278 | Self::GitHubUrl { .. }
4279 | Self::GitHubFile { .. }
4280 | Self::GitHubSnippet { .. } => {}
4281 }
4282 }
4283
4284 fn derived_display_name(&self) -> Option<String> {
4285 match self {
4286 Self::File { path, .. } | Self::Directory { path, .. } => {
4287 Some(attachment_name_from_path(path))
4288 }
4289 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4290 Self::Blob { .. } => Some("attachment".to_string()),
4291 Self::GitHubReference { .. }
4292 | Self::GitHubCommit { .. }
4293 | Self::GitHubRelease { .. }
4294 | Self::GitHubActionsJob { .. }
4295 | Self::GitHubRepository { .. }
4296 | Self::GitHubFileDiff { .. }
4297 | Self::GitHubTreeComparison { .. }
4298 | Self::GitHubUrl { .. }
4299 | Self::GitHubFile { .. }
4300 | Self::GitHubSnippet { .. } => None,
4301 }
4302 }
4303}
4304
4305fn attachment_name_from_path(path: &Path) -> String {
4306 path.file_name()
4307 .map(|name| name.to_string_lossy().into_owned())
4308 .filter(|name| !name.is_empty())
4309 .unwrap_or_else(|| {
4310 let full = path.to_string_lossy();
4311 if full.is_empty() {
4312 "attachment".to_string()
4313 } else {
4314 full.into_owned()
4315 }
4316 })
4317}
4318
4319pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
4321 for attachment in attachments {
4322 attachment.ensure_display_name();
4323 }
4324}
4325
4326#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4331#[serde(rename_all = "lowercase")]
4332#[non_exhaustive]
4333pub enum DeliveryMode {
4334 Enqueue,
4336 Immediate,
4338}
4339
4340#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4345#[serde(rename_all = "lowercase")]
4346#[non_exhaustive]
4347pub enum AgentMode {
4348 Interactive,
4350 Plan,
4352 Autopilot,
4354 Shell,
4356}
4357
4358#[derive(Debug, Clone)]
4387#[non_exhaustive]
4388pub struct MessageOptions {
4389 pub prompt: String,
4391 pub mode: Option<DeliveryMode>,
4397 pub agent_mode: Option<AgentMode>,
4401 pub attachments: Option<Vec<Attachment>>,
4403 pub wait_timeout: Option<Duration>,
4406 pub request_headers: Option<HashMap<String, String>>,
4410 pub traceparent: Option<String>,
4417 pub tracestate: Option<String>,
4421 pub display_prompt: Option<String>,
4423}
4424
4425impl MessageOptions {
4426 pub fn new(prompt: impl Into<String>) -> Self {
4428 Self {
4429 prompt: prompt.into(),
4430 mode: None,
4431 agent_mode: None,
4432 attachments: None,
4433 wait_timeout: None,
4434 request_headers: None,
4435 traceparent: None,
4436 tracestate: None,
4437 display_prompt: None,
4438 }
4439 }
4440
4441 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
4447 self.mode = Some(mode);
4448 self
4449 }
4450
4451 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
4455 self.agent_mode = Some(agent_mode);
4456 self
4457 }
4458
4459 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
4461 self.attachments = Some(attachments);
4462 self
4463 }
4464
4465 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
4467 self.wait_timeout = Some(timeout);
4468 self
4469 }
4470
4471 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
4473 self.request_headers = Some(headers);
4474 self
4475 }
4476
4477 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
4482 self.traceparent = ctx.traceparent;
4483 self.tracestate = ctx.tracestate;
4484 self
4485 }
4486
4487 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
4489 self.traceparent = Some(traceparent.into());
4490 self
4491 }
4492
4493 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
4495 self.tracestate = Some(tracestate.into());
4496 self
4497 }
4498
4499 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
4501 self.display_prompt = Some(display_prompt.into());
4502 self
4503 }
4504}
4505
4506impl From<&str> for MessageOptions {
4507 fn from(prompt: &str) -> Self {
4508 Self::new(prompt)
4509 }
4510}
4511
4512impl From<String> for MessageOptions {
4513 fn from(prompt: String) -> Self {
4514 Self::new(prompt)
4515 }
4516}
4517
4518impl From<&String> for MessageOptions {
4519 fn from(prompt: &String) -> Self {
4520 Self::new(prompt.clone())
4521 }
4522}
4523
4524#[derive(Debug, Clone, Serialize, Deserialize)]
4526#[serde(rename_all = "camelCase")]
4527#[non_exhaustive]
4528pub struct GetStatusResponse {
4529 pub version: String,
4531 pub protocol_version: u32,
4533}
4534
4535#[derive(Debug, Clone, Serialize, Deserialize)]
4537#[serde(rename_all = "camelCase")]
4538#[non_exhaustive]
4539pub struct GetAuthStatusResponse {
4540 pub is_authenticated: bool,
4542 #[serde(skip_serializing_if = "Option::is_none")]
4545 pub auth_type: Option<String>,
4546 #[serde(skip_serializing_if = "Option::is_none")]
4548 pub host: Option<String>,
4549 #[serde(skip_serializing_if = "Option::is_none")]
4551 pub login: Option<String>,
4552 #[serde(skip_serializing_if = "Option::is_none")]
4554 pub status_message: Option<String>,
4555}
4556
4557#[derive(Debug, Clone, Serialize, Deserialize)]
4561#[serde(rename_all = "camelCase")]
4562pub struct SessionEventNotification {
4563 pub session_id: SessionId,
4565 pub event: SessionEvent,
4567}
4568
4569#[derive(Debug, Clone, Serialize, Deserialize)]
4576#[serde(rename_all = "camelCase")]
4577pub struct SessionEvent {
4578 pub id: String,
4580 pub timestamp: String,
4582 pub parent_id: Option<String>,
4584 #[serde(skip_serializing_if = "Option::is_none")]
4586 pub ephemeral: Option<bool>,
4587 #[serde(skip_serializing_if = "Option::is_none")]
4590 pub agent_id: Option<String>,
4591 #[serde(skip_serializing_if = "Option::is_none")]
4593 pub debug_cli_received_at_ms: Option<i64>,
4594 #[serde(skip_serializing_if = "Option::is_none")]
4596 pub debug_ws_forwarded_at_ms: Option<i64>,
4597 #[serde(rename = "type")]
4599 pub event_type: String,
4600 pub data: Value,
4602}
4603
4604impl SessionEvent {
4605 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
4610 use serde::de::IntoDeserializer;
4611 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
4612 self.event_type.as_str().into_deserializer();
4613 crate::generated::SessionEventType::deserialize(deserializer)
4614 .unwrap_or(crate::generated::SessionEventType::Unknown)
4615 }
4616
4617 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
4623 serde_json::from_value(self.data.clone()).ok()
4624 }
4625
4626 pub fn is_transient_error(&self) -> bool {
4630 self.event_type == "session.error"
4631 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
4632 }
4633}
4634
4635#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4640#[serde(rename_all = "camelCase")]
4641#[non_exhaustive]
4642pub struct ToolInvocation {
4643 pub session_id: SessionId,
4645 pub tool_call_id: String,
4647 pub tool_name: String,
4649 pub arguments: Value,
4651 #[serde(default, skip_serializing_if = "Option::is_none")]
4656 pub traceparent: Option<String>,
4657 #[serde(default, skip_serializing_if = "Option::is_none")]
4660 pub tracestate: Option<String>,
4661}
4662
4663impl ToolInvocation {
4664 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
4685 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
4686 }
4687
4688 pub fn trace_context(&self) -> TraceContext {
4691 TraceContext {
4692 traceparent: self.traceparent.clone(),
4693 tracestate: self.tracestate.clone(),
4694 }
4695 }
4696}
4697
4698#[derive(Debug, Clone, Serialize, Deserialize)]
4700#[serde(rename_all = "camelCase")]
4701pub struct ToolBinaryResult {
4702 pub data: String,
4704 pub mime_type: String,
4706 pub r#type: String,
4708 #[serde(default, skip_serializing_if = "Option::is_none")]
4710 pub description: Option<String>,
4711}
4712
4713#[derive(Debug, Clone, Serialize, Deserialize)]
4715#[serde(rename_all = "camelCase")]
4716pub struct ToolResultExpanded {
4717 pub text_result_for_llm: String,
4719 pub result_type: String,
4721 #[serde(default, skip_serializing_if = "Option::is_none")]
4723 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
4724 #[serde(skip_serializing_if = "Option::is_none")]
4726 pub session_log: Option<String>,
4727 #[serde(skip_serializing_if = "Option::is_none")]
4729 pub error: Option<String>,
4730 #[serde(default, skip_serializing_if = "Option::is_none")]
4732 pub tool_telemetry: Option<HashMap<String, Value>>,
4733}
4734
4735#[derive(Debug, Clone, Serialize, Deserialize)]
4737#[serde(untagged)]
4738#[non_exhaustive]
4739pub enum ToolResult {
4740 Text(String),
4742 Expanded(ToolResultExpanded),
4744}
4745
4746#[derive(Debug, Clone, Serialize, Deserialize)]
4748#[serde(rename_all = "camelCase")]
4749pub struct ToolResultResponse {
4750 pub result: ToolResult,
4752}
4753
4754#[derive(Debug, Clone, Serialize, Deserialize)]
4756#[serde(rename_all = "camelCase")]
4757pub struct SessionMetadata {
4758 pub session_id: SessionId,
4760 pub start_time: String,
4762 pub modified_time: String,
4764 #[serde(skip_serializing_if = "Option::is_none")]
4766 pub summary: Option<String>,
4767 pub is_remote: bool,
4769}
4770
4771#[derive(Debug, Clone, Serialize, Deserialize)]
4773#[serde(rename_all = "camelCase")]
4774pub struct ListSessionsResponse {
4775 pub sessions: Vec<SessionMetadata>,
4777}
4778
4779#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4783#[serde(rename_all = "camelCase")]
4784pub struct SessionListFilter {
4785 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
4787 pub working_directory: Option<String>,
4788 #[serde(default, skip_serializing_if = "Option::is_none")]
4790 pub git_root: Option<String>,
4791 #[serde(default, skip_serializing_if = "Option::is_none")]
4793 pub repository: Option<String>,
4794 #[serde(default, skip_serializing_if = "Option::is_none")]
4796 pub branch: Option<String>,
4797}
4798
4799#[derive(Debug, Clone, Serialize, Deserialize)]
4801#[serde(rename_all = "camelCase")]
4802pub struct GetSessionMetadataResponse {
4803 #[serde(skip_serializing_if = "Option::is_none")]
4805 pub session: Option<SessionMetadata>,
4806}
4807
4808#[derive(Debug, Clone, Serialize, Deserialize)]
4810#[serde(rename_all = "camelCase")]
4811pub struct GetLastSessionIdResponse {
4812 #[serde(skip_serializing_if = "Option::is_none")]
4814 pub session_id: Option<SessionId>,
4815}
4816
4817#[derive(Debug, Clone, Serialize, Deserialize)]
4819#[serde(rename_all = "camelCase")]
4820pub struct GetForegroundSessionResponse {
4821 #[serde(skip_serializing_if = "Option::is_none")]
4823 pub session_id: Option<SessionId>,
4824}
4825
4826#[derive(Debug, Clone, Serialize, Deserialize)]
4828#[serde(rename_all = "camelCase")]
4829pub struct GetMessagesResponse {
4830 pub events: Vec<SessionEvent>,
4832}
4833
4834#[derive(Debug, Clone, Serialize, Deserialize)]
4836#[serde(rename_all = "camelCase")]
4837pub struct ElicitationResult {
4838 pub action: String,
4840 #[serde(skip_serializing_if = "Option::is_none")]
4842 pub content: Option<Value>,
4843}
4844
4845#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4851#[serde(rename_all = "camelCase")]
4852#[non_exhaustive]
4853pub enum ElicitationMode {
4854 Form,
4856 Url,
4858 #[serde(other)]
4860 Unknown,
4861}
4862
4863#[derive(Debug, Clone, Serialize, Deserialize)]
4870#[serde(rename_all = "camelCase")]
4871pub struct ElicitationRequest {
4872 pub message: String,
4874 #[serde(skip_serializing_if = "Option::is_none")]
4876 pub requested_schema: Option<Value>,
4877 #[serde(skip_serializing_if = "Option::is_none")]
4879 pub mode: Option<ElicitationMode>,
4880 #[serde(skip_serializing_if = "Option::is_none")]
4882 pub elicitation_source: Option<String>,
4883 #[serde(skip_serializing_if = "Option::is_none")]
4885 pub url: Option<String>,
4886}
4887
4888#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4893#[serde(rename_all = "camelCase")]
4894pub struct SessionCapabilities {
4895 #[serde(skip_serializing_if = "Option::is_none")]
4897 pub ui: Option<UiCapabilities>,
4898}
4899
4900#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4902#[serde(rename_all = "camelCase")]
4903pub struct UiCapabilities {
4904 #[serde(skip_serializing_if = "Option::is_none")]
4906 pub elicitation: Option<bool>,
4907 #[serde(skip_serializing_if = "Option::is_none")]
4918 pub mcp_apps: Option<bool>,
4919 #[serde(skip_serializing_if = "Option::is_none")]
4921 pub canvases: Option<bool>,
4922}
4923
4924#[derive(Debug, Clone, Default)]
4926pub struct UiInputOptions<'a> {
4927 pub title: Option<&'a str>,
4929 pub description: Option<&'a str>,
4931 pub min_length: Option<u64>,
4933 pub max_length: Option<u64>,
4935 pub format: Option<InputFormat>,
4937 pub default: Option<&'a str>,
4939}
4940
4941#[derive(Debug, Clone, Copy)]
4943#[non_exhaustive]
4944pub enum InputFormat {
4945 Email,
4947 Uri,
4949 Date,
4951 DateTime,
4953}
4954
4955impl InputFormat {
4956 pub fn as_str(&self) -> &'static str {
4958 match self {
4959 Self::Email => "email",
4960 Self::Uri => "uri",
4961 Self::Date => "date",
4962 Self::DateTime => "date-time",
4963 }
4964 }
4965}
4966
4967pub use crate::generated::api_types::{
4972 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
4973 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
4974 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
4975 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
4976};
4977
4978#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4984#[serde(rename_all = "kebab-case")]
4985#[non_exhaustive]
4986pub enum PermissionRequestKind {
4987 Shell,
4989 Write,
4991 Read,
4993 Url,
4995 Mcp,
4997 CustomTool,
4999 Memory,
5001 Hook,
5003 #[serde(other)]
5006 Unknown,
5007}
5008
5009#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5015#[serde(rename_all = "camelCase")]
5016pub struct PermissionRequestData {
5017 #[serde(default, skip_serializing_if = "Option::is_none")]
5021 pub kind: Option<PermissionRequestKind>,
5022 #[serde(default, skip_serializing_if = "Option::is_none")]
5025 pub tool_call_id: Option<String>,
5026 #[serde(flatten)]
5029 pub extra: Value,
5030}
5031
5032#[derive(Debug, Clone, Serialize, Deserialize)]
5034#[serde(rename_all = "camelCase")]
5035pub struct ExitPlanModeData {
5036 #[serde(default)]
5038 pub summary: String,
5039 #[serde(default, skip_serializing_if = "Option::is_none")]
5041 pub plan_content: Option<String>,
5042 #[serde(default)]
5044 pub actions: Vec<String>,
5045 #[serde(default = "default_recommended_action")]
5047 pub recommended_action: String,
5048}
5049
5050fn default_recommended_action() -> String {
5051 "autopilot".to_string()
5052}
5053
5054impl Default for ExitPlanModeData {
5055 fn default() -> Self {
5056 Self {
5057 summary: String::new(),
5058 plan_content: None,
5059 actions: Vec::new(),
5060 recommended_action: default_recommended_action(),
5061 }
5062 }
5063}
5064
5065#[cfg(test)]
5066mod tests {
5067 use std::path::PathBuf;
5068
5069 use serde_json::json;
5070
5071 use super::{
5072 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5073 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5074 CustomAgentConfig, DeliveryMode, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig,
5075 LargeToolOutputConfig, MemoryConfiguration, NamedProviderConfig, ProviderConfig,
5076 ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
5077 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
5078 ToolResultResponse, ensure_attachment_display_names,
5079 };
5080 use crate::generated::session_events::TypedSessionEvent;
5081
5082 #[test]
5083 fn tool_builder_composes() {
5084 let tool = Tool::new("greet")
5085 .with_description("Say hello")
5086 .with_namespaced_name("hello/greet")
5087 .with_instructions("Pass the user's name")
5088 .with_parameters(json!({
5089 "type": "object",
5090 "properties": { "name": { "type": "string" } },
5091 "required": ["name"]
5092 }))
5093 .with_overrides_built_in_tool(true)
5094 .with_skip_permission(true);
5095 assert_eq!(tool.name, "greet");
5096 assert_eq!(tool.description, "Say hello");
5097 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5098 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5099 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5100 assert!(tool.overrides_built_in_tool);
5101 assert!(tool.skip_permission);
5102 }
5103
5104 #[test]
5105 fn tool_defer_serialization() {
5106 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5107 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5108 let value = serde_json::to_value(&tool).unwrap();
5109 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5110
5111 let plain = Tool::new("plain");
5112 let value = serde_json::to_value(&plain).unwrap();
5113 assert!(value.get("defer").is_none());
5114 }
5115
5116 #[test]
5117 fn custom_agent_config_builder_with_model() {
5118 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5119 .with_model("claude-haiku-4.5")
5120 .with_display_name("My Agent");
5121 assert_eq!(agent.name, "my-agent");
5122 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5123 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5124 }
5125
5126 #[test]
5127 fn custom_agent_config_serializes_model() {
5128 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5129 let wire = serde_json::to_value(&agent).unwrap();
5130 assert_eq!(wire["model"], "claude-haiku-4.5");
5131 assert_eq!(wire["name"], "model-agent");
5132 }
5133
5134 #[test]
5135 fn custom_agent_config_omits_model_when_none() {
5136 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5137 let wire = serde_json::to_value(&agent).unwrap();
5138 assert!(wire.get("model").is_none());
5139 }
5140
5141 #[test]
5142 #[should_panic(expected = "tool parameter schema must be a JSON object")]
5143 fn tool_with_parameters_panics_on_non_object_value() {
5144 let _ = Tool::new("noop").with_parameters(json!(null));
5145 }
5146
5147 #[test]
5148 fn tool_result_expanded_serializes_binary_results_for_llm() {
5149 let response = ToolResultResponse {
5150 result: ToolResult::Expanded(ToolResultExpanded {
5151 text_result_for_llm: "rendered chart".to_string(),
5152 result_type: "success".to_string(),
5153 binary_results_for_llm: Some(vec![ToolBinaryResult {
5154 data: "aW1n".to_string(),
5155 mime_type: "image/png".to_string(),
5156 r#type: "image".to_string(),
5157 description: Some("chart preview".to_string()),
5158 }]),
5159 session_log: None,
5160 error: None,
5161 tool_telemetry: None,
5162 }),
5163 };
5164
5165 let wire = serde_json::to_value(&response).unwrap();
5166
5167 assert_eq!(
5168 wire,
5169 json!({
5170 "result": {
5171 "textResultForLlm": "rendered chart",
5172 "resultType": "success",
5173 "binaryResultsForLlm": [
5174 {
5175 "data": "aW1n",
5176 "mimeType": "image/png",
5177 "type": "image",
5178 "description": "chart preview"
5179 }
5180 ]
5181 }
5182 })
5183 );
5184 }
5185
5186 #[test]
5187 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
5188 let response = ToolResultResponse {
5189 result: ToolResult::Expanded(ToolResultExpanded {
5190 text_result_for_llm: "ok".to_string(),
5191 result_type: "success".to_string(),
5192 binary_results_for_llm: None,
5193 session_log: None,
5194 error: None,
5195 tool_telemetry: None,
5196 }),
5197 };
5198
5199 let wire = serde_json::to_value(&response).unwrap();
5200
5201 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5202 assert!(wire["result"].get("binaryResultsForLlm").is_none());
5203 }
5204
5205 #[test]
5206 fn session_config_default_wire_flags_off_without_handlers() {
5207 let cfg = SessionConfig::default();
5208 assert_eq!(cfg.mcp_oauth_token_storage, None);
5209 let (wire, _runtime) = cfg
5213 .into_wire(Some(SessionId::from("default-flags")))
5214 .expect("default config has no duplicate handlers");
5215 assert!(!wire.request_user_input);
5216 assert!(!wire.request_permission);
5217 assert!(!wire.request_elicitation);
5218 assert!(!wire.request_exit_plan_mode);
5219 assert!(!wire.request_auto_mode_switch);
5220 assert!(!wire.hooks);
5221 assert!(!wire.request_mcp_apps);
5222 }
5223
5224 #[test]
5225 fn resume_session_config_new_wire_flags_off_without_handlers() {
5226 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
5227 assert_eq!(cfg.mcp_oauth_token_storage, None);
5228 let (wire, _runtime) = cfg
5229 .into_wire()
5230 .expect("default resume config has no duplicate handlers");
5231 assert!(!wire.request_user_input);
5232 assert!(!wire.request_permission);
5233 assert!(!wire.request_elicitation);
5234 assert!(!wire.request_exit_plan_mode);
5235 assert!(!wire.request_auto_mode_switch);
5236 assert!(!wire.hooks);
5237 assert!(!wire.request_mcp_apps);
5238 }
5239
5240 #[test]
5241 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5242 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
5243 assert_eq!(cfg.enable_mcp_apps, Some(true));
5244
5245 let (wire, _runtime) = cfg
5246 .into_wire(Some(SessionId::from("enable-mcp-apps")))
5247 .expect("enable_mcp_apps config has no duplicate handlers");
5248 assert!(wire.request_mcp_apps);
5249
5250 let json = serde_json::to_value(&wire).unwrap();
5251 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5252 }
5253
5254 #[test]
5255 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5256 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
5257 .with_enable_mcp_apps(true);
5258 assert_eq!(cfg.enable_mcp_apps, Some(true));
5259
5260 let (wire, _runtime) = cfg
5261 .into_wire()
5262 .expect("resume enable_mcp_apps config has no duplicate handlers");
5263 assert!(wire.request_mcp_apps);
5264
5265 let json = serde_json::to_value(&wire).unwrap();
5266 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5267 }
5268
5269 #[test]
5270 fn memory_configuration_constructors_and_serde() {
5271 assert!(MemoryConfiguration::enabled().enabled);
5272 assert!(!MemoryConfiguration::disabled().enabled);
5273 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
5274
5275 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
5276 assert_eq!(json, serde_json::json!({ "enabled": true }));
5277 }
5278
5279 #[test]
5280 fn session_config_with_memory_serializes() {
5281 let (wire, _runtime) = SessionConfig::default()
5282 .with_memory(MemoryConfiguration::enabled())
5283 .into_wire(Some(SessionId::from("memory-on")))
5284 .expect("no duplicate handlers");
5285 let json = serde_json::to_value(&wire).unwrap();
5286 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5287
5288 let (wire_off, _) = SessionConfig::default()
5289 .with_memory(MemoryConfiguration::disabled())
5290 .into_wire(Some(SessionId::from("memory-off")))
5291 .expect("no duplicate handlers");
5292 let json_off = serde_json::to_value(&wire_off).unwrap();
5293 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
5294
5295 let (empty_wire, _) = SessionConfig::default()
5297 .into_wire(Some(SessionId::from("memory-unset")))
5298 .expect("no duplicate handlers");
5299 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5300 assert!(empty_json.get("memory").is_none());
5301 }
5302
5303 #[test]
5304 fn resume_session_config_with_memory_serializes() {
5305 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
5306 .with_memory(MemoryConfiguration::enabled())
5307 .into_wire()
5308 .expect("no duplicate handlers");
5309 let json = serde_json::to_value(&wire).unwrap();
5310 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5311
5312 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
5314 .into_wire()
5315 .expect("no duplicate handlers");
5316 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5317 assert!(empty_json.get("memory").is_none());
5318 }
5319
5320 #[test]
5321 fn session_config_with_exp_assignments_serializes() {
5322 let assignments = serde_json::json!({
5323 "Parameters": { "copilot_exp_flag": "treatment" },
5324 "AssignmentContext": "ctx-123",
5325 });
5326 let (wire, _runtime) = SessionConfig::default()
5327 .with_exp_assignments(assignments.clone())
5328 .into_wire(Some(SessionId::from("exp-on")))
5329 .expect("no duplicate handlers");
5330 let json = serde_json::to_value(&wire).unwrap();
5331 assert_eq!(json["expAssignments"], assignments);
5332
5333 let (empty_wire, _) = SessionConfig::default()
5335 .into_wire(Some(SessionId::from("exp-unset")))
5336 .expect("no duplicate handlers");
5337 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5338 assert!(empty_json.get("expAssignments").is_none());
5339 }
5340
5341 #[test]
5342 fn resume_session_config_with_exp_assignments_serializes() {
5343 let assignments = serde_json::json!({
5344 "Parameters": { "copilot_exp_flag": "treatment" },
5345 "AssignmentContext": "ctx-456",
5346 });
5347 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
5348 .with_exp_assignments(assignments.clone())
5349 .into_wire()
5350 .expect("no duplicate handlers");
5351 let json = serde_json::to_value(&wire).unwrap();
5352 assert_eq!(json["expAssignments"], assignments);
5353
5354 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
5356 .into_wire()
5357 .expect("no duplicate handlers");
5358 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5359 assert!(empty_json.get("expAssignments").is_none());
5360 }
5361
5362 #[test]
5363 fn session_config_clone_preserves_exp_assignments() {
5364 let assignments = serde_json::json!({
5365 "Parameters": { "copilot_exp_flag": "treatment" },
5366 "AssignmentContext": "ctx-clone",
5367 });
5368 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
5369 let cloned = config.clone();
5370
5371 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5372
5373 let (wire, _runtime) = cloned
5374 .into_wire(Some(SessionId::from("exp-clone")))
5375 .expect("no duplicate handlers");
5376 let json = serde_json::to_value(&wire).unwrap();
5377 assert_eq!(json["expAssignments"], assignments);
5378 }
5379
5380 #[test]
5381 fn resume_session_config_clone_preserves_exp_assignments() {
5382 let assignments = serde_json::json!({
5383 "Parameters": { "copilot_exp_flag": "treatment" },
5384 "AssignmentContext": "ctx-clone-resume",
5385 });
5386 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
5387 .with_exp_assignments(assignments.clone());
5388 let cloned = config.clone();
5389
5390 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5391
5392 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
5393 let json = serde_json::to_value(&wire).unwrap();
5394 assert_eq!(json["expAssignments"], assignments);
5395 }
5396
5397 #[test]
5398 #[allow(clippy::field_reassign_with_default)]
5399 fn session_config_into_wire_serializes_bucket_b_fields() {
5400 use std::path::PathBuf;
5401
5402 use super::{CloudSessionOptions, CloudSessionRepository};
5403
5404 let mut cfg = SessionConfig::default();
5405 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5406 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5407 cfg.github_token = Some("ghs_secret".to_string());
5408 cfg.include_sub_agent_streaming_events = Some(false);
5409 cfg.enable_session_telemetry = Some(false);
5410 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
5411 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
5412 cfg.enable_on_demand_instruction_discovery = Some(false);
5413 cfg.cloud = Some(CloudSessionOptions::with_repository(
5414 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
5415 ));
5416
5417 let (wire, _runtime) = cfg
5418 .into_wire(Some(SessionId::from("custom-id")))
5419 .expect("no duplicate handlers");
5420 let wire_json = serde_json::to_value(&wire).unwrap();
5421 assert_eq!(wire_json["sessionId"], "custom-id");
5422 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5423 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5424 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5425 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
5426 assert_eq!(wire_json["enableSessionTelemetry"], false);
5427 assert_eq!(wire_json["reasoningSummary"], "concise");
5428 assert_eq!(wire_json["remoteSession"], "export");
5429 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5430 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
5431 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
5432 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
5433
5434 let (empty_wire, _) = SessionConfig::default()
5436 .into_wire(Some(SessionId::from("empty")))
5437 .expect("default has no duplicate handlers");
5438 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5439 assert!(empty_json.get("gitHubToken").is_none());
5440 assert!(empty_json.get("enableSessionTelemetry").is_none());
5441 assert!(empty_json.get("reasoningSummary").is_none());
5442 assert!(empty_json.get("remoteSession").is_none());
5443 assert!(
5444 empty_json
5445 .get("enableOnDemandInstructionDiscovery")
5446 .is_none()
5447 );
5448 assert!(empty_json.get("cloud").is_none());
5449 }
5450
5451 #[test]
5452 fn session_config_into_wire_serializes_named_providers_and_models() {
5453 let cfg = SessionConfig::default()
5454 .with_providers(vec![
5455 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
5456 .with_provider_type("openai")
5457 .with_wire_api("responses")
5458 .with_api_key("sk-test"),
5459 ])
5460 .with_models(vec![
5461 ProviderModelConfig::new("gpt-x", "my-openai")
5462 .with_wire_model("gpt-x-2025")
5463 .with_max_output_tokens(2048),
5464 ]);
5465
5466 let (wire, _) = cfg
5467 .into_wire(Some(SessionId::from("sess-providers")))
5468 .expect("no duplicate handlers");
5469 let wire_json = serde_json::to_value(&wire).unwrap();
5470 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
5471 assert_eq!(
5472 wire_json["providers"][0]["baseUrl"],
5473 "https://api.example.com/v1"
5474 );
5475 assert_eq!(wire_json["providers"][0]["type"], "openai");
5476 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
5477 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
5478 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
5479 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
5480 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
5481 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
5482
5483 let (empty_wire, _) = SessionConfig::default()
5484 .into_wire(Some(SessionId::from("empty")))
5485 .expect("default has no duplicate handlers");
5486 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5487 assert!(empty_json.get("providers").is_none());
5488 assert!(empty_json.get("models").is_none());
5489 }
5490
5491 #[test]
5492 fn resume_config_into_wire_serializes_named_providers_and_models() {
5493 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
5494 .with_providers(vec![
5495 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
5496 .with_provider_type("azure")
5497 .with_azure(AzureProviderOptions {
5498 api_version: Some("2024-10-21".to_string()),
5499 }),
5500 ])
5501 .with_models(vec![
5502 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
5503 ]);
5504
5505 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5506 let wire_json = serde_json::to_value(&wire).unwrap();
5507 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
5508 assert_eq!(wire_json["providers"][0]["type"], "azure");
5509 assert_eq!(
5510 wire_json["providers"][0]["azure"]["apiVersion"],
5511 "2024-10-21"
5512 );
5513 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
5514 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
5515 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
5516
5517 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
5518 .into_wire()
5519 .expect("default has no duplicate handlers");
5520 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5521 assert!(empty_json.get("providers").is_none());
5522 assert!(empty_json.get("models").is_none());
5523 }
5524
5525 #[test]
5526 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
5527 use std::path::PathBuf;
5528
5529 let cfg = SessionConfig {
5530 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
5531 large_output: Some(
5532 LargeToolOutputConfig::new()
5533 .with_enabled(true)
5534 .with_max_size_bytes(1024)
5535 .with_output_directory(PathBuf::from("/tmp/large-output")),
5536 ),
5537 ..Default::default()
5538 };
5539
5540 let (wire, _) = cfg
5541 .into_wire(Some(SessionId::from("sess-1")))
5542 .expect("no duplicate handlers");
5543 let wire_json = serde_json::to_value(&wire).unwrap();
5544 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
5545 assert_eq!(wire_json["largeOutput"]["enabled"], true);
5546 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
5547 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
5548
5549 let (empty_wire, _) = SessionConfig::default()
5550 .into_wire(Some(SessionId::from("empty")))
5551 .expect("default has no duplicate handlers");
5552 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5553 assert!(empty_json.get("pluginDirectories").is_none());
5554 assert!(empty_json.get("largeOutput").is_none());
5555 }
5556
5557 #[test]
5558 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
5559 use std::path::PathBuf;
5560
5561 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5562 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5563 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5564 cfg.github_token = Some("ghs_secret".to_string());
5565 cfg.include_sub_agent_streaming_events = Some(true);
5566 cfg.enable_session_telemetry = Some(false);
5567 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
5568 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
5569 cfg.enable_on_demand_instruction_discovery = Some(false);
5570
5571 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5572 let wire_json = serde_json::to_value(&wire).unwrap();
5573 assert_eq!(wire_json["sessionId"], "sess-1");
5574 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5575 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5576 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5577 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
5578 assert_eq!(wire_json["enableSessionTelemetry"], false);
5579 assert_eq!(wire_json["reasoningSummary"], "detailed");
5580 assert_eq!(wire_json["remoteSession"], "on");
5581 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5582
5583 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5585 .into_wire()
5586 .expect("default resume has no duplicate handlers");
5587 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5588 assert!(empty_json.get("reasoningSummary").is_none());
5589 assert!(empty_json.get("remoteSession").is_none());
5590 assert!(
5591 empty_json
5592 .get("enableOnDemandInstructionDiscovery")
5593 .is_none()
5594 );
5595 }
5596
5597 #[test]
5598 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
5599 use std::path::PathBuf;
5600
5601 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5602 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
5603 cfg.large_output = Some(
5604 LargeToolOutputConfig::new()
5605 .with_enabled(false)
5606 .with_max_size_bytes(2048)
5607 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
5608 );
5609
5610 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5611 let wire_json = serde_json::to_value(&wire).unwrap();
5612 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
5613 assert_eq!(wire_json["largeOutput"]["enabled"], false);
5614 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
5615 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
5616
5617 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5618 .into_wire()
5619 .expect("default resume has no duplicate handlers");
5620 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5621 assert!(empty_json.get("pluginDirectories").is_none());
5622 assert!(empty_json.get("largeOutput").is_none());
5623 }
5624
5625 #[test]
5626 fn session_config_builder_composes() {
5627 use std::collections::HashMap;
5628
5629 let cfg = SessionConfig::default()
5630 .with_session_id(SessionId::from("sess-1"))
5631 .with_model("claude-sonnet-4")
5632 .with_client_name("test-app")
5633 .with_reasoning_effort("medium")
5634 .with_reasoning_summary(ReasoningSummary::Concise)
5635 .with_context_tier("long_context")
5636 .with_streaming(true)
5637 .with_tools([Tool::new("greet")])
5638 .with_available_tools(["bash", "view"])
5639 .with_excluded_tools(["dangerous"])
5640 .with_mcp_servers(HashMap::new())
5641 .with_mcp_oauth_token_storage("persistent")
5642 .with_enable_config_discovery(true)
5643 .with_enable_on_demand_instruction_discovery(true)
5644 .with_skill_directories([PathBuf::from("/tmp/skills")])
5645 .with_disabled_skills(["broken-skill"])
5646 .with_agent("researcher")
5647 .with_config_directory(PathBuf::from("/tmp/config"))
5648 .with_working_directory(PathBuf::from("/tmp/work"))
5649 .with_github_token("ghp_test")
5650 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5651 .with_enable_session_telemetry(false)
5652 .with_include_sub_agent_streaming_events(false)
5653 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5654
5655 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
5656 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
5657 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5658 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
5659 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
5660 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
5661 assert_eq!(cfg.streaming, Some(true));
5662 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5663 assert_eq!(
5664 cfg.available_tools.as_deref(),
5665 Some(&["bash".to_string(), "view".to_string()][..])
5666 );
5667 assert_eq!(
5668 cfg.excluded_tools.as_deref(),
5669 Some(&["dangerous".to_string()][..])
5670 );
5671 assert!(cfg.mcp_servers.is_some());
5672 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5673 assert_eq!(cfg.enable_config_discovery, Some(true));
5674 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
5675 assert_eq!(
5676 cfg.skill_directories.as_deref(),
5677 Some(&[PathBuf::from("/tmp/skills")][..])
5678 );
5679 assert_eq!(
5680 cfg.disabled_skills.as_deref(),
5681 Some(&["broken-skill".to_string()][..])
5682 );
5683 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5684 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5685 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5686 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5687 assert_eq!(
5688 cfg.capi,
5689 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5690 );
5691 assert_eq!(cfg.enable_session_telemetry, Some(false));
5692 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
5693 assert_eq!(
5694 cfg.extension_info,
5695 Some(ExtensionInfo::new("github-app", "counter"))
5696 );
5697 }
5698
5699 #[test]
5700 fn resume_session_config_builder_composes() {
5701 use std::collections::HashMap;
5702
5703 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
5704 .with_client_name("test-app")
5705 .with_reasoning_summary(ReasoningSummary::None)
5706 .with_context_tier("default")
5707 .with_streaming(true)
5708 .with_tools([Tool::new("greet")])
5709 .with_available_tools(["bash", "view"])
5710 .with_excluded_tools(["dangerous"])
5711 .with_mcp_servers(HashMap::new())
5712 .with_mcp_oauth_token_storage("persistent")
5713 .with_enable_config_discovery(true)
5714 .with_enable_on_demand_instruction_discovery(false)
5715 .with_skill_directories([PathBuf::from("/tmp/skills")])
5716 .with_disabled_skills(["broken-skill"])
5717 .with_agent("researcher")
5718 .with_config_directory(PathBuf::from("/tmp/config"))
5719 .with_working_directory(PathBuf::from("/tmp/work"))
5720 .with_github_token("ghp_test")
5721 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5722 .with_enable_session_telemetry(false)
5723 .with_include_sub_agent_streaming_events(true)
5724 .with_suppress_resume_event(true)
5725 .with_continue_pending_work(true)
5726 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5727
5728 assert_eq!(cfg.session_id.as_str(), "sess-2");
5729 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5730 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
5731 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
5732 assert_eq!(cfg.streaming, Some(true));
5733 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5734 assert_eq!(
5735 cfg.available_tools.as_deref(),
5736 Some(&["bash".to_string(), "view".to_string()][..])
5737 );
5738 assert_eq!(
5739 cfg.excluded_tools.as_deref(),
5740 Some(&["dangerous".to_string()][..])
5741 );
5742 assert!(cfg.mcp_servers.is_some());
5743 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5744 assert_eq!(cfg.enable_config_discovery, Some(true));
5745 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
5746 assert_eq!(
5747 cfg.skill_directories.as_deref(),
5748 Some(&[PathBuf::from("/tmp/skills")][..])
5749 );
5750 assert_eq!(
5751 cfg.disabled_skills.as_deref(),
5752 Some(&["broken-skill".to_string()][..])
5753 );
5754 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5755 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5756 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5757 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5758 assert_eq!(
5759 cfg.capi,
5760 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5761 );
5762 assert_eq!(cfg.enable_session_telemetry, Some(false));
5763 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
5764 assert_eq!(cfg.suppress_resume_event, Some(true));
5765 assert_eq!(cfg.continue_pending_work, Some(true));
5766 assert_eq!(
5767 cfg.extension_info,
5768 Some(ExtensionInfo::new("github-app", "counter"))
5769 );
5770 }
5771
5772 #[test]
5776 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
5777 let cfg =
5778 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
5779 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5780 let json = serde_json::to_value(&wire).unwrap();
5781 assert_eq!(json["continuePendingWork"], true);
5782
5783 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5785 .into_wire()
5786 .expect("no duplicate handlers");
5787 let json = serde_json::to_value(&wire).unwrap();
5788 assert!(json.get("continuePendingWork").is_none());
5789 }
5790
5791 #[test]
5795 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
5796 let cfg =
5797 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
5798 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5799 let json = serde_json::to_value(&wire).unwrap();
5800 assert_eq!(json["disableResume"], true);
5801 assert!(json.get("suppressResumeEvent").is_none());
5802 }
5803
5804 #[test]
5807 fn session_config_serializes_instruction_directories_to_camel_case() {
5808 let cfg =
5809 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
5810 let (wire, _) = cfg
5811 .into_wire(Some(SessionId::from("instr-on")))
5812 .expect("no duplicate handlers");
5813 let json = serde_json::to_value(&wire).unwrap();
5814 assert_eq!(
5815 json["instructionDirectories"],
5816 serde_json::json!(["/tmp/instr"])
5817 );
5818
5819 let (wire, _) = SessionConfig::default()
5821 .into_wire(Some(SessionId::from("instr-off")))
5822 .expect("no duplicate handlers");
5823 let json = serde_json::to_value(&wire).unwrap();
5824 assert!(json.get("instructionDirectories").is_none());
5825 }
5826
5827 #[test]
5830 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
5831 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
5832 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
5833 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5834 let json = serde_json::to_value(&wire).unwrap();
5835 assert_eq!(
5836 json["instructionDirectories"],
5837 serde_json::json!(["/tmp/instr"])
5838 );
5839
5840 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5841 .into_wire()
5842 .expect("no duplicate handlers");
5843 let json = serde_json::to_value(&wire).unwrap();
5844 assert!(json.get("instructionDirectories").is_none());
5845 }
5846
5847 #[test]
5848 fn custom_agent_config_builder_composes() {
5849 use std::collections::HashMap;
5850
5851 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
5852 .with_display_name("Research Assistant")
5853 .with_description("Investigates technical questions.")
5854 .with_tools(["bash", "view"])
5855 .with_mcp_servers(HashMap::new())
5856 .with_infer(true)
5857 .with_skills(["rust-coding-skill"]);
5858
5859 assert_eq!(cfg.name, "researcher");
5860 assert_eq!(cfg.prompt, "You are a research assistant.");
5861 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
5862 assert_eq!(
5863 cfg.description.as_deref(),
5864 Some("Investigates technical questions.")
5865 );
5866 assert_eq!(
5867 cfg.tools.as_deref(),
5868 Some(&["bash".to_string(), "view".to_string()][..])
5869 );
5870 assert!(cfg.mcp_servers.is_some());
5871 assert_eq!(cfg.infer, Some(true));
5872 assert_eq!(
5873 cfg.skills.as_deref(),
5874 Some(&["rust-coding-skill".to_string()][..])
5875 );
5876 }
5877
5878 #[test]
5879 fn infinite_session_config_builder_composes() {
5880 let cfg = InfiniteSessionConfig::new()
5881 .with_enabled(true)
5882 .with_background_compaction_threshold(0.75)
5883 .with_buffer_exhaustion_threshold(0.92);
5884
5885 assert_eq!(cfg.enabled, Some(true));
5886 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
5887 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
5888 }
5889
5890 #[test]
5891 fn provider_config_builder_composes() {
5892 use std::collections::HashMap;
5893
5894 let mut headers = HashMap::new();
5895 headers.insert("X-Custom".to_string(), "value".to_string());
5896
5897 let cfg = ProviderConfig::new("https://api.example.com")
5898 .with_provider_type("openai")
5899 .with_wire_api("completions")
5900 .with_transport("websockets")
5901 .with_api_key("sk-test")
5902 .with_bearer_token("bearer-test")
5903 .with_headers(headers)
5904 .with_model_id("gpt-4")
5905 .with_wire_model("azure-gpt-4-deployment")
5906 .with_max_prompt_tokens(8192)
5907 .with_max_output_tokens(2048);
5908
5909 assert_eq!(cfg.base_url, "https://api.example.com");
5910 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
5911 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
5912 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
5913 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
5914 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
5915 assert_eq!(
5916 cfg.headers
5917 .as_ref()
5918 .and_then(|h| h.get("X-Custom"))
5919 .map(String::as_str),
5920 Some("value"),
5921 );
5922 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
5923 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
5924 assert_eq!(cfg.max_prompt_tokens, Some(8192));
5925 assert_eq!(cfg.max_output_tokens, Some(2048));
5926
5927 let wire = serde_json::to_value(&cfg).unwrap();
5929 assert_eq!(wire["modelId"], "gpt-4");
5930 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
5931 assert_eq!(wire["maxPromptTokens"], 8192);
5932 assert_eq!(wire["maxOutputTokens"], 2048);
5933
5934 let unset = ProviderConfig::new("https://api.example.com");
5935 let wire_unset = serde_json::to_value(&unset).unwrap();
5936 assert!(wire_unset.get("modelId").is_none());
5937 assert!(wire_unset.get("wireModel").is_none());
5938 assert!(wire_unset.get("maxPromptTokens").is_none());
5939 assert!(wire_unset.get("maxOutputTokens").is_none());
5940 }
5941
5942 #[test]
5943 fn capi_session_options_builder_composes_and_serializes() {
5944 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
5945
5946 assert_eq!(cfg.enable_web_socket_responses, Some(false));
5947
5948 let wire = serde_json::to_value(&cfg).unwrap();
5949 assert_eq!(
5950 wire,
5951 serde_json::json!({ "enableWebSocketResponses": false })
5952 );
5953
5954 let unset = CapiSessionOptions::new();
5955 let wire_unset = serde_json::to_value(&unset).unwrap();
5956 assert!(wire_unset.get("enableWebSocketResponses").is_none());
5957 }
5958
5959 #[test]
5960 fn session_config_with_capi_serializes() {
5961 let (wire, _) = SessionConfig::default()
5962 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5963 .into_wire(Some(SessionId::from("capi-create")))
5964 .expect("no duplicate handlers");
5965 let json = serde_json::to_value(&wire).unwrap();
5966 assert_eq!(
5967 json["capi"],
5968 serde_json::json!({ "enableWebSocketResponses": false })
5969 );
5970
5971 let (empty_wire, _) = SessionConfig::default()
5972 .into_wire(Some(SessionId::from("capi-create-unset")))
5973 .expect("no duplicate handlers");
5974 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5975 assert!(empty_json.get("capi").is_none());
5976 }
5977
5978 #[test]
5979 fn resume_session_config_with_capi_serializes() {
5980 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
5981 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5982 .into_wire()
5983 .expect("no duplicate handlers");
5984 let json = serde_json::to_value(&wire).unwrap();
5985 assert_eq!(
5986 json["capi"],
5987 serde_json::json!({ "enableWebSocketResponses": false })
5988 );
5989
5990 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
5991 .into_wire()
5992 .expect("no duplicate handlers");
5993 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5994 assert!(empty_json.get("capi").is_none());
5995 }
5996
5997 #[test]
5998 fn system_message_config_builder_composes() {
5999 use std::collections::HashMap;
6000
6001 let cfg = SystemMessageConfig::new()
6002 .with_mode("replace")
6003 .with_content("Custom system message.")
6004 .with_sections(HashMap::new());
6005
6006 assert_eq!(cfg.mode.as_deref(), Some("replace"));
6007 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
6008 assert!(cfg.sections.is_some());
6009 }
6010
6011 #[test]
6012 fn delivery_mode_serializes_to_kebab_case_strings() {
6013 assert_eq!(
6014 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
6015 "\"enqueue\""
6016 );
6017 assert_eq!(
6018 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
6019 "\"immediate\""
6020 );
6021 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
6022 assert_eq!(parsed, DeliveryMode::Immediate);
6023 }
6024
6025 #[test]
6026 fn agent_mode_serializes_to_kebab_case_strings() {
6027 assert_eq!(
6028 serde_json::to_string(&AgentMode::Interactive).unwrap(),
6029 "\"interactive\""
6030 );
6031 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
6032 assert_eq!(
6033 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
6034 "\"autopilot\""
6035 );
6036 assert_eq!(
6037 serde_json::to_string(&AgentMode::Shell).unwrap(),
6038 "\"shell\""
6039 );
6040 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
6041 assert_eq!(parsed, AgentMode::Plan);
6042 }
6043
6044 #[test]
6045 fn connection_state_distinguishes_variants() {
6046 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
6049 }
6050
6051 #[test]
6057 fn session_event_round_trips_agent_id_on_envelope() {
6058 let wire = json!({
6059 "id": "evt-1",
6060 "timestamp": "2026-04-30T12:00:00Z",
6061 "parentId": null,
6062 "agentId": "sub-agent-42",
6063 "type": "assistant.message",
6064 "data": { "message": "hi" }
6065 });
6066
6067 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
6068 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6069
6070 let roundtripped = serde_json::to_value(&event).unwrap();
6072 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6073
6074 let main_agent_event: SessionEvent = serde_json::from_value(json!({
6076 "id": "evt-2",
6077 "timestamp": "2026-04-30T12:00:01Z",
6078 "parentId": null,
6079 "type": "session.idle",
6080 "data": {}
6081 }))
6082 .unwrap();
6083 assert!(main_agent_event.agent_id.is_none());
6084 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
6085 assert!(roundtripped.get("agentId").is_none());
6086 }
6087
6088 #[test]
6090 fn typed_session_event_round_trips_agent_id_on_envelope() {
6091 let wire = json!({
6092 "id": "evt-1",
6093 "timestamp": "2026-04-30T12:00:00Z",
6094 "parentId": null,
6095 "agentId": "sub-agent-42",
6096 "type": "session.idle",
6097 "data": {}
6098 });
6099
6100 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
6101 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6102
6103 let roundtripped = serde_json::to_value(&event).unwrap();
6104 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6105 }
6106
6107 #[test]
6108 fn connection_state_variants_compile() {
6109 let _ = ConnectionState::Disconnected;
6113 let _ = ConnectionState::Connecting;
6114 let _ = ConnectionState::Connected;
6115 let _ = ConnectionState::Error;
6116 }
6117
6118 #[test]
6119 fn deserializes_runtime_attachment_variants() {
6120 let attachments: Vec<Attachment> = serde_json::from_value(json!([
6121 {
6122 "type": "file",
6123 "path": "/tmp/file.rs",
6124 "displayName": "file.rs",
6125 "lineRange": { "start": 7, "end": 12 }
6126 },
6127 {
6128 "type": "directory",
6129 "path": "/tmp/project",
6130 "displayName": "project"
6131 },
6132 {
6133 "type": "selection",
6134 "filePath": "/tmp/lib.rs",
6135 "displayName": "lib.rs",
6136 "text": "fn main() {}",
6137 "selection": {
6138 "start": { "line": 1, "character": 2 },
6139 "end": { "line": 3, "character": 4 }
6140 }
6141 },
6142 {
6143 "type": "blob",
6144 "data": "Zm9v",
6145 "mimeType": "image/png",
6146 "displayName": "image.png"
6147 },
6148 {
6149 "type": "github_reference",
6150 "number": 42,
6151 "title": "Fix rendering",
6152 "referenceType": "issue",
6153 "state": "open",
6154 "url": "https://github.com/example/repo/issues/42"
6155 }
6156 ]))
6157 .expect("attachments should deserialize");
6158
6159 assert_eq!(attachments.len(), 5);
6160 assert!(matches!(
6161 &attachments[0],
6162 Attachment::File {
6163 path,
6164 display_name,
6165 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
6166 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
6167 ));
6168 assert!(matches!(
6169 &attachments[1],
6170 Attachment::Directory { path, display_name }
6171 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
6172 ));
6173 assert!(matches!(
6174 &attachments[2],
6175 Attachment::Selection {
6176 file_path,
6177 display_name,
6178 selection:
6179 AttachmentSelectionRange {
6180 start: AttachmentSelectionPosition { line: 1, character: 2 },
6181 end: AttachmentSelectionPosition { line: 3, character: 4 },
6182 },
6183 ..
6184 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
6185 ));
6186 assert!(matches!(
6187 &attachments[3],
6188 Attachment::Blob {
6189 data,
6190 mime_type,
6191 display_name,
6192 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
6193 ));
6194 assert!(matches!(
6195 &attachments[4],
6196 Attachment::GitHubReference {
6197 number: 42,
6198 title,
6199 reference_type: GitHubReferenceType::Issue,
6200 state,
6201 url,
6202 } if title == "Fix rendering"
6203 && state == "open"
6204 && url == "https://github.com/example/repo/issues/42"
6205 ));
6206 }
6207
6208 #[test]
6209 fn ensures_display_names_for_variants_that_support_them() {
6210 let mut attachments = vec![
6211 Attachment::File {
6212 path: PathBuf::from("/tmp/file.rs"),
6213 display_name: None,
6214 line_range: None,
6215 },
6216 Attachment::Selection {
6217 file_path: PathBuf::from("/tmp/src/lib.rs"),
6218 display_name: None,
6219 text: "fn main() {}".to_string(),
6220 selection: AttachmentSelectionRange {
6221 start: AttachmentSelectionPosition {
6222 line: 0,
6223 character: 0,
6224 },
6225 end: AttachmentSelectionPosition {
6226 line: 0,
6227 character: 10,
6228 },
6229 },
6230 },
6231 Attachment::Blob {
6232 data: "Zm9v".to_string(),
6233 mime_type: "image/png".to_string(),
6234 display_name: None,
6235 },
6236 Attachment::GitHubReference {
6237 number: 7,
6238 title: "Track regressions".to_string(),
6239 reference_type: GitHubReferenceType::Issue,
6240 state: "open".to_string(),
6241 url: "https://example.com/issues/7".to_string(),
6242 },
6243 ];
6244
6245 ensure_attachment_display_names(&mut attachments);
6246
6247 assert_eq!(attachments[0].display_name(), Some("file.rs"));
6248 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
6249 assert_eq!(attachments[2].display_name(), Some("attachment"));
6250 assert_eq!(attachments[3].display_name(), None);
6251 assert_eq!(
6252 attachments[3].label(),
6253 Some("Track regressions".to_string())
6254 );
6255 }
6256
6257 #[test]
6258 fn github_anchored_attachment_variants_round_trip() {
6259 let cases = vec![
6260 (
6261 "github_commit",
6262 json!({
6263 "type": "github_commit",
6264 "message": "Fix the thing",
6265 "oid": "abc123",
6266 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
6267 "url": "https://github.com/octocat/repo/commit/abc123"
6268 }),
6269 ),
6270 (
6271 "github_release",
6272 json!({
6273 "type": "github_release",
6274 "name": "v1.2.3",
6275 "repo": { "name": "repo", "owner": "octocat" },
6276 "tagName": "v1.2.3",
6277 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
6278 }),
6279 ),
6280 (
6281 "github_actions_job",
6282 json!({
6283 "type": "github_actions_job",
6284 "conclusion": "failure",
6285 "jobId": 99,
6286 "jobName": "build",
6287 "repo": { "name": "repo", "owner": "octocat" },
6288 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
6289 "workflowName": "CI"
6290 }),
6291 ),
6292 (
6293 "github_repository",
6294 json!({
6295 "type": "github_repository",
6296 "description": "An example repository",
6297 "ref": "main",
6298 "repo": { "name": "repo", "owner": "octocat" },
6299 "url": "https://github.com/octocat/repo"
6300 }),
6301 ),
6302 (
6303 "github_file_diff",
6304 json!({
6305 "type": "github_file_diff",
6306 "base": {
6307 "path": "src/lib.rs",
6308 "ref": "main",
6309 "repo": { "name": "repo", "owner": "octocat" }
6310 },
6311 "head": {
6312 "path": "src/lib.rs",
6313 "ref": "feature",
6314 "repo": { "name": "repo", "owner": "octocat" }
6315 },
6316 "url": "https://github.com/octocat/repo/compare/main...feature"
6317 }),
6318 ),
6319 (
6320 "github_tree_comparison",
6321 json!({
6322 "type": "github_tree_comparison",
6323 "base": {
6324 "repo": { "name": "repo", "owner": "octocat" },
6325 "revision": "main"
6326 },
6327 "head": {
6328 "repo": { "name": "repo", "owner": "octocat" },
6329 "revision": "feature"
6330 },
6331 "url": "https://github.com/octocat/repo/compare/main...feature"
6332 }),
6333 ),
6334 (
6335 "github_url",
6336 json!({
6337 "type": "github_url",
6338 "url": "https://github.com/octocat/repo/wiki"
6339 }),
6340 ),
6341 (
6342 "github_file",
6343 json!({
6344 "type": "github_file",
6345 "path": "src/main.rs",
6346 "ref": "main",
6347 "repo": { "name": "repo", "owner": "octocat" },
6348 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
6349 }),
6350 ),
6351 (
6352 "github_snippet",
6353 json!({
6354 "type": "github_snippet",
6355 "lineRange": { "start": 10, "end": 20 },
6356 "path": "src/main.rs",
6357 "ref": "main",
6358 "repo": { "name": "repo", "owner": "octocat" },
6359 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
6360 }),
6361 ),
6362 ];
6363
6364 for (expected_type, input) in cases {
6365 let attachment: Attachment = serde_json::from_value(input.clone())
6366 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
6367
6368 let serialized_string = serde_json::to_string(&attachment)
6373 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
6374
6375 assert_eq!(
6377 serialized_string.matches("\"type\":").count(),
6378 1,
6379 "{expected_type} must serialize a single `type` key"
6380 );
6381
6382 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
6383 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
6384 assert_eq!(
6385 serialized.get("type").and_then(|value| value.as_str()),
6386 Some(expected_type),
6387 "{expected_type} must serialize the correct discriminator"
6388 );
6389
6390 assert_eq!(
6392 serialized, input,
6393 "{expected_type} should round-trip without data loss"
6394 );
6395 let reparsed: Attachment = serde_json::from_value(serialized)
6396 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
6397 assert_eq!(
6398 reparsed, attachment,
6399 "{expected_type} should re-deserialize to the same value"
6400 );
6401 }
6402 }
6403}
6404
6405#[cfg(test)]
6406mod permission_builder_tests {
6407 use std::sync::Arc;
6408
6409 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
6410 use crate::permission;
6411 use crate::types::{
6412 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
6413 SessionId,
6414 };
6415
6416 fn data() -> PermissionRequestData {
6417 PermissionRequestData {
6418 extra: serde_json::json!({"tool": "shell"}),
6419 ..Default::default()
6420 }
6421 }
6422
6423 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6426 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6427 }
6428
6429 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6430 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6431 }
6432
6433 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
6434 handler
6435 .handle(SessionId::from("s1"), RequestId::new("1"), data())
6436 .await
6437 }
6438
6439 #[tokio::test]
6440 async fn approve_all_with_handler_present_approves() {
6441 let cfg = SessionConfig::default()
6442 .with_permission_handler(Arc::new(ApproveAllHandler))
6443 .approve_all_permissions();
6444 let h = resolve_create(cfg).expect("policy + handler yields handler");
6445 assert!(matches!(
6446 dispatch(&h).await,
6447 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6448 ));
6449 }
6450
6451 #[tokio::test]
6452 async fn approve_all_standalone_produces_handler() {
6453 let cfg = SessionConfig::default().approve_all_permissions();
6454 let h = resolve_create(cfg).expect("policy alone yields handler");
6455 assert!(matches!(
6456 dispatch(&h).await,
6457 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6458 ));
6459 }
6460
6461 #[tokio::test]
6464 async fn approve_all_is_order_independent() {
6465 let a = SessionConfig::default()
6466 .with_permission_handler(Arc::new(ApproveAllHandler))
6467 .approve_all_permissions();
6468 let b = SessionConfig::default()
6469 .approve_all_permissions()
6470 .with_permission_handler(Arc::new(ApproveAllHandler));
6471 let ha = resolve_create(a).unwrap();
6472 let hb = resolve_create(b).unwrap();
6473 assert!(matches!(
6474 dispatch(&ha).await,
6475 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6476 ));
6477 assert!(matches!(
6478 dispatch(&hb).await,
6479 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6480 ));
6481 }
6482
6483 #[tokio::test]
6484 async fn deny_all_is_order_independent() {
6485 let a = SessionConfig::default()
6486 .with_permission_handler(Arc::new(ApproveAllHandler))
6487 .deny_all_permissions();
6488 let b = SessionConfig::default()
6489 .deny_all_permissions()
6490 .with_permission_handler(Arc::new(ApproveAllHandler));
6491 let ha = resolve_create(a).unwrap();
6492 let hb = resolve_create(b).unwrap();
6493 assert!(matches!(
6494 dispatch(&ha).await,
6495 PermissionResult::Decision(PermissionDecision::Reject(_))
6496 ));
6497 assert!(matches!(
6498 dispatch(&hb).await,
6499 PermissionResult::Decision(PermissionDecision::Reject(_))
6500 ));
6501 }
6502
6503 #[tokio::test]
6504 async fn approve_permissions_if_consults_predicate() {
6505 let cfg = SessionConfig::default().approve_permissions_if(|d| {
6506 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6507 });
6508 let h = resolve_create(cfg).unwrap();
6509 assert!(matches!(
6510 dispatch(&h).await,
6511 PermissionResult::Decision(PermissionDecision::Reject(_))
6512 ));
6513 }
6514
6515 #[tokio::test]
6516 async fn approve_permissions_if_is_order_independent() {
6517 let predicate = |d: &PermissionRequestData| {
6518 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6519 };
6520 let a = SessionConfig::default()
6521 .with_permission_handler(Arc::new(ApproveAllHandler))
6522 .approve_permissions_if(predicate);
6523 let b = SessionConfig::default()
6524 .approve_permissions_if(predicate)
6525 .with_permission_handler(Arc::new(ApproveAllHandler));
6526 let ha = resolve_create(a).unwrap();
6527 let hb = resolve_create(b).unwrap();
6528 assert!(matches!(
6529 dispatch(&ha).await,
6530 PermissionResult::Decision(PermissionDecision::Reject(_))
6531 ));
6532 assert!(matches!(
6533 dispatch(&hb).await,
6534 PermissionResult::Decision(PermissionDecision::Reject(_))
6535 ));
6536 }
6537
6538 #[tokio::test]
6539 async fn resume_session_config_approve_all_works() {
6540 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
6541 .with_permission_handler(Arc::new(ApproveAllHandler))
6542 .approve_all_permissions();
6543 let h = resolve_resume(cfg).unwrap();
6544 assert!(matches!(
6545 dispatch(&h).await,
6546 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6547 ));
6548 }
6549
6550 #[tokio::test]
6551 async fn resume_session_config_approve_all_is_order_independent() {
6552 let a = ResumeSessionConfig::new(SessionId::from("s1"))
6553 .with_permission_handler(Arc::new(ApproveAllHandler))
6554 .approve_all_permissions();
6555 let b = ResumeSessionConfig::new(SessionId::from("s1"))
6556 .approve_all_permissions()
6557 .with_permission_handler(Arc::new(ApproveAllHandler));
6558 let ha = resolve_resume(a).unwrap();
6559 let hb = resolve_resume(b).unwrap();
6560 assert!(matches!(
6561 dispatch(&ha).await,
6562 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6563 ));
6564 assert!(matches!(
6565 dispatch(&hb).await,
6566 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6567 ));
6568 }
6569}