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, PermissionHandler,
28 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 get_bearer_token: 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 "get_bearer_token",
1101 &self.get_bearer_token.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_get_bearer_token(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1162 self.get_bearer_token = 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 get_bearer_token: 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 "get_bearer_token",
1316 &self.get_bearer_token.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_get_bearer_token(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1367 self.get_bearer_token = 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.get_bearer_token.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.get_bearer_token.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 user_input_handler: Option<Arc<dyn UserInputHandler>>,
1779 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
1782 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
1785 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1789 pub(crate) permission_policy: Option<crate::permission::Policy>,
1793 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
1798 pub skip_custom_instructions: Option<bool>,
1802 pub custom_agents_local_only: Option<bool>,
1806 pub coauthor_enabled: Option<bool>,
1810 pub manage_schedule_enabled: Option<bool>,
1814}
1815
1816impl std::fmt::Debug for SessionConfig {
1817 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1818 f.debug_struct("SessionConfig")
1819 .field("session_id", &self.session_id)
1820 .field("model", &self.model)
1821 .field("client_name", &self.client_name)
1822 .field("reasoning_effort", &self.reasoning_effort)
1823 .field("reasoning_summary", &self.reasoning_summary)
1824 .field("context_tier", &self.context_tier)
1825 .field("streaming", &self.streaming)
1826 .field("system_message", &self.system_message)
1827 .field("tools", &self.tools)
1828 .field("canvases", &self.canvases)
1829 .field(
1830 "canvas_handler",
1831 &self.canvas_handler.as_ref().map(|_| "<set>"),
1832 )
1833 .field("request_canvas_renderer", &self.request_canvas_renderer)
1834 .field("request_extensions", &self.request_extensions)
1835 .field("extension_sdk_path", &self.extension_sdk_path)
1836 .field("extension_info", &self.extension_info)
1837 .field("available_tools", &self.available_tools)
1838 .field("excluded_tools", &self.excluded_tools)
1839 .field("mcp_servers", &self.mcp_servers)
1840 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
1841 .field("embedding_cache_storage", &self.embedding_cache_storage)
1842 .field("enable_config_discovery", &self.enable_config_discovery)
1843 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
1844 .field(
1845 "organization_custom_instructions",
1846 &self
1847 .organization_custom_instructions
1848 .as_ref()
1849 .map(|_| "<redacted>"),
1850 )
1851 .field(
1852 "enable_on_demand_instruction_discovery",
1853 &self.enable_on_demand_instruction_discovery,
1854 )
1855 .field("enable_file_hooks", &self.enable_file_hooks)
1856 .field(
1857 "enable_host_git_operations",
1858 &self.enable_host_git_operations,
1859 )
1860 .field("enable_session_store", &self.enable_session_store)
1861 .field("enable_skills", &self.enable_skills)
1862 .field("enable_mcp_apps", &self.enable_mcp_apps)
1863 .field("skill_directories", &self.skill_directories)
1864 .field("instruction_directories", &self.instruction_directories)
1865 .field("plugin_directories", &self.plugin_directories)
1866 .field("large_output", &self.large_output)
1867 .field("disabled_skills", &self.disabled_skills)
1868 .field("hooks", &self.hooks)
1869 .field("custom_agents", &self.custom_agents)
1870 .field("default_agent", &self.default_agent)
1871 .field("agent", &self.agent)
1872 .field("infinite_sessions", &self.infinite_sessions)
1873 .field("provider", &self.provider)
1874 .field("capi", &self.capi)
1875 .field("enable_session_telemetry", &self.enable_session_telemetry)
1876 .field("model_capabilities", &self.model_capabilities)
1877 .field("memory", &self.memory)
1878 .field("config_directory", &self.config_directory)
1879 .field("working_directory", &self.working_directory)
1880 .field(
1881 "github_token",
1882 &self.github_token.as_ref().map(|_| "<redacted>"),
1883 )
1884 .field("remote_session", &self.remote_session)
1885 .field("cloud", &self.cloud)
1886 .field(
1887 "include_sub_agent_streaming_events",
1888 &self.include_sub_agent_streaming_events,
1889 )
1890 .field("commands", &self.commands)
1891 .field("exp_assignments", &self.exp_assignments)
1892 .field(
1893 "session_fs_provider",
1894 &self.session_fs_provider.as_ref().map(|_| "<set>"),
1895 )
1896 .field(
1897 "permission_handler",
1898 &self.permission_handler.as_ref().map(|_| "<set>"),
1899 )
1900 .field(
1901 "elicitation_handler",
1902 &self.elicitation_handler.as_ref().map(|_| "<set>"),
1903 )
1904 .field(
1905 "user_input_handler",
1906 &self.user_input_handler.as_ref().map(|_| "<set>"),
1907 )
1908 .field(
1909 "exit_plan_mode_handler",
1910 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
1911 )
1912 .field(
1913 "auto_mode_switch_handler",
1914 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
1915 )
1916 .field(
1917 "hooks_handler",
1918 &self.hooks_handler.as_ref().map(|_| "<set>"),
1919 )
1920 .field(
1921 "system_message_transform",
1922 &self.system_message_transform.as_ref().map(|_| "<set>"),
1923 )
1924 .finish()
1925 }
1926}
1927
1928impl Default for SessionConfig {
1929 fn default() -> Self {
1935 Self {
1936 session_id: None,
1937 model: None,
1938 client_name: None,
1939 reasoning_effort: None,
1940 reasoning_summary: None,
1941 context_tier: None,
1942 streaming: None,
1943 system_message: None,
1944 tools: None,
1945 canvases: None,
1946 canvas_handler: None,
1947 request_canvas_renderer: None,
1948 request_extensions: None,
1949 extension_sdk_path: None,
1950 extension_info: None,
1951 available_tools: None,
1952 excluded_tools: None,
1953 mcp_servers: None,
1954 mcp_oauth_token_storage: None,
1955 enable_config_discovery: None,
1956 skip_embedding_retrieval: None,
1957 organization_custom_instructions: None,
1958 enable_on_demand_instruction_discovery: None,
1959 enable_file_hooks: None,
1960 enable_host_git_operations: None,
1961 enable_session_store: None,
1962 enable_skills: None,
1963 embedding_cache_storage: None,
1964 enable_mcp_apps: None,
1965 skill_directories: None,
1966 instruction_directories: None,
1967 plugin_directories: None,
1968 large_output: None,
1969 disabled_skills: None,
1970 hooks: None,
1971 custom_agents: None,
1972 default_agent: None,
1973 agent: None,
1974 infinite_sessions: None,
1975 provider: None,
1976 capi: None,
1977 providers: None,
1978 models: None,
1979 enable_session_telemetry: None,
1980 model_capabilities: None,
1981 memory: None,
1982 config_directory: None,
1983 working_directory: None,
1984 github_token: None,
1985 remote_session: None,
1986 cloud: None,
1987 include_sub_agent_streaming_events: None,
1988 commands: None,
1989 exp_assignments: None,
1990 session_fs_provider: None,
1991 permission_handler: None,
1992 elicitation_handler: None,
1993 user_input_handler: None,
1994 exit_plan_mode_handler: None,
1995 auto_mode_switch_handler: None,
1996 hooks_handler: None,
1997 permission_policy: None,
1998 system_message_transform: None,
1999 skip_custom_instructions: None,
2000 custom_agents_local_only: None,
2001 coauthor_enabled: None,
2002 manage_schedule_enabled: None,
2003 }
2004 }
2005}
2006
2007pub(crate) struct SessionConfigRuntime {
2013 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2014 pub permission_policy: Option<crate::permission::Policy>,
2015 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2016 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2017 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2018 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2019 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2020 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2021 pub tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>>,
2022 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2023 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2024 pub bearer_token_providers: HashMap<String, Arc<dyn BearerTokenProvider>>,
2025 pub commands: Option<Vec<CommandDefinition>>,
2026}
2027
2028impl SessionConfig {
2029 pub(crate) fn into_wire(
2041 mut self,
2042 session_id: Option<SessionId>,
2043 ) -> Result<(crate::wire::SessionCreateWire, SessionConfigRuntime), crate::Error> {
2044 let permission_active =
2045 self.permission_handler.is_some() || self.permission_policy.is_some();
2046 let request_user_input = self.user_input_handler.is_some();
2047 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
2048 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
2049 let request_elicitation = self.elicitation_handler.is_some();
2050 let hooks_flag = self.hooks_handler.is_some();
2051
2052 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
2053 if let Some(tools) = self.tools.as_mut() {
2054 for tool in tools.iter_mut() {
2055 if let Some(handler) = tool.handler.take()
2056 && tool_handlers.insert(tool.name.clone(), handler).is_some()
2057 {
2058 return Err(crate::Error::with_message(
2059 crate::ErrorKind::InvalidConfig,
2060 format!("duplicate tool handler registered for name {:?}", tool.name),
2061 ));
2062 }
2063 }
2064 }
2065
2066 let wire_commands = self.commands.as_ref().map(|cmds| {
2067 cmds.iter()
2068 .map(|c| crate::wire::CommandWireDefinition {
2069 name: c.name.clone(),
2070 description: c.description.clone(),
2071 })
2072 .collect()
2073 });
2074 let wire_canvases = self.canvases.clone();
2075 let canvas_handler = self.canvas_handler.clone();
2076 let bearer_token_providers =
2077 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
2078
2079 let wire = crate::wire::SessionCreateWire {
2080 session_id,
2081 model: self.model,
2082 client_name: self.client_name,
2083 reasoning_effort: self.reasoning_effort,
2084 reasoning_summary: self.reasoning_summary,
2085 context_tier: self.context_tier,
2086 streaming: self.streaming,
2087 system_message: self.system_message,
2088 tools: self.tools,
2089 canvases: wire_canvases,
2090 request_canvas_renderer: self.request_canvas_renderer,
2091 request_extensions: self.request_extensions,
2092 extension_sdk_path: self.extension_sdk_path,
2093 extension_info: self.extension_info,
2094 available_tools: self.available_tools,
2095 excluded_tools: self.excluded_tools,
2096 tool_filter_precedence: "excluded",
2097 mcp_servers: self.mcp_servers,
2098 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
2099 embedding_cache_storage: self.embedding_cache_storage,
2100 env_value_mode: "direct",
2101 enable_config_discovery: self.enable_config_discovery,
2102 skip_embedding_retrieval: self.skip_embedding_retrieval,
2103 organization_custom_instructions: self.organization_custom_instructions,
2104 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
2105 enable_file_hooks: self.enable_file_hooks,
2106 enable_host_git_operations: self.enable_host_git_operations,
2107 enable_session_store: self.enable_session_store,
2108 enable_skills: self.enable_skills,
2109 request_user_input,
2110 request_permission: permission_active,
2111 request_exit_plan_mode,
2112 request_auto_mode_switch,
2113 request_elicitation,
2114 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
2115 hooks: hooks_flag,
2116 skill_directories: self.skill_directories,
2117 instruction_directories: self.instruction_directories,
2118 plugin_directories: self.plugin_directories,
2119 large_output: self.large_output,
2120 disabled_skills: self.disabled_skills,
2121 custom_agents: self.custom_agents,
2122 default_agent: self.default_agent,
2123 agent: self.agent,
2124 infinite_sessions: self.infinite_sessions,
2125 provider: self.provider,
2126 capi: self.capi,
2127 providers: self.providers,
2128 models: self.models,
2129 enable_session_telemetry: self.enable_session_telemetry,
2130 model_capabilities: self.model_capabilities,
2131 memory: self.memory,
2132 config_dir: self.config_directory,
2133 working_directory: self.working_directory,
2134 github_token: self.github_token,
2135 remote_session: self.remote_session,
2136 cloud: self.cloud,
2137 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
2138 commands: wire_commands,
2139 exp_assignments: self.exp_assignments,
2140 };
2141
2142 let runtime = SessionConfigRuntime {
2143 permission_handler: self.permission_handler,
2144 permission_policy: self.permission_policy,
2145 elicitation_handler: self.elicitation_handler,
2146 user_input_handler: self.user_input_handler,
2147 exit_plan_mode_handler: self.exit_plan_mode_handler,
2148 auto_mode_switch_handler: self.auto_mode_switch_handler,
2149 hooks_handler: self.hooks_handler,
2150 system_message_transform: self.system_message_transform,
2151 tool_handlers,
2152 canvas_handler,
2153 session_fs_provider: self.session_fs_provider,
2154 bearer_token_providers,
2155 commands: self.commands,
2156 };
2157
2158 Ok((wire, runtime))
2159 }
2160
2161 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
2165 self.permission_handler = Some(handler);
2166 self
2167 }
2168
2169 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
2172 self.elicitation_handler = Some(handler);
2173 self
2174 }
2175
2176 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
2179 self.user_input_handler = Some(handler);
2180 self
2181 }
2182
2183 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
2185 self.exit_plan_mode_handler = Some(handler);
2186 self
2187 }
2188
2189 pub fn with_auto_mode_switch_handler(
2191 mut self,
2192 handler: Arc<dyn AutoModeSwitchHandler>,
2193 ) -> Self {
2194 self.auto_mode_switch_handler = Some(handler);
2195 self
2196 }
2197
2198 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
2203 self.commands = Some(commands);
2204 self
2205 }
2206
2207 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
2211 self.session_fs_provider = Some(provider);
2212 self
2213 }
2214
2215 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
2218 self.hooks_handler = Some(hooks);
2219 self
2220 }
2221
2222 pub fn with_system_message_transform(
2226 mut self,
2227 transform: Arc<dyn SystemMessageTransform>,
2228 ) -> Self {
2229 self.system_message_transform = Some(transform);
2230 self
2231 }
2232
2233 pub fn approve_all_permissions(mut self) -> Self {
2239 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
2240 self
2241 }
2242
2243 pub fn deny_all_permissions(mut self) -> Self {
2246 self.permission_policy = Some(crate::permission::Policy::DenyAll);
2247 self
2248 }
2249
2250 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
2255 where
2256 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
2257 {
2258 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
2259 self
2260 }
2261
2262 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
2264 self.session_id = Some(id.into());
2265 self
2266 }
2267
2268 pub fn with_model(mut self, model: impl Into<String>) -> Self {
2270 self.model = Some(model.into());
2271 self
2272 }
2273
2274 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
2276 self.client_name = Some(name.into());
2277 self
2278 }
2279
2280 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2282 self.reasoning_effort = Some(effort.into());
2283 self
2284 }
2285
2286 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
2288 self.reasoning_summary = Some(summary);
2289 self
2290 }
2291
2292 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
2294 self.context_tier = Some(tier.into());
2295 self
2296 }
2297
2298 pub fn with_streaming(mut self, streaming: bool) -> Self {
2300 self.streaming = Some(streaming);
2301 self
2302 }
2303
2304 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
2306 self.system_message = Some(system_message);
2307 self
2308 }
2309
2310 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
2312 self.tools = Some(tools.into_iter().collect());
2313 self
2314 }
2315
2316 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
2321 self.canvases = Some(canvases.into_iter().collect());
2322 self
2323 }
2324
2325 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
2327 self.canvas_handler = Some(handler);
2328 self
2329 }
2330
2331 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
2333 self.request_canvas_renderer = Some(request);
2334 self
2335 }
2336
2337 pub fn with_request_extensions(mut self, request: bool) -> Self {
2339 self.request_extensions = Some(request);
2340 self
2341 }
2342
2343 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
2347 self.extension_sdk_path = Some(path.into());
2348 self
2349 }
2350
2351 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
2353 self.extension_info = Some(extension_info);
2354 self
2355 }
2356
2357 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
2359 where
2360 I: IntoIterator<Item = S>,
2361 S: Into<String>,
2362 {
2363 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
2364 self
2365 }
2366
2367 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
2369 where
2370 I: IntoIterator<Item = S>,
2371 S: Into<String>,
2372 {
2373 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2374 self
2375 }
2376
2377 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
2379 self.mcp_servers = Some(servers);
2380 self
2381 }
2382
2383 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
2391 self.mcp_oauth_token_storage = Some(mode.into());
2392 self
2393 }
2394
2395 pub fn with_embedding_cache_storage(
2397 mut self,
2398 embedding_cache_storage: impl Into<String>,
2399 ) -> Self {
2400 self.embedding_cache_storage = Some(embedding_cache_storage.into());
2401 self
2402 }
2403
2404 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2406 self.enable_config_discovery = Some(enable);
2407 self
2408 }
2409
2410 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
2412 self.skip_embedding_retrieval = Some(value);
2413 self
2414 }
2415
2416 pub fn with_organization_custom_instructions(
2418 mut self,
2419 instructions: impl Into<String>,
2420 ) -> Self {
2421 self.organization_custom_instructions = Some(instructions.into());
2422 self
2423 }
2424
2425 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
2427 self.enable_on_demand_instruction_discovery = Some(value);
2428 self
2429 }
2430
2431 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
2433 self.enable_file_hooks = Some(value);
2434 self
2435 }
2436
2437 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
2439 self.enable_host_git_operations = Some(value);
2440 self
2441 }
2442
2443 pub fn with_enable_session_store(mut self, value: bool) -> Self {
2445 self.enable_session_store = Some(value);
2446 self
2447 }
2448
2449 pub fn with_enable_skills(mut self, value: bool) -> Self {
2451 self.enable_skills = Some(value);
2452 self
2453 }
2454
2455 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
2461 self.enable_mcp_apps = Some(enable);
2462 self
2463 }
2464
2465 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2467 where
2468 I: IntoIterator<Item = P>,
2469 P: Into<PathBuf>,
2470 {
2471 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2472 self
2473 }
2474
2475 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2479 where
2480 I: IntoIterator<Item = P>,
2481 P: Into<PathBuf>,
2482 {
2483 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2484 self
2485 }
2486
2487 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
2489 where
2490 I: IntoIterator<Item = P>,
2491 P: Into<PathBuf>,
2492 {
2493 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
2494 self
2495 }
2496
2497 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
2499 self.large_output = Some(config);
2500 self
2501 }
2502
2503 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2505 where
2506 I: IntoIterator<Item = S>,
2507 S: Into<String>,
2508 {
2509 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2510 self
2511 }
2512
2513 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2515 mut self,
2516 agents: I,
2517 ) -> Self {
2518 self.custom_agents = Some(agents.into_iter().collect());
2519 self
2520 }
2521
2522 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2524 self.default_agent = Some(agent);
2525 self
2526 }
2527
2528 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2531 self.agent = Some(name.into());
2532 self
2533 }
2534
2535 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2538 self.infinite_sessions = Some(config);
2539 self
2540 }
2541
2542 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2544 self.provider = Some(provider);
2545 self
2546 }
2547
2548 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
2550 self.capi = Some(capi);
2551 self
2552 }
2553
2554 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
2560 self.providers = Some(providers);
2561 self
2562 }
2563
2564 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
2570 self.models = Some(models);
2571 self
2572 }
2573
2574 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2578 self.enable_session_telemetry = Some(enable);
2579 self
2580 }
2581
2582 pub fn with_model_capabilities(
2584 mut self,
2585 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2586 ) -> Self {
2587 self.model_capabilities = Some(capabilities);
2588 self
2589 }
2590
2591 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
2593 self.memory = Some(memory);
2594 self
2595 }
2596
2597 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2599 self.config_directory = Some(dir.into());
2600 self
2601 }
2602
2603 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2606 self.working_directory = Some(dir.into());
2607 self
2608 }
2609
2610 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2615 self.github_token = Some(token.into());
2616 self
2617 }
2618
2619 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2622 self.include_sub_agent_streaming_events = Some(include);
2623 self
2624 }
2625
2626 pub fn with_remote_session(
2628 mut self,
2629 mode: crate::generated::api_types::RemoteSessionMode,
2630 ) -> Self {
2631 self.remote_session = Some(mode);
2632 self
2633 }
2634
2635 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
2637 self.cloud = Some(cloud);
2638 self
2639 }
2640
2641 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
2643 self.skip_custom_instructions = Some(value);
2644 self
2645 }
2646
2647 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
2649 self.custom_agents_local_only = Some(value);
2650 self
2651 }
2652
2653 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
2655 self.coauthor_enabled = Some(value);
2656 self
2657 }
2658
2659 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
2661 self.manage_schedule_enabled = Some(value);
2662 self
2663 }
2664
2665 #[doc(hidden)]
2673 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
2674 self.exp_assignments = Some(assignments);
2675 self
2676 }
2677}
2678#[derive(Clone)]
2685#[non_exhaustive]
2686pub struct ResumeSessionConfig {
2687 pub session_id: SessionId,
2689 pub client_name: Option<String>,
2691 pub reasoning_effort: Option<String>,
2693 pub reasoning_summary: Option<ReasoningSummary>,
2697 pub context_tier: Option<String>,
2700 pub streaming: Option<bool>,
2702 pub system_message: Option<SystemMessageConfig>,
2705 pub tools: Option<Vec<Tool>>,
2707 pub canvases: Option<Vec<CanvasDeclaration>>,
2709 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
2712 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
2714 pub request_canvas_renderer: Option<bool>,
2716 pub request_extensions: Option<bool>,
2718 pub extension_sdk_path: Option<String>,
2722 pub extension_info: Option<ExtensionInfo>,
2724 pub available_tools: Option<Vec<String>>,
2726 pub excluded_tools: Option<Vec<String>>,
2728 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
2730 pub mcp_oauth_token_storage: Option<String>,
2733 pub enable_config_discovery: Option<bool>,
2735 pub skip_embedding_retrieval: Option<bool>,
2737 pub embedding_cache_storage: Option<String>,
2739 pub organization_custom_instructions: Option<String>,
2741 pub enable_on_demand_instruction_discovery: Option<bool>,
2743 pub enable_file_hooks: Option<bool>,
2745 pub enable_host_git_operations: Option<bool>,
2747 pub enable_session_store: Option<bool>,
2749 pub enable_skills: Option<bool>,
2751 pub enable_mcp_apps: Option<bool>,
2757 pub skill_directories: Option<Vec<PathBuf>>,
2759 pub instruction_directories: Option<Vec<PathBuf>>,
2762 pub plugin_directories: Option<Vec<PathBuf>>,
2764 pub large_output: Option<LargeToolOutputConfig>,
2766 pub disabled_skills: Option<Vec<String>>,
2768 pub hooks: Option<bool>,
2770 pub custom_agents: Option<Vec<CustomAgentConfig>>,
2772 pub default_agent: Option<DefaultAgentConfig>,
2774 pub agent: Option<String>,
2776 pub infinite_sessions: Option<InfiniteSessionConfig>,
2778 pub provider: Option<ProviderConfig>,
2780 pub capi: Option<CapiSessionOptions>,
2786 pub providers: Option<Vec<NamedProviderConfig>>,
2792 pub models: Option<Vec<ProviderModelConfig>>,
2798 pub enable_session_telemetry: Option<bool>,
2806 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2808 pub memory: Option<MemoryConfiguration>,
2810 pub config_directory: Option<PathBuf>,
2812 pub working_directory: Option<PathBuf>,
2814 pub github_token: Option<String>,
2817 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
2820 pub include_sub_agent_streaming_events: Option<bool>,
2822 pub commands: Option<Vec<CommandDefinition>>,
2826 #[doc(hidden)]
2831 pub exp_assignments: Option<Value>,
2832 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
2837 pub suppress_resume_event: Option<bool>,
2840 pub continue_pending_work: Option<bool>,
2848 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
2851 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
2854 pub user_input_handler: Option<Arc<dyn UserInputHandler>>,
2857 pub exit_plan_mode_handler: Option<Arc<dyn ExitPlanModeHandler>>,
2860 pub auto_mode_switch_handler: Option<Arc<dyn AutoModeSwitchHandler>>,
2863 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
2865 pub(crate) permission_policy: Option<crate::permission::Policy>,
2867 pub system_message_transform: Option<Arc<dyn SystemMessageTransform>>,
2869 pub skip_custom_instructions: Option<bool>,
2871 pub custom_agents_local_only: Option<bool>,
2873 pub coauthor_enabled: Option<bool>,
2875 pub manage_schedule_enabled: Option<bool>,
2877}
2878
2879impl std::fmt::Debug for ResumeSessionConfig {
2880 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2881 f.debug_struct("ResumeSessionConfig")
2882 .field("session_id", &self.session_id)
2883 .field("client_name", &self.client_name)
2884 .field("reasoning_effort", &self.reasoning_effort)
2885 .field("reasoning_summary", &self.reasoning_summary)
2886 .field("context_tier", &self.context_tier)
2887 .field("streaming", &self.streaming)
2888 .field("system_message", &self.system_message)
2889 .field("tools", &self.tools)
2890 .field("canvases", &self.canvases)
2891 .field(
2892 "canvas_handler",
2893 &self.canvas_handler.as_ref().map(|_| "<set>"),
2894 )
2895 .field("open_canvases", &self.open_canvases)
2896 .field("request_canvas_renderer", &self.request_canvas_renderer)
2897 .field("request_extensions", &self.request_extensions)
2898 .field("extension_sdk_path", &self.extension_sdk_path)
2899 .field("extension_info", &self.extension_info)
2900 .field("available_tools", &self.available_tools)
2901 .field("excluded_tools", &self.excluded_tools)
2902 .field("mcp_servers", &self.mcp_servers)
2903 .field("mcp_oauth_token_storage", &self.mcp_oauth_token_storage)
2904 .field("embedding_cache_storage", &self.embedding_cache_storage)
2905 .field("enable_config_discovery", &self.enable_config_discovery)
2906 .field("skip_embedding_retrieval", &self.skip_embedding_retrieval)
2907 .field(
2908 "organization_custom_instructions",
2909 &self
2910 .organization_custom_instructions
2911 .as_ref()
2912 .map(|_| "<redacted>"),
2913 )
2914 .field(
2915 "enable_on_demand_instruction_discovery",
2916 &self.enable_on_demand_instruction_discovery,
2917 )
2918 .field("enable_file_hooks", &self.enable_file_hooks)
2919 .field(
2920 "enable_host_git_operations",
2921 &self.enable_host_git_operations,
2922 )
2923 .field("enable_session_store", &self.enable_session_store)
2924 .field("enable_skills", &self.enable_skills)
2925 .field("enable_mcp_apps", &self.enable_mcp_apps)
2926 .field("skill_directories", &self.skill_directories)
2927 .field("instruction_directories", &self.instruction_directories)
2928 .field("plugin_directories", &self.plugin_directories)
2929 .field("large_output", &self.large_output)
2930 .field("disabled_skills", &self.disabled_skills)
2931 .field("hooks", &self.hooks)
2932 .field("custom_agents", &self.custom_agents)
2933 .field("default_agent", &self.default_agent)
2934 .field("agent", &self.agent)
2935 .field("infinite_sessions", &self.infinite_sessions)
2936 .field("provider", &self.provider)
2937 .field("capi", &self.capi)
2938 .field("enable_session_telemetry", &self.enable_session_telemetry)
2939 .field("model_capabilities", &self.model_capabilities)
2940 .field("memory", &self.memory)
2941 .field("config_directory", &self.config_directory)
2942 .field("working_directory", &self.working_directory)
2943 .field(
2944 "github_token",
2945 &self.github_token.as_ref().map(|_| "<redacted>"),
2946 )
2947 .field("remote_session", &self.remote_session)
2948 .field(
2949 "include_sub_agent_streaming_events",
2950 &self.include_sub_agent_streaming_events,
2951 )
2952 .field("commands", &self.commands)
2953 .field("exp_assignments", &self.exp_assignments)
2954 .field(
2955 "session_fs_provider",
2956 &self.session_fs_provider.as_ref().map(|_| "<set>"),
2957 )
2958 .field(
2959 "permission_handler",
2960 &self.permission_handler.as_ref().map(|_| "<set>"),
2961 )
2962 .field(
2963 "elicitation_handler",
2964 &self.elicitation_handler.as_ref().map(|_| "<set>"),
2965 )
2966 .field(
2967 "user_input_handler",
2968 &self.user_input_handler.as_ref().map(|_| "<set>"),
2969 )
2970 .field(
2971 "exit_plan_mode_handler",
2972 &self.exit_plan_mode_handler.as_ref().map(|_| "<set>"),
2973 )
2974 .field(
2975 "auto_mode_switch_handler",
2976 &self.auto_mode_switch_handler.as_ref().map(|_| "<set>"),
2977 )
2978 .field(
2979 "hooks_handler",
2980 &self.hooks_handler.as_ref().map(|_| "<set>"),
2981 )
2982 .field(
2983 "system_message_transform",
2984 &self.system_message_transform.as_ref().map(|_| "<set>"),
2985 )
2986 .field("suppress_resume_event", &self.suppress_resume_event)
2987 .field("continue_pending_work", &self.continue_pending_work)
2988 .finish()
2989 }
2990}
2991
2992impl ResumeSessionConfig {
2993 pub(crate) fn into_wire(
3001 mut self,
3002 ) -> Result<(crate::wire::SessionResumeWire, SessionConfigRuntime), crate::Error> {
3003 let permission_active =
3004 self.permission_handler.is_some() || self.permission_policy.is_some();
3005 let request_user_input = self.user_input_handler.is_some();
3006 let request_exit_plan_mode = self.exit_plan_mode_handler.is_some();
3007 let request_auto_mode_switch = self.auto_mode_switch_handler.is_some();
3008 let request_elicitation = self.elicitation_handler.is_some();
3009 let hooks_flag = self.hooks_handler.is_some();
3010
3011 let mut tool_handlers: HashMap<String, Arc<dyn crate::tool::ToolHandler>> = HashMap::new();
3012 if let Some(tools) = self.tools.as_mut() {
3013 for tool in tools.iter_mut() {
3014 if let Some(handler) = tool.handler.take()
3015 && tool_handlers.insert(tool.name.clone(), handler).is_some()
3016 {
3017 return Err(crate::Error::with_message(
3018 crate::ErrorKind::InvalidConfig,
3019 format!("duplicate tool handler registered for name {:?}", tool.name),
3020 ));
3021 }
3022 }
3023 }
3024
3025 let wire_commands = self.commands.as_ref().map(|cmds| {
3026 cmds.iter()
3027 .map(|c| crate::wire::CommandWireDefinition {
3028 name: c.name.clone(),
3029 description: c.description.clone(),
3030 })
3031 .collect()
3032 });
3033 let wire_canvases = self.canvases.clone();
3034 let canvas_handler = self.canvas_handler.clone();
3035 let bearer_token_providers =
3036 prepare_bearer_token_providers(&mut self.provider, &mut self.providers);
3037
3038 let wire = crate::wire::SessionResumeWire {
3039 session_id: self.session_id,
3040 client_name: self.client_name,
3041 reasoning_effort: self.reasoning_effort,
3042 reasoning_summary: self.reasoning_summary,
3043 context_tier: self.context_tier,
3044 streaming: self.streaming,
3045 system_message: self.system_message,
3046 tools: self.tools,
3047 canvases: wire_canvases,
3048 open_canvases: self.open_canvases,
3049 request_canvas_renderer: self.request_canvas_renderer,
3050 request_extensions: self.request_extensions,
3051 extension_sdk_path: self.extension_sdk_path,
3052 extension_info: self.extension_info,
3053 available_tools: self.available_tools,
3054 excluded_tools: self.excluded_tools,
3055 tool_filter_precedence: "excluded",
3056 mcp_servers: self.mcp_servers,
3057 mcp_oauth_token_storage: self.mcp_oauth_token_storage,
3058 embedding_cache_storage: self.embedding_cache_storage,
3059 env_value_mode: "direct",
3060 enable_config_discovery: self.enable_config_discovery,
3061 skip_embedding_retrieval: self.skip_embedding_retrieval,
3062 organization_custom_instructions: self.organization_custom_instructions,
3063 enable_on_demand_instruction_discovery: self.enable_on_demand_instruction_discovery,
3064 enable_file_hooks: self.enable_file_hooks,
3065 enable_host_git_operations: self.enable_host_git_operations,
3066 enable_session_store: self.enable_session_store,
3067 enable_skills: self.enable_skills,
3068 request_user_input,
3069 request_permission: permission_active,
3070 request_exit_plan_mode,
3071 request_auto_mode_switch,
3072 request_elicitation,
3073 request_mcp_apps: self.enable_mcp_apps.unwrap_or(false),
3074 hooks: hooks_flag,
3075 skill_directories: self.skill_directories,
3076 instruction_directories: self.instruction_directories,
3077 plugin_directories: self.plugin_directories,
3078 large_output: self.large_output,
3079 disabled_skills: self.disabled_skills,
3080 custom_agents: self.custom_agents,
3081 default_agent: self.default_agent,
3082 agent: self.agent,
3083 infinite_sessions: self.infinite_sessions,
3084 provider: self.provider,
3085 capi: self.capi,
3086 providers: self.providers,
3087 models: self.models,
3088 enable_session_telemetry: self.enable_session_telemetry,
3089 model_capabilities: self.model_capabilities,
3090 memory: self.memory,
3091 config_dir: self.config_directory,
3092 working_directory: self.working_directory,
3093 github_token: self.github_token,
3094 remote_session: self.remote_session,
3095 include_sub_agent_streaming_events: self.include_sub_agent_streaming_events,
3096 commands: wire_commands,
3097 exp_assignments: self.exp_assignments,
3098 suppress_resume_event: self.suppress_resume_event,
3099 continue_pending_work: self.continue_pending_work,
3100 };
3101
3102 let runtime = SessionConfigRuntime {
3103 permission_handler: self.permission_handler,
3104 permission_policy: self.permission_policy,
3105 elicitation_handler: self.elicitation_handler,
3106 user_input_handler: self.user_input_handler,
3107 exit_plan_mode_handler: self.exit_plan_mode_handler,
3108 auto_mode_switch_handler: self.auto_mode_switch_handler,
3109 hooks_handler: self.hooks_handler,
3110 system_message_transform: self.system_message_transform,
3111 tool_handlers,
3112 canvas_handler,
3113 session_fs_provider: self.session_fs_provider,
3114 bearer_token_providers,
3115 commands: self.commands,
3116 };
3117
3118 Ok((wire, runtime))
3119 }
3120
3121 pub fn new(session_id: SessionId) -> Self {
3126 Self {
3127 session_id,
3128 client_name: None,
3129 reasoning_effort: None,
3130 reasoning_summary: None,
3131 context_tier: None,
3132 streaming: None,
3133 system_message: None,
3134 tools: None,
3135 canvases: None,
3136 canvas_handler: None,
3137 open_canvases: None,
3138 request_canvas_renderer: None,
3139 request_extensions: None,
3140 extension_sdk_path: None,
3141 extension_info: None,
3142 available_tools: None,
3143 excluded_tools: None,
3144 mcp_servers: None,
3145 mcp_oauth_token_storage: None,
3146 enable_config_discovery: None,
3147 skip_embedding_retrieval: None,
3148 organization_custom_instructions: None,
3149 enable_on_demand_instruction_discovery: None,
3150 enable_file_hooks: None,
3151 enable_host_git_operations: None,
3152 enable_session_store: None,
3153 enable_skills: None,
3154 embedding_cache_storage: None,
3155 enable_mcp_apps: None,
3156 skill_directories: None,
3157 instruction_directories: None,
3158 plugin_directories: None,
3159 large_output: None,
3160 disabled_skills: None,
3161 hooks: None,
3162 custom_agents: None,
3163 default_agent: None,
3164 agent: None,
3165 infinite_sessions: None,
3166 provider: None,
3167 capi: None,
3168 providers: None,
3169 models: None,
3170 enable_session_telemetry: None,
3171 model_capabilities: None,
3172 memory: None,
3173 config_directory: None,
3174 working_directory: None,
3175 github_token: None,
3176 remote_session: None,
3177 include_sub_agent_streaming_events: None,
3178 commands: None,
3179 exp_assignments: None,
3180 session_fs_provider: None,
3181 suppress_resume_event: None,
3182 continue_pending_work: None,
3183 permission_handler: None,
3184 elicitation_handler: None,
3185 user_input_handler: None,
3186 exit_plan_mode_handler: None,
3187 auto_mode_switch_handler: None,
3188 hooks_handler: None,
3189 permission_policy: None,
3190 system_message_transform: None,
3191 skip_custom_instructions: None,
3192 custom_agents_local_only: None,
3193 coauthor_enabled: None,
3194 manage_schedule_enabled: None,
3195 }
3196 }
3197
3198 pub fn with_permission_handler(mut self, handler: Arc<dyn PermissionHandler>) -> Self {
3200 self.permission_handler = Some(handler);
3201 self
3202 }
3203
3204 pub fn with_elicitation_handler(mut self, handler: Arc<dyn ElicitationHandler>) -> Self {
3206 self.elicitation_handler = Some(handler);
3207 self
3208 }
3209
3210 pub fn with_user_input_handler(mut self, handler: Arc<dyn UserInputHandler>) -> Self {
3212 self.user_input_handler = Some(handler);
3213 self
3214 }
3215
3216 pub fn with_exit_plan_mode_handler(mut self, handler: Arc<dyn ExitPlanModeHandler>) -> Self {
3218 self.exit_plan_mode_handler = Some(handler);
3219 self
3220 }
3221
3222 pub fn with_auto_mode_switch_handler(
3224 mut self,
3225 handler: Arc<dyn AutoModeSwitchHandler>,
3226 ) -> Self {
3227 self.auto_mode_switch_handler = Some(handler);
3228 self
3229 }
3230
3231 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
3234 self.hooks_handler = Some(hooks);
3235 self
3236 }
3237
3238 pub fn with_system_message_transform(
3240 mut self,
3241 transform: Arc<dyn SystemMessageTransform>,
3242 ) -> Self {
3243 self.system_message_transform = Some(transform);
3244 self
3245 }
3246
3247 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
3251 self.commands = Some(commands);
3252 self
3253 }
3254
3255 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
3258 self.session_fs_provider = Some(provider);
3259 self
3260 }
3261
3262 pub fn approve_all_permissions(mut self) -> Self {
3265 self.permission_policy = Some(crate::permission::Policy::ApproveAll);
3266 self
3267 }
3268
3269 pub fn deny_all_permissions(mut self) -> Self {
3272 self.permission_policy = Some(crate::permission::Policy::DenyAll);
3273 self
3274 }
3275
3276 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
3279 where
3280 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
3281 {
3282 self.permission_policy = Some(crate::permission::Policy::Predicate(Arc::new(predicate)));
3283 self
3284 }
3285
3286 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
3288 self.client_name = Some(name.into());
3289 self
3290 }
3291
3292 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3294 self.reasoning_effort = Some(effort.into());
3295 self
3296 }
3297
3298 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3300 self.reasoning_summary = Some(summary);
3301 self
3302 }
3303
3304 pub fn with_context_tier(mut self, tier: impl Into<String>) -> Self {
3307 self.context_tier = Some(tier.into());
3308 self
3309 }
3310
3311 pub fn with_streaming(mut self, streaming: bool) -> Self {
3313 self.streaming = Some(streaming);
3314 self
3315 }
3316
3317 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
3320 self.system_message = Some(system_message);
3321 self
3322 }
3323
3324 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
3326 self.tools = Some(tools.into_iter().collect());
3327 self
3328 }
3329
3330 pub fn with_canvases<I: IntoIterator<Item = CanvasDeclaration>>(mut self, canvases: I) -> Self {
3332 self.canvases = Some(canvases.into_iter().collect());
3333 self
3334 }
3335
3336 pub fn with_canvas_handler(mut self, handler: Arc<dyn CanvasHandler>) -> Self {
3338 self.canvas_handler = Some(handler);
3339 self
3340 }
3341
3342 pub fn with_open_canvases<I: IntoIterator<Item = OpenCanvasInstance>>(
3344 mut self,
3345 open_canvases: I,
3346 ) -> Self {
3347 self.open_canvases = Some(open_canvases.into_iter().collect());
3348 self
3349 }
3350
3351 pub fn with_request_canvas_renderer(mut self, request: bool) -> Self {
3353 self.request_canvas_renderer = Some(request);
3354 self
3355 }
3356
3357 pub fn with_request_extensions(mut self, request: bool) -> Self {
3359 self.request_extensions = Some(request);
3360 self
3361 }
3362
3363 pub fn with_extension_sdk_path(mut self, path: impl Into<String>) -> Self {
3367 self.extension_sdk_path = Some(path.into());
3368 self
3369 }
3370
3371 pub fn with_extension_info(mut self, extension_info: ExtensionInfo) -> Self {
3373 self.extension_info = Some(extension_info);
3374 self
3375 }
3376
3377 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
3379 where
3380 I: IntoIterator<Item = S>,
3381 S: Into<String>,
3382 {
3383 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
3384 self
3385 }
3386
3387 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
3389 where
3390 I: IntoIterator<Item = S>,
3391 S: Into<String>,
3392 {
3393 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
3394 self
3395 }
3396
3397 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
3399 self.mcp_servers = Some(servers);
3400 self
3401 }
3402
3403 pub fn with_mcp_oauth_token_storage(mut self, mode: impl Into<String>) -> Self {
3406 self.mcp_oauth_token_storage = Some(mode.into());
3407 self
3408 }
3409
3410 pub fn with_embedding_cache_storage(
3412 mut self,
3413 embedding_cache_storage: impl Into<String>,
3414 ) -> Self {
3415 self.embedding_cache_storage = Some(embedding_cache_storage.into());
3416 self
3417 }
3418
3419 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
3421 self.enable_config_discovery = Some(enable);
3422 self
3423 }
3424
3425 pub fn with_skip_embedding_retrieval(mut self, value: bool) -> Self {
3427 self.skip_embedding_retrieval = Some(value);
3428 self
3429 }
3430
3431 pub fn with_organization_custom_instructions(
3433 mut self,
3434 instructions: impl Into<String>,
3435 ) -> Self {
3436 self.organization_custom_instructions = Some(instructions.into());
3437 self
3438 }
3439
3440 pub fn with_enable_on_demand_instruction_discovery(mut self, value: bool) -> Self {
3442 self.enable_on_demand_instruction_discovery = Some(value);
3443 self
3444 }
3445
3446 pub fn with_enable_file_hooks(mut self, value: bool) -> Self {
3448 self.enable_file_hooks = Some(value);
3449 self
3450 }
3451
3452 pub fn with_enable_host_git_operations(mut self, value: bool) -> Self {
3454 self.enable_host_git_operations = Some(value);
3455 self
3456 }
3457
3458 pub fn with_enable_session_store(mut self, value: bool) -> Self {
3460 self.enable_session_store = Some(value);
3461 self
3462 }
3463
3464 pub fn with_enable_skills(mut self, value: bool) -> Self {
3466 self.enable_skills = Some(value);
3467 self
3468 }
3469
3470 pub fn with_enable_mcp_apps(mut self, enable: bool) -> Self {
3476 self.enable_mcp_apps = Some(enable);
3477 self
3478 }
3479
3480 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
3482 where
3483 I: IntoIterator<Item = P>,
3484 P: Into<PathBuf>,
3485 {
3486 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
3487 self
3488 }
3489
3490 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
3494 where
3495 I: IntoIterator<Item = P>,
3496 P: Into<PathBuf>,
3497 {
3498 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
3499 self
3500 }
3501
3502 pub fn with_plugin_directories<I, P>(mut self, paths: I) -> Self
3504 where
3505 I: IntoIterator<Item = P>,
3506 P: Into<PathBuf>,
3507 {
3508 self.plugin_directories = Some(paths.into_iter().map(Into::into).collect());
3509 self
3510 }
3511
3512 pub fn with_large_output(mut self, config: LargeToolOutputConfig) -> Self {
3514 self.large_output = Some(config);
3515 self
3516 }
3517
3518 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
3520 where
3521 I: IntoIterator<Item = S>,
3522 S: Into<String>,
3523 {
3524 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
3525 self
3526 }
3527
3528 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
3530 mut self,
3531 agents: I,
3532 ) -> Self {
3533 self.custom_agents = Some(agents.into_iter().collect());
3534 self
3535 }
3536
3537 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
3539 self.default_agent = Some(agent);
3540 self
3541 }
3542
3543 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
3545 self.agent = Some(name.into());
3546 self
3547 }
3548
3549 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
3551 self.infinite_sessions = Some(config);
3552 self
3553 }
3554
3555 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
3557 self.provider = Some(provider);
3558 self
3559 }
3560
3561 pub fn with_capi(mut self, capi: CapiSessionOptions) -> Self {
3563 self.capi = Some(capi);
3564 self
3565 }
3566
3567 pub fn with_providers(mut self, providers: Vec<NamedProviderConfig>) -> Self {
3573 self.providers = Some(providers);
3574 self
3575 }
3576
3577 pub fn with_models(mut self, models: Vec<ProviderModelConfig>) -> Self {
3583 self.models = Some(models);
3584 self
3585 }
3586
3587 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
3591 self.enable_session_telemetry = Some(enable);
3592 self
3593 }
3594
3595 pub fn with_model_capabilities(
3597 mut self,
3598 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
3599 ) -> Self {
3600 self.model_capabilities = Some(capabilities);
3601 self
3602 }
3603
3604 pub fn with_memory(mut self, memory: MemoryConfiguration) -> Self {
3606 self.memory = Some(memory);
3607 self
3608 }
3609
3610 pub fn with_config_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3612 self.config_directory = Some(dir.into());
3613 self
3614 }
3615
3616 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
3618 self.working_directory = Some(dir.into());
3619 self
3620 }
3621
3622 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
3626 self.github_token = Some(token.into());
3627 self
3628 }
3629
3630 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
3632 self.include_sub_agent_streaming_events = Some(include);
3633 self
3634 }
3635
3636 pub fn with_remote_session(
3638 mut self,
3639 mode: crate::generated::api_types::RemoteSessionMode,
3640 ) -> Self {
3641 self.remote_session = Some(mode);
3642 self
3643 }
3644
3645 pub fn with_suppress_resume_event(mut self, suppress: bool) -> Self {
3648 self.suppress_resume_event = Some(suppress);
3649 self
3650 }
3651
3652 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
3658 self.continue_pending_work = Some(continue_pending);
3659 self
3660 }
3661
3662 pub fn with_skip_custom_instructions(mut self, value: bool) -> Self {
3664 self.skip_custom_instructions = Some(value);
3665 self
3666 }
3667
3668 pub fn with_custom_agents_local_only(mut self, value: bool) -> Self {
3670 self.custom_agents_local_only = Some(value);
3671 self
3672 }
3673
3674 pub fn with_coauthor_enabled(mut self, value: bool) -> Self {
3676 self.coauthor_enabled = Some(value);
3677 self
3678 }
3679
3680 pub fn with_manage_schedule_enabled(mut self, value: bool) -> Self {
3682 self.manage_schedule_enabled = Some(value);
3683 self
3684 }
3685
3686 #[doc(hidden)]
3690 pub fn with_exp_assignments(mut self, assignments: Value) -> Self {
3691 self.exp_assignments = Some(assignments);
3692 self
3693 }
3694}
3695
3696#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3702#[serde(rename_all = "camelCase")]
3703#[non_exhaustive]
3704pub struct SystemMessageConfig {
3705 #[serde(skip_serializing_if = "Option::is_none")]
3707 pub mode: Option<String>,
3708 #[serde(skip_serializing_if = "Option::is_none")]
3710 pub content: Option<String>,
3711 #[serde(skip_serializing_if = "Option::is_none")]
3713 pub sections: Option<HashMap<String, SectionOverride>>,
3714}
3715
3716impl SystemMessageConfig {
3717 pub fn new() -> Self {
3720 Self::default()
3721 }
3722
3723 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
3726 self.mode = Some(mode.into());
3727 self
3728 }
3729
3730 pub fn with_content(mut self, content: impl Into<String>) -> Self {
3733 self.content = Some(content.into());
3734 self
3735 }
3736
3737 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
3739 self.sections = Some(sections);
3740 self
3741 }
3742}
3743
3744#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3750#[serde(rename_all = "camelCase")]
3751pub struct SectionOverride {
3752 #[serde(skip_serializing_if = "Option::is_none")]
3755 pub action: Option<String>,
3756 #[serde(skip_serializing_if = "Option::is_none")]
3758 pub content: Option<String>,
3759}
3760
3761#[derive(Debug, Clone, Serialize, Deserialize)]
3763#[serde(rename_all = "camelCase")]
3764pub struct CreateSessionResult {
3765 pub session_id: SessionId,
3767 #[serde(skip_serializing_if = "Option::is_none")]
3769 pub workspace_path: Option<PathBuf>,
3770 #[serde(default, alias = "remote_url")]
3772 pub remote_url: Option<String>,
3773 #[serde(skip_serializing_if = "Option::is_none")]
3775 pub capabilities: Option<SessionCapabilities>,
3776}
3777
3778#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3780#[serde(rename_all = "camelCase")]
3781pub(crate) struct ResumeSessionResult {
3782 #[serde(default)]
3784 pub session_id: Option<SessionId>,
3785 #[serde(default, skip_serializing_if = "Option::is_none")]
3787 pub workspace_path: Option<PathBuf>,
3788 #[serde(default, alias = "remote_url")]
3790 pub remote_url: Option<String>,
3791 #[serde(default, skip_serializing_if = "Option::is_none")]
3793 pub capabilities: Option<SessionCapabilities>,
3794 #[serde(
3796 default,
3797 alias = "openCanvasInstances",
3798 skip_serializing_if = "Option::is_none"
3799 )]
3800 pub open_canvases: Option<Vec<OpenCanvasInstance>>,
3801}
3802
3803#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
3805#[serde(rename_all = "lowercase")]
3806pub enum LogLevel {
3807 #[default]
3809 Info,
3810 Warning,
3812 Error,
3814}
3815
3816#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
3821#[serde(rename_all = "camelCase")]
3822pub struct LogOptions {
3823 #[serde(skip_serializing_if = "Option::is_none")]
3825 pub level: Option<LogLevel>,
3826 #[serde(skip_serializing_if = "Option::is_none")]
3829 pub ephemeral: Option<bool>,
3830}
3831
3832impl LogOptions {
3833 pub fn with_level(mut self, level: LogLevel) -> Self {
3835 self.level = Some(level);
3836 self
3837 }
3838
3839 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
3841 self.ephemeral = Some(ephemeral);
3842 self
3843 }
3844}
3845
3846#[derive(Debug, Clone, Default)]
3850pub struct SetModelOptions {
3851 pub reasoning_effort: Option<String>,
3854 pub reasoning_summary: Option<ReasoningSummary>,
3858 pub context_tier: Option<ContextTier>,
3861 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
3865}
3866
3867impl SetModelOptions {
3868 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
3870 self.reasoning_effort = Some(effort.into());
3871 self
3872 }
3873
3874 pub fn with_reasoning_summary(mut self, summary: ReasoningSummary) -> Self {
3876 self.reasoning_summary = Some(summary);
3877 self
3878 }
3879
3880 pub fn with_context_tier(mut self, tier: ContextTier) -> Self {
3882 self.context_tier = Some(tier);
3883 self
3884 }
3885
3886 pub fn with_model_capabilities(
3888 mut self,
3889 caps: crate::generated::api_types::ModelCapabilitiesOverride,
3890 ) -> Self {
3891 self.model_capabilities = Some(caps);
3892 self
3893 }
3894}
3895
3896#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
3903#[serde(rename_all = "camelCase")]
3904pub struct PingResponse {
3905 #[serde(default)]
3907 pub message: String,
3908 #[serde(default)]
3910 pub timestamp: String,
3911 #[serde(skip_serializing_if = "Option::is_none")]
3913 pub protocol_version: Option<u32>,
3914}
3915
3916#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3918#[serde(rename_all = "camelCase")]
3919pub struct AttachmentLineRange {
3920 pub start: u32,
3922 pub end: u32,
3924}
3925
3926#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3928#[serde(rename_all = "camelCase")]
3929pub struct AttachmentSelectionPosition {
3930 pub line: u32,
3932 pub character: u32,
3934}
3935
3936#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3938#[serde(rename_all = "camelCase")]
3939pub struct AttachmentSelectionRange {
3940 pub start: AttachmentSelectionPosition,
3942 pub end: AttachmentSelectionPosition,
3944}
3945
3946#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3948#[serde(rename_all = "snake_case")]
3949#[non_exhaustive]
3950pub enum GitHubReferenceType {
3951 Issue,
3953 Pr,
3955 Discussion,
3957}
3958
3959#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3961#[serde(
3962 tag = "type",
3963 rename_all = "camelCase",
3964 rename_all_fields = "camelCase"
3965)]
3966#[non_exhaustive]
3967pub enum Attachment {
3968 File {
3970 path: PathBuf,
3972 #[serde(skip_serializing_if = "Option::is_none")]
3974 display_name: Option<String>,
3975 #[serde(skip_serializing_if = "Option::is_none")]
3977 line_range: Option<AttachmentLineRange>,
3978 },
3979 Directory {
3981 path: PathBuf,
3983 #[serde(skip_serializing_if = "Option::is_none")]
3985 display_name: Option<String>,
3986 },
3987 Selection {
3989 file_path: PathBuf,
3991 text: String,
3993 #[serde(skip_serializing_if = "Option::is_none")]
3995 display_name: Option<String>,
3996 selection: AttachmentSelectionRange,
3998 },
3999 Blob {
4001 data: String,
4003 mime_type: String,
4005 #[serde(skip_serializing_if = "Option::is_none")]
4007 display_name: Option<String>,
4008 },
4009 #[serde(rename = "github_reference")]
4011 GitHubReference {
4012 number: u64,
4014 title: String,
4016 reference_type: GitHubReferenceType,
4018 state: String,
4020 url: String,
4022 },
4023}
4024
4025impl Attachment {
4026 pub fn display_name(&self) -> Option<&str> {
4028 match self {
4029 Self::File { display_name, .. }
4030 | Self::Directory { display_name, .. }
4031 | Self::Selection { display_name, .. }
4032 | Self::Blob { display_name, .. } => display_name.as_deref(),
4033 Self::GitHubReference { .. } => None,
4034 }
4035 }
4036
4037 pub fn label(&self) -> Option<String> {
4039 if let Some(display_name) = self
4040 .display_name()
4041 .map(str::trim)
4042 .filter(|name| !name.is_empty())
4043 {
4044 return Some(display_name.to_string());
4045 }
4046
4047 match self {
4048 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4049 format!("#{}", number)
4050 } else {
4051 title.trim().to_string()
4052 }),
4053 _ => self.derived_display_name(),
4054 }
4055 }
4056
4057 pub fn ensure_display_name(&mut self) {
4059 if self
4060 .display_name()
4061 .map(str::trim)
4062 .is_some_and(|name| !name.is_empty())
4063 {
4064 return;
4065 }
4066
4067 let Some(derived_display_name) = self.derived_display_name() else {
4068 return;
4069 };
4070
4071 match self {
4072 Self::File { display_name, .. }
4073 | Self::Directory { display_name, .. }
4074 | Self::Selection { display_name, .. }
4075 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4076 Self::GitHubReference { .. } => {}
4077 }
4078 }
4079
4080 fn derived_display_name(&self) -> Option<String> {
4081 match self {
4082 Self::File { path, .. } | Self::Directory { path, .. } => {
4083 Some(attachment_name_from_path(path))
4084 }
4085 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4086 Self::Blob { .. } => Some("attachment".to_string()),
4087 Self::GitHubReference { .. } => None,
4088 }
4089 }
4090}
4091
4092fn attachment_name_from_path(path: &Path) -> String {
4093 path.file_name()
4094 .map(|name| name.to_string_lossy().into_owned())
4095 .filter(|name| !name.is_empty())
4096 .unwrap_or_else(|| {
4097 let full = path.to_string_lossy();
4098 if full.is_empty() {
4099 "attachment".to_string()
4100 } else {
4101 full.into_owned()
4102 }
4103 })
4104}
4105
4106pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
4108 for attachment in attachments {
4109 attachment.ensure_display_name();
4110 }
4111}
4112
4113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4118#[serde(rename_all = "lowercase")]
4119#[non_exhaustive]
4120pub enum DeliveryMode {
4121 Enqueue,
4123 Immediate,
4125}
4126
4127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4132#[serde(rename_all = "lowercase")]
4133#[non_exhaustive]
4134pub enum AgentMode {
4135 Interactive,
4137 Plan,
4139 Autopilot,
4141 Shell,
4143}
4144
4145#[derive(Debug, Clone)]
4174#[non_exhaustive]
4175pub struct MessageOptions {
4176 pub prompt: String,
4178 pub mode: Option<DeliveryMode>,
4184 pub agent_mode: Option<AgentMode>,
4188 pub attachments: Option<Vec<Attachment>>,
4190 pub wait_timeout: Option<Duration>,
4193 pub request_headers: Option<HashMap<String, String>>,
4197 pub traceparent: Option<String>,
4204 pub tracestate: Option<String>,
4208 pub display_prompt: Option<String>,
4210}
4211
4212impl MessageOptions {
4213 pub fn new(prompt: impl Into<String>) -> Self {
4215 Self {
4216 prompt: prompt.into(),
4217 mode: None,
4218 agent_mode: None,
4219 attachments: None,
4220 wait_timeout: None,
4221 request_headers: None,
4222 traceparent: None,
4223 tracestate: None,
4224 display_prompt: None,
4225 }
4226 }
4227
4228 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
4234 self.mode = Some(mode);
4235 self
4236 }
4237
4238 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
4242 self.agent_mode = Some(agent_mode);
4243 self
4244 }
4245
4246 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
4248 self.attachments = Some(attachments);
4249 self
4250 }
4251
4252 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
4254 self.wait_timeout = Some(timeout);
4255 self
4256 }
4257
4258 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
4260 self.request_headers = Some(headers);
4261 self
4262 }
4263
4264 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
4269 self.traceparent = ctx.traceparent;
4270 self.tracestate = ctx.tracestate;
4271 self
4272 }
4273
4274 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
4276 self.traceparent = Some(traceparent.into());
4277 self
4278 }
4279
4280 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
4282 self.tracestate = Some(tracestate.into());
4283 self
4284 }
4285
4286 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
4288 self.display_prompt = Some(display_prompt.into());
4289 self
4290 }
4291}
4292
4293impl From<&str> for MessageOptions {
4294 fn from(prompt: &str) -> Self {
4295 Self::new(prompt)
4296 }
4297}
4298
4299impl From<String> for MessageOptions {
4300 fn from(prompt: String) -> Self {
4301 Self::new(prompt)
4302 }
4303}
4304
4305impl From<&String> for MessageOptions {
4306 fn from(prompt: &String) -> Self {
4307 Self::new(prompt.clone())
4308 }
4309}
4310
4311#[derive(Debug, Clone, Serialize, Deserialize)]
4313#[serde(rename_all = "camelCase")]
4314#[non_exhaustive]
4315pub struct GetStatusResponse {
4316 pub version: String,
4318 pub protocol_version: u32,
4320}
4321
4322#[derive(Debug, Clone, Serialize, Deserialize)]
4324#[serde(rename_all = "camelCase")]
4325#[non_exhaustive]
4326pub struct GetAuthStatusResponse {
4327 pub is_authenticated: bool,
4329 #[serde(skip_serializing_if = "Option::is_none")]
4332 pub auth_type: Option<String>,
4333 #[serde(skip_serializing_if = "Option::is_none")]
4335 pub host: Option<String>,
4336 #[serde(skip_serializing_if = "Option::is_none")]
4338 pub login: Option<String>,
4339 #[serde(skip_serializing_if = "Option::is_none")]
4341 pub status_message: Option<String>,
4342}
4343
4344#[derive(Debug, Clone, Serialize, Deserialize)]
4348#[serde(rename_all = "camelCase")]
4349pub struct SessionEventNotification {
4350 pub session_id: SessionId,
4352 pub event: SessionEvent,
4354}
4355
4356#[derive(Debug, Clone, Serialize, Deserialize)]
4363#[serde(rename_all = "camelCase")]
4364pub struct SessionEvent {
4365 pub id: String,
4367 pub timestamp: String,
4369 pub parent_id: Option<String>,
4371 #[serde(skip_serializing_if = "Option::is_none")]
4373 pub ephemeral: Option<bool>,
4374 #[serde(skip_serializing_if = "Option::is_none")]
4377 pub agent_id: Option<String>,
4378 #[serde(skip_serializing_if = "Option::is_none")]
4380 pub debug_cli_received_at_ms: Option<i64>,
4381 #[serde(skip_serializing_if = "Option::is_none")]
4383 pub debug_ws_forwarded_at_ms: Option<i64>,
4384 #[serde(rename = "type")]
4386 pub event_type: String,
4387 pub data: Value,
4389}
4390
4391impl SessionEvent {
4392 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
4397 use serde::de::IntoDeserializer;
4398 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
4399 self.event_type.as_str().into_deserializer();
4400 crate::generated::SessionEventType::deserialize(deserializer)
4401 .unwrap_or(crate::generated::SessionEventType::Unknown)
4402 }
4403
4404 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
4410 serde_json::from_value(self.data.clone()).ok()
4411 }
4412
4413 pub fn is_transient_error(&self) -> bool {
4417 self.event_type == "session.error"
4418 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
4419 }
4420}
4421
4422#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4427#[serde(rename_all = "camelCase")]
4428#[non_exhaustive]
4429pub struct ToolInvocation {
4430 pub session_id: SessionId,
4432 pub tool_call_id: String,
4434 pub tool_name: String,
4436 pub arguments: Value,
4438 #[serde(default, skip_serializing_if = "Option::is_none")]
4443 pub traceparent: Option<String>,
4444 #[serde(default, skip_serializing_if = "Option::is_none")]
4447 pub tracestate: Option<String>,
4448}
4449
4450impl ToolInvocation {
4451 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
4472 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
4473 }
4474
4475 pub fn trace_context(&self) -> TraceContext {
4478 TraceContext {
4479 traceparent: self.traceparent.clone(),
4480 tracestate: self.tracestate.clone(),
4481 }
4482 }
4483}
4484
4485#[derive(Debug, Clone, Serialize, Deserialize)]
4487#[serde(rename_all = "camelCase")]
4488pub struct ToolBinaryResult {
4489 pub data: String,
4491 pub mime_type: String,
4493 pub r#type: String,
4495 #[serde(default, skip_serializing_if = "Option::is_none")]
4497 pub description: Option<String>,
4498}
4499
4500#[derive(Debug, Clone, Serialize, Deserialize)]
4502#[serde(rename_all = "camelCase")]
4503pub struct ToolResultExpanded {
4504 pub text_result_for_llm: String,
4506 pub result_type: String,
4508 #[serde(default, skip_serializing_if = "Option::is_none")]
4510 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
4511 #[serde(skip_serializing_if = "Option::is_none")]
4513 pub session_log: Option<String>,
4514 #[serde(skip_serializing_if = "Option::is_none")]
4516 pub error: Option<String>,
4517 #[serde(default, skip_serializing_if = "Option::is_none")]
4519 pub tool_telemetry: Option<HashMap<String, Value>>,
4520}
4521
4522#[derive(Debug, Clone, Serialize, Deserialize)]
4524#[serde(untagged)]
4525#[non_exhaustive]
4526pub enum ToolResult {
4527 Text(String),
4529 Expanded(ToolResultExpanded),
4531}
4532
4533#[derive(Debug, Clone, Serialize, Deserialize)]
4535#[serde(rename_all = "camelCase")]
4536pub struct ToolResultResponse {
4537 pub result: ToolResult,
4539}
4540
4541#[derive(Debug, Clone, Serialize, Deserialize)]
4543#[serde(rename_all = "camelCase")]
4544pub struct SessionMetadata {
4545 pub session_id: SessionId,
4547 pub start_time: String,
4549 pub modified_time: String,
4551 #[serde(skip_serializing_if = "Option::is_none")]
4553 pub summary: Option<String>,
4554 pub is_remote: bool,
4556}
4557
4558#[derive(Debug, Clone, Serialize, Deserialize)]
4560#[serde(rename_all = "camelCase")]
4561pub struct ListSessionsResponse {
4562 pub sessions: Vec<SessionMetadata>,
4564}
4565
4566#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4570#[serde(rename_all = "camelCase")]
4571pub struct SessionListFilter {
4572 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
4574 pub working_directory: Option<String>,
4575 #[serde(default, skip_serializing_if = "Option::is_none")]
4577 pub git_root: Option<String>,
4578 #[serde(default, skip_serializing_if = "Option::is_none")]
4580 pub repository: Option<String>,
4581 #[serde(default, skip_serializing_if = "Option::is_none")]
4583 pub branch: Option<String>,
4584}
4585
4586#[derive(Debug, Clone, Serialize, Deserialize)]
4588#[serde(rename_all = "camelCase")]
4589pub struct GetSessionMetadataResponse {
4590 #[serde(skip_serializing_if = "Option::is_none")]
4592 pub session: Option<SessionMetadata>,
4593}
4594
4595#[derive(Debug, Clone, Serialize, Deserialize)]
4597#[serde(rename_all = "camelCase")]
4598pub struct GetLastSessionIdResponse {
4599 #[serde(skip_serializing_if = "Option::is_none")]
4601 pub session_id: Option<SessionId>,
4602}
4603
4604#[derive(Debug, Clone, Serialize, Deserialize)]
4606#[serde(rename_all = "camelCase")]
4607pub struct GetForegroundSessionResponse {
4608 #[serde(skip_serializing_if = "Option::is_none")]
4610 pub session_id: Option<SessionId>,
4611}
4612
4613#[derive(Debug, Clone, Serialize, Deserialize)]
4615#[serde(rename_all = "camelCase")]
4616pub struct GetMessagesResponse {
4617 pub events: Vec<SessionEvent>,
4619}
4620
4621#[derive(Debug, Clone, Serialize, Deserialize)]
4623#[serde(rename_all = "camelCase")]
4624pub struct ElicitationResult {
4625 pub action: String,
4627 #[serde(skip_serializing_if = "Option::is_none")]
4629 pub content: Option<Value>,
4630}
4631
4632#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4638#[serde(rename_all = "camelCase")]
4639#[non_exhaustive]
4640pub enum ElicitationMode {
4641 Form,
4643 Url,
4645 #[serde(other)]
4647 Unknown,
4648}
4649
4650#[derive(Debug, Clone, Serialize, Deserialize)]
4657#[serde(rename_all = "camelCase")]
4658pub struct ElicitationRequest {
4659 pub message: String,
4661 #[serde(skip_serializing_if = "Option::is_none")]
4663 pub requested_schema: Option<Value>,
4664 #[serde(skip_serializing_if = "Option::is_none")]
4666 pub mode: Option<ElicitationMode>,
4667 #[serde(skip_serializing_if = "Option::is_none")]
4669 pub elicitation_source: Option<String>,
4670 #[serde(skip_serializing_if = "Option::is_none")]
4672 pub url: Option<String>,
4673}
4674
4675#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4680#[serde(rename_all = "camelCase")]
4681pub struct SessionCapabilities {
4682 #[serde(skip_serializing_if = "Option::is_none")]
4684 pub ui: Option<UiCapabilities>,
4685}
4686
4687#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4689#[serde(rename_all = "camelCase")]
4690pub struct UiCapabilities {
4691 #[serde(skip_serializing_if = "Option::is_none")]
4693 pub elicitation: Option<bool>,
4694 #[serde(skip_serializing_if = "Option::is_none")]
4705 pub mcp_apps: Option<bool>,
4706 #[serde(skip_serializing_if = "Option::is_none")]
4708 pub canvases: Option<bool>,
4709}
4710
4711#[derive(Debug, Clone, Default)]
4713pub struct UiInputOptions<'a> {
4714 pub title: Option<&'a str>,
4716 pub description: Option<&'a str>,
4718 pub min_length: Option<u64>,
4720 pub max_length: Option<u64>,
4722 pub format: Option<InputFormat>,
4724 pub default: Option<&'a str>,
4726}
4727
4728#[derive(Debug, Clone, Copy)]
4730#[non_exhaustive]
4731pub enum InputFormat {
4732 Email,
4734 Uri,
4736 Date,
4738 DateTime,
4740}
4741
4742impl InputFormat {
4743 pub fn as_str(&self) -> &'static str {
4745 match self {
4746 Self::Email => "email",
4747 Self::Uri => "uri",
4748 Self::Date => "date",
4749 Self::DateTime => "date-time",
4750 }
4751 }
4752}
4753
4754pub use crate::generated::api_types::{
4759 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
4760 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
4761 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
4762 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
4763};
4764
4765#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4771#[serde(rename_all = "kebab-case")]
4772#[non_exhaustive]
4773pub enum PermissionRequestKind {
4774 Shell,
4776 Write,
4778 Read,
4780 Url,
4782 Mcp,
4784 CustomTool,
4786 Memory,
4788 Hook,
4790 #[serde(other)]
4793 Unknown,
4794}
4795
4796#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4802#[serde(rename_all = "camelCase")]
4803pub struct PermissionRequestData {
4804 #[serde(default, skip_serializing_if = "Option::is_none")]
4808 pub kind: Option<PermissionRequestKind>,
4809 #[serde(default, skip_serializing_if = "Option::is_none")]
4812 pub tool_call_id: Option<String>,
4813 #[serde(flatten)]
4816 pub extra: Value,
4817}
4818
4819#[derive(Debug, Clone, Serialize, Deserialize)]
4821#[serde(rename_all = "camelCase")]
4822pub struct ExitPlanModeData {
4823 #[serde(default)]
4825 pub summary: String,
4826 #[serde(default, skip_serializing_if = "Option::is_none")]
4828 pub plan_content: Option<String>,
4829 #[serde(default)]
4831 pub actions: Vec<String>,
4832 #[serde(default = "default_recommended_action")]
4834 pub recommended_action: String,
4835}
4836
4837fn default_recommended_action() -> String {
4838 "autopilot".to_string()
4839}
4840
4841impl Default for ExitPlanModeData {
4842 fn default() -> Self {
4843 Self {
4844 summary: String::new(),
4845 plan_content: None,
4846 actions: Vec::new(),
4847 recommended_action: default_recommended_action(),
4848 }
4849 }
4850}
4851
4852#[cfg(test)]
4853mod tests {
4854 use std::path::PathBuf;
4855
4856 use serde_json::json;
4857
4858 use super::{
4859 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
4860 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
4861 CustomAgentConfig, DeliveryMode, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig,
4862 LargeToolOutputConfig, MemoryConfiguration, NamedProviderConfig, ProviderConfig,
4863 ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
4864 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
4865 ToolResultResponse, ensure_attachment_display_names,
4866 };
4867 use crate::generated::session_events::TypedSessionEvent;
4868
4869 #[test]
4870 fn tool_builder_composes() {
4871 let tool = Tool::new("greet")
4872 .with_description("Say hello")
4873 .with_namespaced_name("hello/greet")
4874 .with_instructions("Pass the user's name")
4875 .with_parameters(json!({
4876 "type": "object",
4877 "properties": { "name": { "type": "string" } },
4878 "required": ["name"]
4879 }))
4880 .with_overrides_built_in_tool(true)
4881 .with_skip_permission(true);
4882 assert_eq!(tool.name, "greet");
4883 assert_eq!(tool.description, "Say hello");
4884 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
4885 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
4886 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
4887 assert!(tool.overrides_built_in_tool);
4888 assert!(tool.skip_permission);
4889 }
4890
4891 #[test]
4892 fn tool_defer_serialization() {
4893 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
4894 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
4895 let value = serde_json::to_value(&tool).unwrap();
4896 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
4897
4898 let plain = Tool::new("plain");
4899 let value = serde_json::to_value(&plain).unwrap();
4900 assert!(value.get("defer").is_none());
4901 }
4902
4903 #[test]
4904 fn custom_agent_config_builder_with_model() {
4905 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
4906 .with_model("claude-haiku-4.5")
4907 .with_display_name("My Agent");
4908 assert_eq!(agent.name, "my-agent");
4909 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
4910 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
4911 }
4912
4913 #[test]
4914 fn custom_agent_config_serializes_model() {
4915 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
4916 let wire = serde_json::to_value(&agent).unwrap();
4917 assert_eq!(wire["model"], "claude-haiku-4.5");
4918 assert_eq!(wire["name"], "model-agent");
4919 }
4920
4921 #[test]
4922 fn custom_agent_config_omits_model_when_none() {
4923 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
4924 let wire = serde_json::to_value(&agent).unwrap();
4925 assert!(wire.get("model").is_none());
4926 }
4927
4928 #[test]
4929 #[should_panic(expected = "tool parameter schema must be a JSON object")]
4930 fn tool_with_parameters_panics_on_non_object_value() {
4931 let _ = Tool::new("noop").with_parameters(json!(null));
4932 }
4933
4934 #[test]
4935 fn tool_result_expanded_serializes_binary_results_for_llm() {
4936 let response = ToolResultResponse {
4937 result: ToolResult::Expanded(ToolResultExpanded {
4938 text_result_for_llm: "rendered chart".to_string(),
4939 result_type: "success".to_string(),
4940 binary_results_for_llm: Some(vec![ToolBinaryResult {
4941 data: "aW1n".to_string(),
4942 mime_type: "image/png".to_string(),
4943 r#type: "image".to_string(),
4944 description: Some("chart preview".to_string()),
4945 }]),
4946 session_log: None,
4947 error: None,
4948 tool_telemetry: None,
4949 }),
4950 };
4951
4952 let wire = serde_json::to_value(&response).unwrap();
4953
4954 assert_eq!(
4955 wire,
4956 json!({
4957 "result": {
4958 "textResultForLlm": "rendered chart",
4959 "resultType": "success",
4960 "binaryResultsForLlm": [
4961 {
4962 "data": "aW1n",
4963 "mimeType": "image/png",
4964 "type": "image",
4965 "description": "chart preview"
4966 }
4967 ]
4968 }
4969 })
4970 );
4971 }
4972
4973 #[test]
4974 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
4975 let response = ToolResultResponse {
4976 result: ToolResult::Expanded(ToolResultExpanded {
4977 text_result_for_llm: "ok".to_string(),
4978 result_type: "success".to_string(),
4979 binary_results_for_llm: None,
4980 session_log: None,
4981 error: None,
4982 tool_telemetry: None,
4983 }),
4984 };
4985
4986 let wire = serde_json::to_value(&response).unwrap();
4987
4988 assert_eq!(wire["result"]["textResultForLlm"], "ok");
4989 assert!(wire["result"].get("binaryResultsForLlm").is_none());
4990 }
4991
4992 #[test]
4993 fn session_config_default_wire_flags_off_without_handlers() {
4994 let cfg = SessionConfig::default();
4995 assert_eq!(cfg.mcp_oauth_token_storage, None);
4996 let (wire, _runtime) = cfg
5000 .into_wire(Some(SessionId::from("default-flags")))
5001 .expect("default config has no duplicate handlers");
5002 assert!(!wire.request_user_input);
5003 assert!(!wire.request_permission);
5004 assert!(!wire.request_elicitation);
5005 assert!(!wire.request_exit_plan_mode);
5006 assert!(!wire.request_auto_mode_switch);
5007 assert!(!wire.hooks);
5008 assert!(!wire.request_mcp_apps);
5009 }
5010
5011 #[test]
5012 fn resume_session_config_new_wire_flags_off_without_handlers() {
5013 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
5014 assert_eq!(cfg.mcp_oauth_token_storage, None);
5015 let (wire, _runtime) = cfg
5016 .into_wire()
5017 .expect("default resume config has no duplicate handlers");
5018 assert!(!wire.request_user_input);
5019 assert!(!wire.request_permission);
5020 assert!(!wire.request_elicitation);
5021 assert!(!wire.request_exit_plan_mode);
5022 assert!(!wire.request_auto_mode_switch);
5023 assert!(!wire.hooks);
5024 assert!(!wire.request_mcp_apps);
5025 }
5026
5027 #[test]
5028 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5029 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
5030 assert_eq!(cfg.enable_mcp_apps, Some(true));
5031
5032 let (wire, _runtime) = cfg
5033 .into_wire(Some(SessionId::from("enable-mcp-apps")))
5034 .expect("enable_mcp_apps config has no duplicate handlers");
5035 assert!(wire.request_mcp_apps);
5036
5037 let json = serde_json::to_value(&wire).unwrap();
5038 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5039 }
5040
5041 #[test]
5042 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5043 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
5044 .with_enable_mcp_apps(true);
5045 assert_eq!(cfg.enable_mcp_apps, Some(true));
5046
5047 let (wire, _runtime) = cfg
5048 .into_wire()
5049 .expect("resume enable_mcp_apps config has no duplicate handlers");
5050 assert!(wire.request_mcp_apps);
5051
5052 let json = serde_json::to_value(&wire).unwrap();
5053 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5054 }
5055
5056 #[test]
5057 fn memory_configuration_constructors_and_serde() {
5058 assert!(MemoryConfiguration::enabled().enabled);
5059 assert!(!MemoryConfiguration::disabled().enabled);
5060 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
5061
5062 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
5063 assert_eq!(json, serde_json::json!({ "enabled": true }));
5064 }
5065
5066 #[test]
5067 fn session_config_with_memory_serializes() {
5068 let (wire, _runtime) = SessionConfig::default()
5069 .with_memory(MemoryConfiguration::enabled())
5070 .into_wire(Some(SessionId::from("memory-on")))
5071 .expect("no duplicate handlers");
5072 let json = serde_json::to_value(&wire).unwrap();
5073 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5074
5075 let (wire_off, _) = SessionConfig::default()
5076 .with_memory(MemoryConfiguration::disabled())
5077 .into_wire(Some(SessionId::from("memory-off")))
5078 .expect("no duplicate handlers");
5079 let json_off = serde_json::to_value(&wire_off).unwrap();
5080 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
5081
5082 let (empty_wire, _) = SessionConfig::default()
5084 .into_wire(Some(SessionId::from("memory-unset")))
5085 .expect("no duplicate handlers");
5086 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5087 assert!(empty_json.get("memory").is_none());
5088 }
5089
5090 #[test]
5091 fn resume_session_config_with_memory_serializes() {
5092 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
5093 .with_memory(MemoryConfiguration::enabled())
5094 .into_wire()
5095 .expect("no duplicate handlers");
5096 let json = serde_json::to_value(&wire).unwrap();
5097 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5098
5099 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
5101 .into_wire()
5102 .expect("no duplicate handlers");
5103 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5104 assert!(empty_json.get("memory").is_none());
5105 }
5106
5107 #[test]
5108 fn session_config_with_exp_assignments_serializes() {
5109 let assignments = serde_json::json!({
5110 "Parameters": { "copilot_exp_flag": "treatment" },
5111 "AssignmentContext": "ctx-123",
5112 });
5113 let (wire, _runtime) = SessionConfig::default()
5114 .with_exp_assignments(assignments.clone())
5115 .into_wire(Some(SessionId::from("exp-on")))
5116 .expect("no duplicate handlers");
5117 let json = serde_json::to_value(&wire).unwrap();
5118 assert_eq!(json["expAssignments"], assignments);
5119
5120 let (empty_wire, _) = SessionConfig::default()
5122 .into_wire(Some(SessionId::from("exp-unset")))
5123 .expect("no duplicate handlers");
5124 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5125 assert!(empty_json.get("expAssignments").is_none());
5126 }
5127
5128 #[test]
5129 fn resume_session_config_with_exp_assignments_serializes() {
5130 let assignments = serde_json::json!({
5131 "Parameters": { "copilot_exp_flag": "treatment" },
5132 "AssignmentContext": "ctx-456",
5133 });
5134 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
5135 .with_exp_assignments(assignments.clone())
5136 .into_wire()
5137 .expect("no duplicate handlers");
5138 let json = serde_json::to_value(&wire).unwrap();
5139 assert_eq!(json["expAssignments"], assignments);
5140
5141 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
5143 .into_wire()
5144 .expect("no duplicate handlers");
5145 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5146 assert!(empty_json.get("expAssignments").is_none());
5147 }
5148
5149 #[test]
5150 fn session_config_clone_preserves_exp_assignments() {
5151 let assignments = serde_json::json!({
5152 "Parameters": { "copilot_exp_flag": "treatment" },
5153 "AssignmentContext": "ctx-clone",
5154 });
5155 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
5156 let cloned = config.clone();
5157
5158 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5159
5160 let (wire, _runtime) = cloned
5161 .into_wire(Some(SessionId::from("exp-clone")))
5162 .expect("no duplicate handlers");
5163 let json = serde_json::to_value(&wire).unwrap();
5164 assert_eq!(json["expAssignments"], assignments);
5165 }
5166
5167 #[test]
5168 fn resume_session_config_clone_preserves_exp_assignments() {
5169 let assignments = serde_json::json!({
5170 "Parameters": { "copilot_exp_flag": "treatment" },
5171 "AssignmentContext": "ctx-clone-resume",
5172 });
5173 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
5174 .with_exp_assignments(assignments.clone());
5175 let cloned = config.clone();
5176
5177 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5178
5179 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
5180 let json = serde_json::to_value(&wire).unwrap();
5181 assert_eq!(json["expAssignments"], assignments);
5182 }
5183
5184 #[test]
5185 #[allow(clippy::field_reassign_with_default)]
5186 fn session_config_into_wire_serializes_bucket_b_fields() {
5187 use std::path::PathBuf;
5188
5189 use super::{CloudSessionOptions, CloudSessionRepository};
5190
5191 let mut cfg = SessionConfig::default();
5192 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5193 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5194 cfg.github_token = Some("ghs_secret".to_string());
5195 cfg.include_sub_agent_streaming_events = Some(false);
5196 cfg.enable_session_telemetry = Some(false);
5197 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
5198 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
5199 cfg.enable_on_demand_instruction_discovery = Some(false);
5200 cfg.cloud = Some(CloudSessionOptions::with_repository(
5201 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
5202 ));
5203
5204 let (wire, _runtime) = cfg
5205 .into_wire(Some(SessionId::from("custom-id")))
5206 .expect("no duplicate handlers");
5207 let wire_json = serde_json::to_value(&wire).unwrap();
5208 assert_eq!(wire_json["sessionId"], "custom-id");
5209 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5210 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5211 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5212 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
5213 assert_eq!(wire_json["enableSessionTelemetry"], false);
5214 assert_eq!(wire_json["reasoningSummary"], "concise");
5215 assert_eq!(wire_json["remoteSession"], "export");
5216 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5217 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
5218 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
5219 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
5220
5221 let (empty_wire, _) = SessionConfig::default()
5223 .into_wire(Some(SessionId::from("empty")))
5224 .expect("default has no duplicate handlers");
5225 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5226 assert!(empty_json.get("gitHubToken").is_none());
5227 assert!(empty_json.get("enableSessionTelemetry").is_none());
5228 assert!(empty_json.get("reasoningSummary").is_none());
5229 assert!(empty_json.get("remoteSession").is_none());
5230 assert!(
5231 empty_json
5232 .get("enableOnDemandInstructionDiscovery")
5233 .is_none()
5234 );
5235 assert!(empty_json.get("cloud").is_none());
5236 }
5237
5238 #[test]
5239 fn session_config_into_wire_serializes_named_providers_and_models() {
5240 let cfg = SessionConfig::default()
5241 .with_providers(vec![
5242 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
5243 .with_provider_type("openai")
5244 .with_wire_api("responses")
5245 .with_api_key("sk-test"),
5246 ])
5247 .with_models(vec![
5248 ProviderModelConfig::new("gpt-x", "my-openai")
5249 .with_wire_model("gpt-x-2025")
5250 .with_max_output_tokens(2048),
5251 ]);
5252
5253 let (wire, _) = cfg
5254 .into_wire(Some(SessionId::from("sess-providers")))
5255 .expect("no duplicate handlers");
5256 let wire_json = serde_json::to_value(&wire).unwrap();
5257 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
5258 assert_eq!(
5259 wire_json["providers"][0]["baseUrl"],
5260 "https://api.example.com/v1"
5261 );
5262 assert_eq!(wire_json["providers"][0]["type"], "openai");
5263 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
5264 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
5265 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
5266 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
5267 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
5268 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
5269
5270 let (empty_wire, _) = SessionConfig::default()
5271 .into_wire(Some(SessionId::from("empty")))
5272 .expect("default has no duplicate handlers");
5273 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5274 assert!(empty_json.get("providers").is_none());
5275 assert!(empty_json.get("models").is_none());
5276 }
5277
5278 #[test]
5279 fn resume_config_into_wire_serializes_named_providers_and_models() {
5280 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
5281 .with_providers(vec![
5282 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
5283 .with_provider_type("azure")
5284 .with_azure(AzureProviderOptions {
5285 api_version: Some("2024-10-21".to_string()),
5286 }),
5287 ])
5288 .with_models(vec![
5289 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
5290 ]);
5291
5292 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5293 let wire_json = serde_json::to_value(&wire).unwrap();
5294 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
5295 assert_eq!(wire_json["providers"][0]["type"], "azure");
5296 assert_eq!(
5297 wire_json["providers"][0]["azure"]["apiVersion"],
5298 "2024-10-21"
5299 );
5300 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
5301 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
5302 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
5303
5304 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
5305 .into_wire()
5306 .expect("default has no duplicate handlers");
5307 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5308 assert!(empty_json.get("providers").is_none());
5309 assert!(empty_json.get("models").is_none());
5310 }
5311
5312 #[test]
5313 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
5314 use std::path::PathBuf;
5315
5316 let cfg = SessionConfig {
5317 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
5318 large_output: Some(
5319 LargeToolOutputConfig::new()
5320 .with_enabled(true)
5321 .with_max_size_bytes(1024)
5322 .with_output_directory(PathBuf::from("/tmp/large-output")),
5323 ),
5324 ..Default::default()
5325 };
5326
5327 let (wire, _) = cfg
5328 .into_wire(Some(SessionId::from("sess-1")))
5329 .expect("no duplicate handlers");
5330 let wire_json = serde_json::to_value(&wire).unwrap();
5331 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
5332 assert_eq!(wire_json["largeOutput"]["enabled"], true);
5333 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
5334 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
5335
5336 let (empty_wire, _) = SessionConfig::default()
5337 .into_wire(Some(SessionId::from("empty")))
5338 .expect("default has no duplicate handlers");
5339 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5340 assert!(empty_json.get("pluginDirectories").is_none());
5341 assert!(empty_json.get("largeOutput").is_none());
5342 }
5343
5344 #[test]
5345 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
5346 use std::path::PathBuf;
5347
5348 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5349 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5350 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5351 cfg.github_token = Some("ghs_secret".to_string());
5352 cfg.include_sub_agent_streaming_events = Some(true);
5353 cfg.enable_session_telemetry = Some(false);
5354 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
5355 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
5356 cfg.enable_on_demand_instruction_discovery = Some(false);
5357
5358 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5359 let wire_json = serde_json::to_value(&wire).unwrap();
5360 assert_eq!(wire_json["sessionId"], "sess-1");
5361 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5362 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5363 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5364 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
5365 assert_eq!(wire_json["enableSessionTelemetry"], false);
5366 assert_eq!(wire_json["reasoningSummary"], "detailed");
5367 assert_eq!(wire_json["remoteSession"], "on");
5368 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5369
5370 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5372 .into_wire()
5373 .expect("default resume has no duplicate handlers");
5374 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5375 assert!(empty_json.get("reasoningSummary").is_none());
5376 assert!(empty_json.get("remoteSession").is_none());
5377 assert!(
5378 empty_json
5379 .get("enableOnDemandInstructionDiscovery")
5380 .is_none()
5381 );
5382 }
5383
5384 #[test]
5385 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
5386 use std::path::PathBuf;
5387
5388 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5389 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
5390 cfg.large_output = Some(
5391 LargeToolOutputConfig::new()
5392 .with_enabled(false)
5393 .with_max_size_bytes(2048)
5394 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
5395 );
5396
5397 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5398 let wire_json = serde_json::to_value(&wire).unwrap();
5399 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
5400 assert_eq!(wire_json["largeOutput"]["enabled"], false);
5401 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
5402 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
5403
5404 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5405 .into_wire()
5406 .expect("default resume has no duplicate handlers");
5407 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5408 assert!(empty_json.get("pluginDirectories").is_none());
5409 assert!(empty_json.get("largeOutput").is_none());
5410 }
5411
5412 #[test]
5413 fn session_config_builder_composes() {
5414 use std::collections::HashMap;
5415
5416 let cfg = SessionConfig::default()
5417 .with_session_id(SessionId::from("sess-1"))
5418 .with_model("claude-sonnet-4")
5419 .with_client_name("test-app")
5420 .with_reasoning_effort("medium")
5421 .with_reasoning_summary(ReasoningSummary::Concise)
5422 .with_context_tier("long_context")
5423 .with_streaming(true)
5424 .with_tools([Tool::new("greet")])
5425 .with_available_tools(["bash", "view"])
5426 .with_excluded_tools(["dangerous"])
5427 .with_mcp_servers(HashMap::new())
5428 .with_mcp_oauth_token_storage("persistent")
5429 .with_enable_config_discovery(true)
5430 .with_enable_on_demand_instruction_discovery(true)
5431 .with_skill_directories([PathBuf::from("/tmp/skills")])
5432 .with_disabled_skills(["broken-skill"])
5433 .with_agent("researcher")
5434 .with_config_directory(PathBuf::from("/tmp/config"))
5435 .with_working_directory(PathBuf::from("/tmp/work"))
5436 .with_github_token("ghp_test")
5437 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5438 .with_enable_session_telemetry(false)
5439 .with_include_sub_agent_streaming_events(false)
5440 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5441
5442 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
5443 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
5444 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5445 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
5446 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
5447 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
5448 assert_eq!(cfg.streaming, Some(true));
5449 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5450 assert_eq!(
5451 cfg.available_tools.as_deref(),
5452 Some(&["bash".to_string(), "view".to_string()][..])
5453 );
5454 assert_eq!(
5455 cfg.excluded_tools.as_deref(),
5456 Some(&["dangerous".to_string()][..])
5457 );
5458 assert!(cfg.mcp_servers.is_some());
5459 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5460 assert_eq!(cfg.enable_config_discovery, Some(true));
5461 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
5462 assert_eq!(
5463 cfg.skill_directories.as_deref(),
5464 Some(&[PathBuf::from("/tmp/skills")][..])
5465 );
5466 assert_eq!(
5467 cfg.disabled_skills.as_deref(),
5468 Some(&["broken-skill".to_string()][..])
5469 );
5470 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5471 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5472 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5473 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5474 assert_eq!(
5475 cfg.capi,
5476 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5477 );
5478 assert_eq!(cfg.enable_session_telemetry, Some(false));
5479 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
5480 assert_eq!(
5481 cfg.extension_info,
5482 Some(ExtensionInfo::new("github-app", "counter"))
5483 );
5484 }
5485
5486 #[test]
5487 fn resume_session_config_builder_composes() {
5488 use std::collections::HashMap;
5489
5490 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
5491 .with_client_name("test-app")
5492 .with_reasoning_summary(ReasoningSummary::None)
5493 .with_context_tier("default")
5494 .with_streaming(true)
5495 .with_tools([Tool::new("greet")])
5496 .with_available_tools(["bash", "view"])
5497 .with_excluded_tools(["dangerous"])
5498 .with_mcp_servers(HashMap::new())
5499 .with_mcp_oauth_token_storage("persistent")
5500 .with_enable_config_discovery(true)
5501 .with_enable_on_demand_instruction_discovery(false)
5502 .with_skill_directories([PathBuf::from("/tmp/skills")])
5503 .with_disabled_skills(["broken-skill"])
5504 .with_agent("researcher")
5505 .with_config_directory(PathBuf::from("/tmp/config"))
5506 .with_working_directory(PathBuf::from("/tmp/work"))
5507 .with_github_token("ghp_test")
5508 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5509 .with_enable_session_telemetry(false)
5510 .with_include_sub_agent_streaming_events(true)
5511 .with_suppress_resume_event(true)
5512 .with_continue_pending_work(true)
5513 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5514
5515 assert_eq!(cfg.session_id.as_str(), "sess-2");
5516 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5517 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
5518 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
5519 assert_eq!(cfg.streaming, Some(true));
5520 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5521 assert_eq!(
5522 cfg.available_tools.as_deref(),
5523 Some(&["bash".to_string(), "view".to_string()][..])
5524 );
5525 assert_eq!(
5526 cfg.excluded_tools.as_deref(),
5527 Some(&["dangerous".to_string()][..])
5528 );
5529 assert!(cfg.mcp_servers.is_some());
5530 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5531 assert_eq!(cfg.enable_config_discovery, Some(true));
5532 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
5533 assert_eq!(
5534 cfg.skill_directories.as_deref(),
5535 Some(&[PathBuf::from("/tmp/skills")][..])
5536 );
5537 assert_eq!(
5538 cfg.disabled_skills.as_deref(),
5539 Some(&["broken-skill".to_string()][..])
5540 );
5541 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5542 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5543 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5544 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5545 assert_eq!(
5546 cfg.capi,
5547 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5548 );
5549 assert_eq!(cfg.enable_session_telemetry, Some(false));
5550 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
5551 assert_eq!(cfg.suppress_resume_event, Some(true));
5552 assert_eq!(cfg.continue_pending_work, Some(true));
5553 assert_eq!(
5554 cfg.extension_info,
5555 Some(ExtensionInfo::new("github-app", "counter"))
5556 );
5557 }
5558
5559 #[test]
5563 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
5564 let cfg =
5565 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
5566 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5567 let json = serde_json::to_value(&wire).unwrap();
5568 assert_eq!(json["continuePendingWork"], true);
5569
5570 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5572 .into_wire()
5573 .expect("no duplicate handlers");
5574 let json = serde_json::to_value(&wire).unwrap();
5575 assert!(json.get("continuePendingWork").is_none());
5576 }
5577
5578 #[test]
5582 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
5583 let cfg =
5584 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
5585 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5586 let json = serde_json::to_value(&wire).unwrap();
5587 assert_eq!(json["disableResume"], true);
5588 assert!(json.get("suppressResumeEvent").is_none());
5589 }
5590
5591 #[test]
5594 fn session_config_serializes_instruction_directories_to_camel_case() {
5595 let cfg =
5596 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
5597 let (wire, _) = cfg
5598 .into_wire(Some(SessionId::from("instr-on")))
5599 .expect("no duplicate handlers");
5600 let json = serde_json::to_value(&wire).unwrap();
5601 assert_eq!(
5602 json["instructionDirectories"],
5603 serde_json::json!(["/tmp/instr"])
5604 );
5605
5606 let (wire, _) = SessionConfig::default()
5608 .into_wire(Some(SessionId::from("instr-off")))
5609 .expect("no duplicate handlers");
5610 let json = serde_json::to_value(&wire).unwrap();
5611 assert!(json.get("instructionDirectories").is_none());
5612 }
5613
5614 #[test]
5617 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
5618 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
5619 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
5620 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5621 let json = serde_json::to_value(&wire).unwrap();
5622 assert_eq!(
5623 json["instructionDirectories"],
5624 serde_json::json!(["/tmp/instr"])
5625 );
5626
5627 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5628 .into_wire()
5629 .expect("no duplicate handlers");
5630 let json = serde_json::to_value(&wire).unwrap();
5631 assert!(json.get("instructionDirectories").is_none());
5632 }
5633
5634 #[test]
5635 fn custom_agent_config_builder_composes() {
5636 use std::collections::HashMap;
5637
5638 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
5639 .with_display_name("Research Assistant")
5640 .with_description("Investigates technical questions.")
5641 .with_tools(["bash", "view"])
5642 .with_mcp_servers(HashMap::new())
5643 .with_infer(true)
5644 .with_skills(["rust-coding-skill"]);
5645
5646 assert_eq!(cfg.name, "researcher");
5647 assert_eq!(cfg.prompt, "You are a research assistant.");
5648 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
5649 assert_eq!(
5650 cfg.description.as_deref(),
5651 Some("Investigates technical questions.")
5652 );
5653 assert_eq!(
5654 cfg.tools.as_deref(),
5655 Some(&["bash".to_string(), "view".to_string()][..])
5656 );
5657 assert!(cfg.mcp_servers.is_some());
5658 assert_eq!(cfg.infer, Some(true));
5659 assert_eq!(
5660 cfg.skills.as_deref(),
5661 Some(&["rust-coding-skill".to_string()][..])
5662 );
5663 }
5664
5665 #[test]
5666 fn infinite_session_config_builder_composes() {
5667 let cfg = InfiniteSessionConfig::new()
5668 .with_enabled(true)
5669 .with_background_compaction_threshold(0.75)
5670 .with_buffer_exhaustion_threshold(0.92);
5671
5672 assert_eq!(cfg.enabled, Some(true));
5673 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
5674 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
5675 }
5676
5677 #[test]
5678 fn provider_config_builder_composes() {
5679 use std::collections::HashMap;
5680
5681 let mut headers = HashMap::new();
5682 headers.insert("X-Custom".to_string(), "value".to_string());
5683
5684 let cfg = ProviderConfig::new("https://api.example.com")
5685 .with_provider_type("openai")
5686 .with_wire_api("completions")
5687 .with_transport("websockets")
5688 .with_api_key("sk-test")
5689 .with_bearer_token("bearer-test")
5690 .with_headers(headers)
5691 .with_model_id("gpt-4")
5692 .with_wire_model("azure-gpt-4-deployment")
5693 .with_max_prompt_tokens(8192)
5694 .with_max_output_tokens(2048);
5695
5696 assert_eq!(cfg.base_url, "https://api.example.com");
5697 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
5698 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
5699 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
5700 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
5701 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
5702 assert_eq!(
5703 cfg.headers
5704 .as_ref()
5705 .and_then(|h| h.get("X-Custom"))
5706 .map(String::as_str),
5707 Some("value"),
5708 );
5709 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
5710 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
5711 assert_eq!(cfg.max_prompt_tokens, Some(8192));
5712 assert_eq!(cfg.max_output_tokens, Some(2048));
5713
5714 let wire = serde_json::to_value(&cfg).unwrap();
5716 assert_eq!(wire["modelId"], "gpt-4");
5717 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
5718 assert_eq!(wire["maxPromptTokens"], 8192);
5719 assert_eq!(wire["maxOutputTokens"], 2048);
5720
5721 let unset = ProviderConfig::new("https://api.example.com");
5722 let wire_unset = serde_json::to_value(&unset).unwrap();
5723 assert!(wire_unset.get("modelId").is_none());
5724 assert!(wire_unset.get("wireModel").is_none());
5725 assert!(wire_unset.get("maxPromptTokens").is_none());
5726 assert!(wire_unset.get("maxOutputTokens").is_none());
5727 }
5728
5729 #[test]
5730 fn capi_session_options_builder_composes_and_serializes() {
5731 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
5732
5733 assert_eq!(cfg.enable_web_socket_responses, Some(false));
5734
5735 let wire = serde_json::to_value(&cfg).unwrap();
5736 assert_eq!(
5737 wire,
5738 serde_json::json!({ "enableWebSocketResponses": false })
5739 );
5740
5741 let unset = CapiSessionOptions::new();
5742 let wire_unset = serde_json::to_value(&unset).unwrap();
5743 assert!(wire_unset.get("enableWebSocketResponses").is_none());
5744 }
5745
5746 #[test]
5747 fn session_config_with_capi_serializes() {
5748 let (wire, _) = SessionConfig::default()
5749 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5750 .into_wire(Some(SessionId::from("capi-create")))
5751 .expect("no duplicate handlers");
5752 let json = serde_json::to_value(&wire).unwrap();
5753 assert_eq!(
5754 json["capi"],
5755 serde_json::json!({ "enableWebSocketResponses": false })
5756 );
5757
5758 let (empty_wire, _) = SessionConfig::default()
5759 .into_wire(Some(SessionId::from("capi-create-unset")))
5760 .expect("no duplicate handlers");
5761 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5762 assert!(empty_json.get("capi").is_none());
5763 }
5764
5765 #[test]
5766 fn resume_session_config_with_capi_serializes() {
5767 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
5768 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5769 .into_wire()
5770 .expect("no duplicate handlers");
5771 let json = serde_json::to_value(&wire).unwrap();
5772 assert_eq!(
5773 json["capi"],
5774 serde_json::json!({ "enableWebSocketResponses": false })
5775 );
5776
5777 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
5778 .into_wire()
5779 .expect("no duplicate handlers");
5780 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5781 assert!(empty_json.get("capi").is_none());
5782 }
5783
5784 #[test]
5785 fn system_message_config_builder_composes() {
5786 use std::collections::HashMap;
5787
5788 let cfg = SystemMessageConfig::new()
5789 .with_mode("replace")
5790 .with_content("Custom system message.")
5791 .with_sections(HashMap::new());
5792
5793 assert_eq!(cfg.mode.as_deref(), Some("replace"));
5794 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
5795 assert!(cfg.sections.is_some());
5796 }
5797
5798 #[test]
5799 fn delivery_mode_serializes_to_kebab_case_strings() {
5800 assert_eq!(
5801 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
5802 "\"enqueue\""
5803 );
5804 assert_eq!(
5805 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
5806 "\"immediate\""
5807 );
5808 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
5809 assert_eq!(parsed, DeliveryMode::Immediate);
5810 }
5811
5812 #[test]
5813 fn agent_mode_serializes_to_kebab_case_strings() {
5814 assert_eq!(
5815 serde_json::to_string(&AgentMode::Interactive).unwrap(),
5816 "\"interactive\""
5817 );
5818 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
5819 assert_eq!(
5820 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
5821 "\"autopilot\""
5822 );
5823 assert_eq!(
5824 serde_json::to_string(&AgentMode::Shell).unwrap(),
5825 "\"shell\""
5826 );
5827 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
5828 assert_eq!(parsed, AgentMode::Plan);
5829 }
5830
5831 #[test]
5832 fn connection_state_distinguishes_variants() {
5833 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
5836 }
5837
5838 #[test]
5844 fn session_event_round_trips_agent_id_on_envelope() {
5845 let wire = json!({
5846 "id": "evt-1",
5847 "timestamp": "2026-04-30T12:00:00Z",
5848 "parentId": null,
5849 "agentId": "sub-agent-42",
5850 "type": "assistant.message",
5851 "data": { "message": "hi" }
5852 });
5853
5854 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
5855 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
5856
5857 let roundtripped = serde_json::to_value(&event).unwrap();
5859 assert_eq!(roundtripped["agentId"], "sub-agent-42");
5860
5861 let main_agent_event: SessionEvent = serde_json::from_value(json!({
5863 "id": "evt-2",
5864 "timestamp": "2026-04-30T12:00:01Z",
5865 "parentId": null,
5866 "type": "session.idle",
5867 "data": {}
5868 }))
5869 .unwrap();
5870 assert!(main_agent_event.agent_id.is_none());
5871 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
5872 assert!(roundtripped.get("agentId").is_none());
5873 }
5874
5875 #[test]
5877 fn typed_session_event_round_trips_agent_id_on_envelope() {
5878 let wire = json!({
5879 "id": "evt-1",
5880 "timestamp": "2026-04-30T12:00:00Z",
5881 "parentId": null,
5882 "agentId": "sub-agent-42",
5883 "type": "session.idle",
5884 "data": {}
5885 });
5886
5887 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
5888 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
5889
5890 let roundtripped = serde_json::to_value(&event).unwrap();
5891 assert_eq!(roundtripped["agentId"], "sub-agent-42");
5892 }
5893
5894 #[test]
5895 fn connection_state_variants_compile() {
5896 let _ = ConnectionState::Disconnected;
5900 let _ = ConnectionState::Connecting;
5901 let _ = ConnectionState::Connected;
5902 let _ = ConnectionState::Error;
5903 }
5904
5905 #[test]
5906 fn deserializes_runtime_attachment_variants() {
5907 let attachments: Vec<Attachment> = serde_json::from_value(json!([
5908 {
5909 "type": "file",
5910 "path": "/tmp/file.rs",
5911 "displayName": "file.rs",
5912 "lineRange": { "start": 7, "end": 12 }
5913 },
5914 {
5915 "type": "directory",
5916 "path": "/tmp/project",
5917 "displayName": "project"
5918 },
5919 {
5920 "type": "selection",
5921 "filePath": "/tmp/lib.rs",
5922 "displayName": "lib.rs",
5923 "text": "fn main() {}",
5924 "selection": {
5925 "start": { "line": 1, "character": 2 },
5926 "end": { "line": 3, "character": 4 }
5927 }
5928 },
5929 {
5930 "type": "blob",
5931 "data": "Zm9v",
5932 "mimeType": "image/png",
5933 "displayName": "image.png"
5934 },
5935 {
5936 "type": "github_reference",
5937 "number": 42,
5938 "title": "Fix rendering",
5939 "referenceType": "issue",
5940 "state": "open",
5941 "url": "https://github.com/example/repo/issues/42"
5942 }
5943 ]))
5944 .expect("attachments should deserialize");
5945
5946 assert_eq!(attachments.len(), 5);
5947 assert!(matches!(
5948 &attachments[0],
5949 Attachment::File {
5950 path,
5951 display_name,
5952 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
5953 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
5954 ));
5955 assert!(matches!(
5956 &attachments[1],
5957 Attachment::Directory { path, display_name }
5958 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
5959 ));
5960 assert!(matches!(
5961 &attachments[2],
5962 Attachment::Selection {
5963 file_path,
5964 display_name,
5965 selection:
5966 AttachmentSelectionRange {
5967 start: AttachmentSelectionPosition { line: 1, character: 2 },
5968 end: AttachmentSelectionPosition { line: 3, character: 4 },
5969 },
5970 ..
5971 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
5972 ));
5973 assert!(matches!(
5974 &attachments[3],
5975 Attachment::Blob {
5976 data,
5977 mime_type,
5978 display_name,
5979 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
5980 ));
5981 assert!(matches!(
5982 &attachments[4],
5983 Attachment::GitHubReference {
5984 number: 42,
5985 title,
5986 reference_type: GitHubReferenceType::Issue,
5987 state,
5988 url,
5989 } if title == "Fix rendering"
5990 && state == "open"
5991 && url == "https://github.com/example/repo/issues/42"
5992 ));
5993 }
5994
5995 #[test]
5996 fn ensures_display_names_for_variants_that_support_them() {
5997 let mut attachments = vec![
5998 Attachment::File {
5999 path: PathBuf::from("/tmp/file.rs"),
6000 display_name: None,
6001 line_range: None,
6002 },
6003 Attachment::Selection {
6004 file_path: PathBuf::from("/tmp/src/lib.rs"),
6005 display_name: None,
6006 text: "fn main() {}".to_string(),
6007 selection: AttachmentSelectionRange {
6008 start: AttachmentSelectionPosition {
6009 line: 0,
6010 character: 0,
6011 },
6012 end: AttachmentSelectionPosition {
6013 line: 0,
6014 character: 10,
6015 },
6016 },
6017 },
6018 Attachment::Blob {
6019 data: "Zm9v".to_string(),
6020 mime_type: "image/png".to_string(),
6021 display_name: None,
6022 },
6023 Attachment::GitHubReference {
6024 number: 7,
6025 title: "Track regressions".to_string(),
6026 reference_type: GitHubReferenceType::Issue,
6027 state: "open".to_string(),
6028 url: "https://example.com/issues/7".to_string(),
6029 },
6030 ];
6031
6032 ensure_attachment_display_names(&mut attachments);
6033
6034 assert_eq!(attachments[0].display_name(), Some("file.rs"));
6035 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
6036 assert_eq!(attachments[2].display_name(), Some("attachment"));
6037 assert_eq!(attachments[3].display_name(), None);
6038 assert_eq!(
6039 attachments[3].label(),
6040 Some("Track regressions".to_string())
6041 );
6042 }
6043}
6044
6045#[cfg(test)]
6046mod permission_builder_tests {
6047 use std::sync::Arc;
6048
6049 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
6050 use crate::permission;
6051 use crate::types::{
6052 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
6053 SessionId,
6054 };
6055
6056 fn data() -> PermissionRequestData {
6057 PermissionRequestData {
6058 extra: serde_json::json!({"tool": "shell"}),
6059 ..Default::default()
6060 }
6061 }
6062
6063 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6066 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6067 }
6068
6069 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6070 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6071 }
6072
6073 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
6074 handler
6075 .handle(SessionId::from("s1"), RequestId::new("1"), data())
6076 .await
6077 }
6078
6079 #[tokio::test]
6080 async fn approve_all_with_handler_present_approves() {
6081 let cfg = SessionConfig::default()
6082 .with_permission_handler(Arc::new(ApproveAllHandler))
6083 .approve_all_permissions();
6084 let h = resolve_create(cfg).expect("policy + handler yields handler");
6085 assert!(matches!(
6086 dispatch(&h).await,
6087 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6088 ));
6089 }
6090
6091 #[tokio::test]
6092 async fn approve_all_standalone_produces_handler() {
6093 let cfg = SessionConfig::default().approve_all_permissions();
6094 let h = resolve_create(cfg).expect("policy alone yields handler");
6095 assert!(matches!(
6096 dispatch(&h).await,
6097 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6098 ));
6099 }
6100
6101 #[tokio::test]
6104 async fn approve_all_is_order_independent() {
6105 let a = SessionConfig::default()
6106 .with_permission_handler(Arc::new(ApproveAllHandler))
6107 .approve_all_permissions();
6108 let b = SessionConfig::default()
6109 .approve_all_permissions()
6110 .with_permission_handler(Arc::new(ApproveAllHandler));
6111 let ha = resolve_create(a).unwrap();
6112 let hb = resolve_create(b).unwrap();
6113 assert!(matches!(
6114 dispatch(&ha).await,
6115 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6116 ));
6117 assert!(matches!(
6118 dispatch(&hb).await,
6119 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6120 ));
6121 }
6122
6123 #[tokio::test]
6124 async fn deny_all_is_order_independent() {
6125 let a = SessionConfig::default()
6126 .with_permission_handler(Arc::new(ApproveAllHandler))
6127 .deny_all_permissions();
6128 let b = SessionConfig::default()
6129 .deny_all_permissions()
6130 .with_permission_handler(Arc::new(ApproveAllHandler));
6131 let ha = resolve_create(a).unwrap();
6132 let hb = resolve_create(b).unwrap();
6133 assert!(matches!(
6134 dispatch(&ha).await,
6135 PermissionResult::Decision(PermissionDecision::Reject(_))
6136 ));
6137 assert!(matches!(
6138 dispatch(&hb).await,
6139 PermissionResult::Decision(PermissionDecision::Reject(_))
6140 ));
6141 }
6142
6143 #[tokio::test]
6144 async fn approve_permissions_if_consults_predicate() {
6145 let cfg = SessionConfig::default().approve_permissions_if(|d| {
6146 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6147 });
6148 let h = resolve_create(cfg).unwrap();
6149 assert!(matches!(
6150 dispatch(&h).await,
6151 PermissionResult::Decision(PermissionDecision::Reject(_))
6152 ));
6153 }
6154
6155 #[tokio::test]
6156 async fn approve_permissions_if_is_order_independent() {
6157 let predicate = |d: &PermissionRequestData| {
6158 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6159 };
6160 let a = SessionConfig::default()
6161 .with_permission_handler(Arc::new(ApproveAllHandler))
6162 .approve_permissions_if(predicate);
6163 let b = SessionConfig::default()
6164 .approve_permissions_if(predicate)
6165 .with_permission_handler(Arc::new(ApproveAllHandler));
6166 let ha = resolve_create(a).unwrap();
6167 let hb = resolve_create(b).unwrap();
6168 assert!(matches!(
6169 dispatch(&ha).await,
6170 PermissionResult::Decision(PermissionDecision::Reject(_))
6171 ));
6172 assert!(matches!(
6173 dispatch(&hb).await,
6174 PermissionResult::Decision(PermissionDecision::Reject(_))
6175 ));
6176 }
6177
6178 #[tokio::test]
6179 async fn resume_session_config_approve_all_works() {
6180 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
6181 .with_permission_handler(Arc::new(ApproveAllHandler))
6182 .approve_all_permissions();
6183 let h = resolve_resume(cfg).unwrap();
6184 assert!(matches!(
6185 dispatch(&h).await,
6186 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6187 ));
6188 }
6189
6190 #[tokio::test]
6191 async fn resume_session_config_approve_all_is_order_independent() {
6192 let a = ResumeSessionConfig::new(SessionId::from("s1"))
6193 .with_permission_handler(Arc::new(ApproveAllHandler))
6194 .approve_all_permissions();
6195 let b = ResumeSessionConfig::new(SessionId::from("s1"))
6196 .approve_all_permissions()
6197 .with_permission_handler(Arc::new(ApproveAllHandler));
6198 let ha = resolve_resume(a).unwrap();
6199 let hb = resolve_resume(b).unwrap();
6200 assert!(matches!(
6201 dispatch(&ha).await,
6202 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6203 ));
6204 assert!(matches!(
6205 dispatch(&hb).await,
6206 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6207 ));
6208 }
6209}