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 bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1057 #[serde(default, skip_serializing_if = "Option::is_none")]
1058 pub(crate) has_bearer_token_provider: Option<bool>,
1059 #[serde(default, skip_serializing_if = "Option::is_none")]
1061 pub azure: Option<AzureProviderOptions>,
1062 #[serde(default, skip_serializing_if = "Option::is_none")]
1064 pub headers: Option<HashMap<String, String>>,
1065 #[serde(default, skip_serializing_if = "Option::is_none")]
1069 pub model_id: Option<String>,
1070 #[serde(default, skip_serializing_if = "Option::is_none")]
1077 pub wire_model: Option<String>,
1078 #[serde(default, skip_serializing_if = "Option::is_none")]
1083 pub max_prompt_tokens: Option<i64>,
1084 #[serde(default, skip_serializing_if = "Option::is_none")]
1087 pub max_output_tokens: Option<i64>,
1088}
1089
1090impl std::fmt::Debug for ProviderConfig {
1091 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1092 f.debug_struct("ProviderConfig")
1093 .field("provider_type", &self.provider_type)
1094 .field("wire_api", &self.wire_api)
1095 .field("transport", &self.transport)
1096 .field("base_url", &self.base_url)
1097 .field("api_key", &self.api_key)
1098 .field("bearer_token", &self.bearer_token)
1099 .field(
1100 "bearer_token_provider",
1101 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1102 )
1103 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1104 .field("azure", &self.azure)
1105 .field("headers", &self.headers)
1106 .field("model_id", &self.model_id)
1107 .field("wire_model", &self.wire_model)
1108 .field("max_prompt_tokens", &self.max_prompt_tokens)
1109 .field("max_output_tokens", &self.max_output_tokens)
1110 .finish()
1111 }
1112}
1113
1114impl ProviderConfig {
1115 pub fn new(base_url: impl Into<String>) -> Self {
1118 Self {
1119 base_url: base_url.into(),
1120 ..Self::default()
1121 }
1122 }
1123
1124 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1126 self.provider_type = Some(provider_type.into());
1127 self
1128 }
1129
1130 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1132 self.wire_api = Some(wire_api.into());
1133 self
1134 }
1135
1136 pub fn with_transport(mut self, transport: impl Into<String>) -> Self {
1139 self.transport = Some(transport.into());
1140 self
1141 }
1142
1143 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1145 self.api_key = Some(api_key.into());
1146 self
1147 }
1148
1149 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1152 self.bearer_token = Some(bearer_token.into());
1153 self
1154 }
1155
1156 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1162 self.bearer_token_provider = Some(provider);
1163 self
1164 }
1165
1166 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1168 self.azure = Some(azure);
1169 self
1170 }
1171
1172 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1174 self.headers = Some(headers);
1175 self
1176 }
1177
1178 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1181 self.model_id = Some(model_id.into());
1182 self
1183 }
1184
1185 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1190 self.wire_model = Some(wire_model.into());
1191 self
1192 }
1193
1194 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1198 self.max_prompt_tokens = Some(max);
1199 self
1200 }
1201
1202 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1205 self.max_output_tokens = Some(max);
1206 self
1207 }
1208}
1209
1210#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
1223#[serde(rename_all = "camelCase")]
1224#[non_exhaustive]
1225pub struct CapiSessionOptions {
1226 #[serde(default, skip_serializing_if = "Option::is_none")]
1232 pub enable_web_socket_responses: Option<bool>,
1233}
1234
1235impl CapiSessionOptions {
1236 pub fn new() -> Self {
1238 Self::default()
1239 }
1240
1241 pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self {
1243 self.enable_web_socket_responses = Some(enable);
1244 self
1245 }
1246}
1247
1248#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1250#[serde(rename_all = "camelCase")]
1251pub struct AzureProviderOptions {
1252 #[serde(default, skip_serializing_if = "Option::is_none")]
1254 pub api_version: Option<String>,
1255}
1256
1257#[derive(Clone, Default, Serialize, Deserialize)]
1268#[serde(rename_all = "camelCase")]
1269#[non_exhaustive]
1270pub struct NamedProviderConfig {
1271 pub name: String,
1274 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
1277 pub provider_type: Option<String>,
1278 #[serde(default, skip_serializing_if = "Option::is_none")]
1281 pub wire_api: Option<String>,
1282 pub base_url: String,
1284 #[serde(default, skip_serializing_if = "Option::is_none")]
1286 pub api_key: Option<String>,
1287 #[serde(default, skip_serializing_if = "Option::is_none")]
1290 pub bearer_token: Option<String>,
1291 #[serde(skip)]
1294 pub bearer_token_provider: Option<Arc<dyn BearerTokenProvider>>,
1295 #[serde(default, skip_serializing_if = "Option::is_none")]
1296 pub(crate) has_bearer_token_provider: Option<bool>,
1297 #[serde(default, skip_serializing_if = "Option::is_none")]
1299 pub azure: Option<AzureProviderOptions>,
1300 #[serde(default, skip_serializing_if = "Option::is_none")]
1302 pub headers: Option<HashMap<String, String>>,
1303}
1304
1305impl std::fmt::Debug for NamedProviderConfig {
1306 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1307 f.debug_struct("NamedProviderConfig")
1308 .field("name", &self.name)
1309 .field("provider_type", &self.provider_type)
1310 .field("wire_api", &self.wire_api)
1311 .field("base_url", &self.base_url)
1312 .field("api_key", &self.api_key)
1313 .field("bearer_token", &self.bearer_token)
1314 .field(
1315 "bearer_token_provider",
1316 &self.bearer_token_provider.as_ref().map(|_| "<set>"),
1317 )
1318 .field("has_bearer_token_provider", &self.has_bearer_token_provider)
1319 .field("azure", &self.azure)
1320 .field("headers", &self.headers)
1321 .finish()
1322 }
1323}
1324
1325impl NamedProviderConfig {
1326 pub fn new(name: impl Into<String>, base_url: impl Into<String>) -> Self {
1329 Self {
1330 name: name.into(),
1331 base_url: base_url.into(),
1332 ..Self::default()
1333 }
1334 }
1335
1336 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
1338 self.provider_type = Some(provider_type.into());
1339 self
1340 }
1341
1342 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
1344 self.wire_api = Some(wire_api.into());
1345 self
1346 }
1347
1348 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
1350 self.api_key = Some(api_key.into());
1351 self
1352 }
1353
1354 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
1357 self.bearer_token = Some(bearer_token.into());
1358 self
1359 }
1360
1361 pub fn with_bearer_token_provider(mut self, provider: Arc<dyn BearerTokenProvider>) -> Self {
1367 self.bearer_token_provider = Some(provider);
1368 self
1369 }
1370
1371 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
1373 self.azure = Some(azure);
1374 self
1375 }
1376
1377 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
1379 self.headers = Some(headers);
1380 self
1381 }
1382}
1383
1384fn prepare_bearer_token_providers(
1385 provider: &mut Option<ProviderConfig>,
1386 providers: &mut Option<Vec<NamedProviderConfig>>,
1387) -> HashMap<String, Arc<dyn BearerTokenProvider>> {
1388 let mut bearer_token_providers = HashMap::new();
1389
1390 if let Some(provider) = provider.as_mut()
1391 && let Some(token_provider) = provider.bearer_token_provider.take()
1392 {
1393 provider.has_bearer_token_provider = Some(true);
1394 bearer_token_providers.insert("default".to_string(), token_provider);
1395 }
1396
1397 if let Some(providers) = providers.as_mut() {
1398 for provider in providers {
1399 if let Some(token_provider) = provider.bearer_token_provider.take() {
1400 provider.has_bearer_token_provider = Some(true);
1401 bearer_token_providers.insert(provider.name.clone(), token_provider);
1402 }
1403 }
1404 }
1405
1406 bearer_token_providers
1407}
1408
1409#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1417#[serde(rename_all = "camelCase")]
1418#[non_exhaustive]
1419pub struct ProviderModelConfig {
1420 pub id: String,
1423 pub provider: String,
1425 #[serde(default, skip_serializing_if = "Option::is_none")]
1428 pub wire_model: Option<String>,
1429 #[serde(default, skip_serializing_if = "Option::is_none")]
1432 pub model_id: Option<String>,
1433 #[serde(default, skip_serializing_if = "Option::is_none")]
1435 pub name: Option<String>,
1436 #[serde(default, skip_serializing_if = "Option::is_none")]
1438 pub max_prompt_tokens: Option<i64>,
1439 #[serde(default, skip_serializing_if = "Option::is_none")]
1441 pub max_context_window_tokens: Option<i64>,
1442 #[serde(default, skip_serializing_if = "Option::is_none")]
1444 pub max_output_tokens: Option<i64>,
1445 #[serde(default, skip_serializing_if = "Option::is_none")]
1448 pub capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1449}
1450
1451impl ProviderModelConfig {
1452 pub fn new(id: impl Into<String>, provider: impl Into<String>) -> Self {
1455 Self {
1456 id: id.into(),
1457 provider: provider.into(),
1458 ..Self::default()
1459 }
1460 }
1461
1462 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
1464 self.wire_model = Some(wire_model.into());
1465 self
1466 }
1467
1468 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
1471 self.model_id = Some(model_id.into());
1472 self
1473 }
1474
1475 pub fn with_name(mut self, name: impl Into<String>) -> Self {
1477 self.name = Some(name.into());
1478 self
1479 }
1480
1481 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
1483 self.max_prompt_tokens = Some(max);
1484 self
1485 }
1486
1487 pub fn with_max_context_window_tokens(mut self, max: i64) -> Self {
1489 self.max_context_window_tokens = Some(max);
1490 self
1491 }
1492
1493 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
1495 self.max_output_tokens = Some(max);
1496 self
1497 }
1498
1499 pub fn with_capabilities(
1501 mut self,
1502 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1503 ) -> Self {
1504 self.capabilities = Some(capabilities);
1505 self
1506 }
1507}
1508
1509#[derive(Clone)]
1561#[non_exhaustive]
1562pub struct SessionConfig {
1563 pub session_id: Option<SessionId>,
1565 pub model: Option<String>,
1567 pub client_name: Option<String>,
1569 pub reasoning_effort: Option<String>,
1571 pub reasoning_summary: Option<ReasoningSummary>,
1575 pub context_tier: Option<String>,
1578 pub streaming: Option<bool>,
1580 pub system_message: Option<SystemMessageConfig>,
1582 pub tools: Option<Vec<Tool>>,
1584 pub canvases: Option<Vec<CanvasDeclaration>>,
1586 pub canvas_handler: Option<Arc<dyn CanvasHandler>>,
1591 pub request_canvas_renderer: Option<bool>,
1593 pub request_extensions: Option<bool>,
1595 pub extension_sdk_path: Option<String>,
1599 pub extension_info: Option<ExtensionInfo>,
1601 pub available_tools: Option<Vec<String>>,
1603 pub excluded_tools: Option<Vec<String>>,
1605 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
1607 pub mcp_oauth_token_storage: Option<String>,
1616 pub enable_config_discovery: Option<bool>,
1618 pub skip_embedding_retrieval: Option<bool>,
1620 pub embedding_cache_storage: Option<String>,
1623 pub organization_custom_instructions: Option<String>,
1625 pub enable_on_demand_instruction_discovery: Option<bool>,
1627 pub enable_file_hooks: Option<bool>,
1629 pub enable_host_git_operations: Option<bool>,
1631 pub enable_session_store: Option<bool>,
1633 pub enable_skills: Option<bool>,
1635 pub enable_mcp_apps: Option<bool>,
1662 pub skill_directories: Option<Vec<PathBuf>>,
1664 pub instruction_directories: Option<Vec<PathBuf>>,
1667 pub plugin_directories: Option<Vec<PathBuf>>,
1669 pub large_output: Option<LargeToolOutputConfig>,
1671 pub disabled_skills: Option<Vec<String>>,
1674 pub hooks: Option<bool>,
1678 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1680 pub default_agent: Option<DefaultAgentConfig>,
1684 pub agent: Option<String>,
1687 pub infinite_sessions: Option<InfiniteSessionConfig>,
1690 pub provider: Option<ProviderConfig>,
1694 pub capi: Option<CapiSessionOptions>,
1700 pub providers: Option<Vec<NamedProviderConfig>>,
1707 pub models: Option<Vec<ProviderModelConfig>>,
1713 pub enable_session_telemetry: Option<bool>,
1721 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1724 pub memory: Option<MemoryConfiguration>,
1726 pub config_directory: Option<PathBuf>,
1729 pub working_directory: Option<PathBuf>,
1732 pub github_token: Option<String>,
1738 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
1744 pub cloud: Option<CloudSessionOptions>,
1747 pub include_sub_agent_streaming_events: Option<bool>,
1751 pub commands: Option<Vec<CommandDefinition>>,
1755 #[doc(hidden)]
1762 pub exp_assignments: Option<Value>,
1763 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1768 pub permission_handler: Option<Arc<dyn PermissionHandler>>,
1772 pub elicitation_handler: Option<Arc<dyn ElicitationHandler>>,
1775 pub 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)]
3965#[serde(rename_all = "camelCase")]
3966pub struct GitHubRepoPointer {
3967 #[serde(skip_serializing_if = "Option::is_none")]
3969 pub id: Option<i64>,
3970 pub name: String,
3972 pub owner: String,
3974}
3975
3976#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3978#[serde(rename_all = "camelCase")]
3979pub struct GitHubFileDiffSide {
3980 pub path: String,
3982 pub r#ref: String,
3984 pub repo: GitHubRepoPointer,
3986}
3987
3988#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3990#[serde(rename_all = "camelCase")]
3991pub struct GitHubTreeComparisonSide {
3992 pub repo: GitHubRepoPointer,
3994 pub revision: String,
3996}
3997
3998#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4000#[serde(rename_all = "camelCase")]
4001pub struct GitHubSnippetLineRange {
4002 pub start: i64,
4004 pub end: i64,
4006}
4007
4008#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4010#[serde(
4011 tag = "type",
4012 rename_all = "camelCase",
4013 rename_all_fields = "camelCase"
4014)]
4015#[non_exhaustive]
4016pub enum Attachment {
4017 File {
4019 path: PathBuf,
4021 #[serde(skip_serializing_if = "Option::is_none")]
4023 display_name: Option<String>,
4024 #[serde(skip_serializing_if = "Option::is_none")]
4026 line_range: Option<AttachmentLineRange>,
4027 },
4028 Directory {
4030 path: PathBuf,
4032 #[serde(skip_serializing_if = "Option::is_none")]
4034 display_name: Option<String>,
4035 },
4036 Selection {
4038 file_path: PathBuf,
4040 text: String,
4042 #[serde(skip_serializing_if = "Option::is_none")]
4044 display_name: Option<String>,
4045 selection: AttachmentSelectionRange,
4047 },
4048 Blob {
4050 data: String,
4052 mime_type: String,
4054 #[serde(skip_serializing_if = "Option::is_none")]
4056 display_name: Option<String>,
4057 },
4058 #[serde(rename = "github_reference")]
4060 GitHubReference {
4061 number: u64,
4063 title: String,
4065 reference_type: GitHubReferenceType,
4067 state: String,
4069 url: String,
4071 },
4072 #[serde(rename = "github_commit")]
4074 GitHubCommit {
4075 message: String,
4077 oid: String,
4079 repo: GitHubRepoPointer,
4081 url: String,
4083 },
4084 #[serde(rename = "github_release")]
4086 GitHubRelease {
4087 name: String,
4089 repo: GitHubRepoPointer,
4091 tag_name: String,
4093 url: String,
4095 },
4096 #[serde(rename = "github_actions_job")]
4098 GitHubActionsJob {
4099 #[serde(skip_serializing_if = "Option::is_none")]
4102 conclusion: Option<String>,
4103 job_id: i64,
4105 job_name: String,
4107 repo: GitHubRepoPointer,
4109 url: String,
4111 workflow_name: String,
4113 },
4114 #[serde(rename = "github_repository")]
4116 GitHubRepository {
4117 #[serde(skip_serializing_if = "Option::is_none")]
4119 description: Option<String>,
4120 #[serde(skip_serializing_if = "Option::is_none")]
4123 r#ref: Option<String>,
4124 repo: GitHubRepoPointer,
4126 url: String,
4128 },
4129 #[serde(rename = "github_file_diff")]
4131 GitHubFileDiff {
4132 #[serde(skip_serializing_if = "Option::is_none")]
4134 base: Option<GitHubFileDiffSide>,
4135 #[serde(skip_serializing_if = "Option::is_none")]
4137 head: Option<GitHubFileDiffSide>,
4138 url: String,
4140 },
4141 #[serde(rename = "github_tree_comparison")]
4143 GitHubTreeComparison {
4144 base: GitHubTreeComparisonSide,
4146 head: GitHubTreeComparisonSide,
4148 url: String,
4150 },
4151 #[serde(rename = "github_url")]
4153 GitHubUrl {
4154 url: String,
4156 },
4157 #[serde(rename = "github_file")]
4159 GitHubFile {
4160 path: String,
4162 r#ref: String,
4164 repo: GitHubRepoPointer,
4166 url: String,
4168 },
4169 #[serde(rename = "github_snippet")]
4171 GitHubSnippet {
4172 line_range: GitHubSnippetLineRange,
4174 path: String,
4176 r#ref: String,
4178 repo: GitHubRepoPointer,
4180 url: String,
4182 },
4183}
4184
4185impl Attachment {
4186 pub fn display_name(&self) -> Option<&str> {
4188 match self {
4189 Self::File { display_name, .. }
4190 | Self::Directory { display_name, .. }
4191 | Self::Selection { display_name, .. }
4192 | Self::Blob { display_name, .. } => display_name.as_deref(),
4193 Self::GitHubReference { .. }
4194 | Self::GitHubCommit { .. }
4195 | Self::GitHubRelease { .. }
4196 | Self::GitHubActionsJob { .. }
4197 | Self::GitHubRepository { .. }
4198 | Self::GitHubFileDiff { .. }
4199 | Self::GitHubTreeComparison { .. }
4200 | Self::GitHubUrl { .. }
4201 | Self::GitHubFile { .. }
4202 | Self::GitHubSnippet { .. } => None,
4203 }
4204 }
4205
4206 pub fn label(&self) -> Option<String> {
4208 if let Some(display_name) = self
4209 .display_name()
4210 .map(str::trim)
4211 .filter(|name| !name.is_empty())
4212 {
4213 return Some(display_name.to_string());
4214 }
4215
4216 match self {
4217 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
4218 format!("#{}", number)
4219 } else {
4220 title.trim().to_string()
4221 }),
4222 _ => self.derived_display_name(),
4223 }
4224 }
4225
4226 pub fn ensure_display_name(&mut self) {
4228 if self
4229 .display_name()
4230 .map(str::trim)
4231 .is_some_and(|name| !name.is_empty())
4232 {
4233 return;
4234 }
4235
4236 let Some(derived_display_name) = self.derived_display_name() else {
4237 return;
4238 };
4239
4240 match self {
4241 Self::File { display_name, .. }
4242 | Self::Directory { display_name, .. }
4243 | Self::Selection { display_name, .. }
4244 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
4245 Self::GitHubReference { .. }
4246 | Self::GitHubCommit { .. }
4247 | Self::GitHubRelease { .. }
4248 | Self::GitHubActionsJob { .. }
4249 | Self::GitHubRepository { .. }
4250 | Self::GitHubFileDiff { .. }
4251 | Self::GitHubTreeComparison { .. }
4252 | Self::GitHubUrl { .. }
4253 | Self::GitHubFile { .. }
4254 | Self::GitHubSnippet { .. } => {}
4255 }
4256 }
4257
4258 fn derived_display_name(&self) -> Option<String> {
4259 match self {
4260 Self::File { path, .. } | Self::Directory { path, .. } => {
4261 Some(attachment_name_from_path(path))
4262 }
4263 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
4264 Self::Blob { .. } => Some("attachment".to_string()),
4265 Self::GitHubReference { .. }
4266 | Self::GitHubCommit { .. }
4267 | Self::GitHubRelease { .. }
4268 | Self::GitHubActionsJob { .. }
4269 | Self::GitHubRepository { .. }
4270 | Self::GitHubFileDiff { .. }
4271 | Self::GitHubTreeComparison { .. }
4272 | Self::GitHubUrl { .. }
4273 | Self::GitHubFile { .. }
4274 | Self::GitHubSnippet { .. } => None,
4275 }
4276 }
4277}
4278
4279fn attachment_name_from_path(path: &Path) -> String {
4280 path.file_name()
4281 .map(|name| name.to_string_lossy().into_owned())
4282 .filter(|name| !name.is_empty())
4283 .unwrap_or_else(|| {
4284 let full = path.to_string_lossy();
4285 if full.is_empty() {
4286 "attachment".to_string()
4287 } else {
4288 full.into_owned()
4289 }
4290 })
4291}
4292
4293pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
4295 for attachment in attachments {
4296 attachment.ensure_display_name();
4297 }
4298}
4299
4300#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4305#[serde(rename_all = "lowercase")]
4306#[non_exhaustive]
4307pub enum DeliveryMode {
4308 Enqueue,
4310 Immediate,
4312}
4313
4314#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4319#[serde(rename_all = "lowercase")]
4320#[non_exhaustive]
4321pub enum AgentMode {
4322 Interactive,
4324 Plan,
4326 Autopilot,
4328 Shell,
4330}
4331
4332#[derive(Debug, Clone)]
4361#[non_exhaustive]
4362pub struct MessageOptions {
4363 pub prompt: String,
4365 pub mode: Option<DeliveryMode>,
4371 pub agent_mode: Option<AgentMode>,
4375 pub attachments: Option<Vec<Attachment>>,
4377 pub wait_timeout: Option<Duration>,
4380 pub request_headers: Option<HashMap<String, String>>,
4384 pub traceparent: Option<String>,
4391 pub tracestate: Option<String>,
4395 pub display_prompt: Option<String>,
4397}
4398
4399impl MessageOptions {
4400 pub fn new(prompt: impl Into<String>) -> Self {
4402 Self {
4403 prompt: prompt.into(),
4404 mode: None,
4405 agent_mode: None,
4406 attachments: None,
4407 wait_timeout: None,
4408 request_headers: None,
4409 traceparent: None,
4410 tracestate: None,
4411 display_prompt: None,
4412 }
4413 }
4414
4415 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
4421 self.mode = Some(mode);
4422 self
4423 }
4424
4425 pub fn with_agent_mode(mut self, agent_mode: AgentMode) -> Self {
4429 self.agent_mode = Some(agent_mode);
4430 self
4431 }
4432
4433 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
4435 self.attachments = Some(attachments);
4436 self
4437 }
4438
4439 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
4441 self.wait_timeout = Some(timeout);
4442 self
4443 }
4444
4445 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
4447 self.request_headers = Some(headers);
4448 self
4449 }
4450
4451 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
4456 self.traceparent = ctx.traceparent;
4457 self.tracestate = ctx.tracestate;
4458 self
4459 }
4460
4461 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
4463 self.traceparent = Some(traceparent.into());
4464 self
4465 }
4466
4467 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
4469 self.tracestate = Some(tracestate.into());
4470 self
4471 }
4472
4473 pub fn with_display_prompt(mut self, display_prompt: impl Into<String>) -> Self {
4475 self.display_prompt = Some(display_prompt.into());
4476 self
4477 }
4478}
4479
4480impl From<&str> for MessageOptions {
4481 fn from(prompt: &str) -> Self {
4482 Self::new(prompt)
4483 }
4484}
4485
4486impl From<String> for MessageOptions {
4487 fn from(prompt: String) -> Self {
4488 Self::new(prompt)
4489 }
4490}
4491
4492impl From<&String> for MessageOptions {
4493 fn from(prompt: &String) -> Self {
4494 Self::new(prompt.clone())
4495 }
4496}
4497
4498#[derive(Debug, Clone, Serialize, Deserialize)]
4500#[serde(rename_all = "camelCase")]
4501#[non_exhaustive]
4502pub struct GetStatusResponse {
4503 pub version: String,
4505 pub protocol_version: u32,
4507}
4508
4509#[derive(Debug, Clone, Serialize, Deserialize)]
4511#[serde(rename_all = "camelCase")]
4512#[non_exhaustive]
4513pub struct GetAuthStatusResponse {
4514 pub is_authenticated: bool,
4516 #[serde(skip_serializing_if = "Option::is_none")]
4519 pub auth_type: Option<String>,
4520 #[serde(skip_serializing_if = "Option::is_none")]
4522 pub host: Option<String>,
4523 #[serde(skip_serializing_if = "Option::is_none")]
4525 pub login: Option<String>,
4526 #[serde(skip_serializing_if = "Option::is_none")]
4528 pub status_message: Option<String>,
4529}
4530
4531#[derive(Debug, Clone, Serialize, Deserialize)]
4535#[serde(rename_all = "camelCase")]
4536pub struct SessionEventNotification {
4537 pub session_id: SessionId,
4539 pub event: SessionEvent,
4541}
4542
4543#[derive(Debug, Clone, Serialize, Deserialize)]
4550#[serde(rename_all = "camelCase")]
4551pub struct SessionEvent {
4552 pub id: String,
4554 pub timestamp: String,
4556 pub parent_id: Option<String>,
4558 #[serde(skip_serializing_if = "Option::is_none")]
4560 pub ephemeral: Option<bool>,
4561 #[serde(skip_serializing_if = "Option::is_none")]
4564 pub agent_id: Option<String>,
4565 #[serde(skip_serializing_if = "Option::is_none")]
4567 pub debug_cli_received_at_ms: Option<i64>,
4568 #[serde(skip_serializing_if = "Option::is_none")]
4570 pub debug_ws_forwarded_at_ms: Option<i64>,
4571 #[serde(rename = "type")]
4573 pub event_type: String,
4574 pub data: Value,
4576}
4577
4578impl SessionEvent {
4579 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
4584 use serde::de::IntoDeserializer;
4585 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
4586 self.event_type.as_str().into_deserializer();
4587 crate::generated::SessionEventType::deserialize(deserializer)
4588 .unwrap_or(crate::generated::SessionEventType::Unknown)
4589 }
4590
4591 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
4597 serde_json::from_value(self.data.clone()).ok()
4598 }
4599
4600 pub fn is_transient_error(&self) -> bool {
4604 self.event_type == "session.error"
4605 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
4606 }
4607}
4608
4609#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4614#[serde(rename_all = "camelCase")]
4615#[non_exhaustive]
4616pub struct ToolInvocation {
4617 pub session_id: SessionId,
4619 pub tool_call_id: String,
4621 pub tool_name: String,
4623 pub arguments: Value,
4625 #[serde(default, skip_serializing_if = "Option::is_none")]
4630 pub traceparent: Option<String>,
4631 #[serde(default, skip_serializing_if = "Option::is_none")]
4634 pub tracestate: Option<String>,
4635}
4636
4637impl ToolInvocation {
4638 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
4659 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
4660 }
4661
4662 pub fn trace_context(&self) -> TraceContext {
4665 TraceContext {
4666 traceparent: self.traceparent.clone(),
4667 tracestate: self.tracestate.clone(),
4668 }
4669 }
4670}
4671
4672#[derive(Debug, Clone, Serialize, Deserialize)]
4674#[serde(rename_all = "camelCase")]
4675pub struct ToolBinaryResult {
4676 pub data: String,
4678 pub mime_type: String,
4680 pub r#type: String,
4682 #[serde(default, skip_serializing_if = "Option::is_none")]
4684 pub description: Option<String>,
4685}
4686
4687#[derive(Debug, Clone, Serialize, Deserialize)]
4689#[serde(rename_all = "camelCase")]
4690pub struct ToolResultExpanded {
4691 pub text_result_for_llm: String,
4693 pub result_type: String,
4695 #[serde(default, skip_serializing_if = "Option::is_none")]
4697 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
4698 #[serde(skip_serializing_if = "Option::is_none")]
4700 pub session_log: Option<String>,
4701 #[serde(skip_serializing_if = "Option::is_none")]
4703 pub error: Option<String>,
4704 #[serde(default, skip_serializing_if = "Option::is_none")]
4706 pub tool_telemetry: Option<HashMap<String, Value>>,
4707}
4708
4709#[derive(Debug, Clone, Serialize, Deserialize)]
4711#[serde(untagged)]
4712#[non_exhaustive]
4713pub enum ToolResult {
4714 Text(String),
4716 Expanded(ToolResultExpanded),
4718}
4719
4720#[derive(Debug, Clone, Serialize, Deserialize)]
4722#[serde(rename_all = "camelCase")]
4723pub struct ToolResultResponse {
4724 pub result: ToolResult,
4726}
4727
4728#[derive(Debug, Clone, Serialize, Deserialize)]
4730#[serde(rename_all = "camelCase")]
4731pub struct SessionMetadata {
4732 pub session_id: SessionId,
4734 pub start_time: String,
4736 pub modified_time: String,
4738 #[serde(skip_serializing_if = "Option::is_none")]
4740 pub summary: Option<String>,
4741 pub is_remote: bool,
4743}
4744
4745#[derive(Debug, Clone, Serialize, Deserialize)]
4747#[serde(rename_all = "camelCase")]
4748pub struct ListSessionsResponse {
4749 pub sessions: Vec<SessionMetadata>,
4751}
4752
4753#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4757#[serde(rename_all = "camelCase")]
4758pub struct SessionListFilter {
4759 #[serde(default, skip_serializing_if = "Option::is_none", rename = "cwd")]
4761 pub working_directory: Option<String>,
4762 #[serde(default, skip_serializing_if = "Option::is_none")]
4764 pub git_root: Option<String>,
4765 #[serde(default, skip_serializing_if = "Option::is_none")]
4767 pub repository: Option<String>,
4768 #[serde(default, skip_serializing_if = "Option::is_none")]
4770 pub branch: Option<String>,
4771}
4772
4773#[derive(Debug, Clone, Serialize, Deserialize)]
4775#[serde(rename_all = "camelCase")]
4776pub struct GetSessionMetadataResponse {
4777 #[serde(skip_serializing_if = "Option::is_none")]
4779 pub session: Option<SessionMetadata>,
4780}
4781
4782#[derive(Debug, Clone, Serialize, Deserialize)]
4784#[serde(rename_all = "camelCase")]
4785pub struct GetLastSessionIdResponse {
4786 #[serde(skip_serializing_if = "Option::is_none")]
4788 pub session_id: Option<SessionId>,
4789}
4790
4791#[derive(Debug, Clone, Serialize, Deserialize)]
4793#[serde(rename_all = "camelCase")]
4794pub struct GetForegroundSessionResponse {
4795 #[serde(skip_serializing_if = "Option::is_none")]
4797 pub session_id: Option<SessionId>,
4798}
4799
4800#[derive(Debug, Clone, Serialize, Deserialize)]
4802#[serde(rename_all = "camelCase")]
4803pub struct GetMessagesResponse {
4804 pub events: Vec<SessionEvent>,
4806}
4807
4808#[derive(Debug, Clone, Serialize, Deserialize)]
4810#[serde(rename_all = "camelCase")]
4811pub struct ElicitationResult {
4812 pub action: String,
4814 #[serde(skip_serializing_if = "Option::is_none")]
4816 pub content: Option<Value>,
4817}
4818
4819#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4825#[serde(rename_all = "camelCase")]
4826#[non_exhaustive]
4827pub enum ElicitationMode {
4828 Form,
4830 Url,
4832 #[serde(other)]
4834 Unknown,
4835}
4836
4837#[derive(Debug, Clone, Serialize, Deserialize)]
4844#[serde(rename_all = "camelCase")]
4845pub struct ElicitationRequest {
4846 pub message: String,
4848 #[serde(skip_serializing_if = "Option::is_none")]
4850 pub requested_schema: Option<Value>,
4851 #[serde(skip_serializing_if = "Option::is_none")]
4853 pub mode: Option<ElicitationMode>,
4854 #[serde(skip_serializing_if = "Option::is_none")]
4856 pub elicitation_source: Option<String>,
4857 #[serde(skip_serializing_if = "Option::is_none")]
4859 pub url: Option<String>,
4860}
4861
4862#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4867#[serde(rename_all = "camelCase")]
4868pub struct SessionCapabilities {
4869 #[serde(skip_serializing_if = "Option::is_none")]
4871 pub ui: Option<UiCapabilities>,
4872}
4873
4874#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4876#[serde(rename_all = "camelCase")]
4877pub struct UiCapabilities {
4878 #[serde(skip_serializing_if = "Option::is_none")]
4880 pub elicitation: Option<bool>,
4881 #[serde(skip_serializing_if = "Option::is_none")]
4892 pub mcp_apps: Option<bool>,
4893 #[serde(skip_serializing_if = "Option::is_none")]
4895 pub canvases: Option<bool>,
4896}
4897
4898#[derive(Debug, Clone, Default)]
4900pub struct UiInputOptions<'a> {
4901 pub title: Option<&'a str>,
4903 pub description: Option<&'a str>,
4905 pub min_length: Option<u64>,
4907 pub max_length: Option<u64>,
4909 pub format: Option<InputFormat>,
4911 pub default: Option<&'a str>,
4913}
4914
4915#[derive(Debug, Clone, Copy)]
4917#[non_exhaustive]
4918pub enum InputFormat {
4919 Email,
4921 Uri,
4923 Date,
4925 DateTime,
4927}
4928
4929impl InputFormat {
4930 pub fn as_str(&self) -> &'static str {
4932 match self {
4933 Self::Email => "email",
4934 Self::Uri => "uri",
4935 Self::Date => "date",
4936 Self::DateTime => "date-time",
4937 }
4938 }
4939}
4940
4941pub use crate::generated::api_types::{
4946 Model, ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext,
4947 ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
4948 ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision,
4949 PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable,
4950};
4951
4952#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4958#[serde(rename_all = "kebab-case")]
4959#[non_exhaustive]
4960pub enum PermissionRequestKind {
4961 Shell,
4963 Write,
4965 Read,
4967 Url,
4969 Mcp,
4971 CustomTool,
4973 Memory,
4975 Hook,
4977 #[serde(other)]
4980 Unknown,
4981}
4982
4983#[derive(Debug, Clone, Default, Serialize, Deserialize)]
4989#[serde(rename_all = "camelCase")]
4990pub struct PermissionRequestData {
4991 #[serde(default, skip_serializing_if = "Option::is_none")]
4995 pub kind: Option<PermissionRequestKind>,
4996 #[serde(default, skip_serializing_if = "Option::is_none")]
4999 pub tool_call_id: Option<String>,
5000 #[serde(flatten)]
5003 pub extra: Value,
5004}
5005
5006#[derive(Debug, Clone, Serialize, Deserialize)]
5008#[serde(rename_all = "camelCase")]
5009pub struct ExitPlanModeData {
5010 #[serde(default)]
5012 pub summary: String,
5013 #[serde(default, skip_serializing_if = "Option::is_none")]
5015 pub plan_content: Option<String>,
5016 #[serde(default)]
5018 pub actions: Vec<String>,
5019 #[serde(default = "default_recommended_action")]
5021 pub recommended_action: String,
5022}
5023
5024fn default_recommended_action() -> String {
5025 "autopilot".to_string()
5026}
5027
5028impl Default for ExitPlanModeData {
5029 fn default() -> Self {
5030 Self {
5031 summary: String::new(),
5032 plan_content: None,
5033 actions: Vec::new(),
5034 recommended_action: default_recommended_action(),
5035 }
5036 }
5037}
5038
5039#[cfg(test)]
5040mod tests {
5041 use std::path::PathBuf;
5042
5043 use serde_json::json;
5044
5045 use super::{
5046 AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition,
5047 AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState,
5048 CustomAgentConfig, DeliveryMode, ExtensionInfo, GitHubReferenceType, InfiniteSessionConfig,
5049 LargeToolOutputConfig, MemoryConfiguration, NamedProviderConfig, ProviderConfig,
5050 ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent,
5051 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
5052 ToolResultResponse, ensure_attachment_display_names,
5053 };
5054 use crate::generated::session_events::TypedSessionEvent;
5055
5056 #[test]
5057 fn tool_builder_composes() {
5058 let tool = Tool::new("greet")
5059 .with_description("Say hello")
5060 .with_namespaced_name("hello/greet")
5061 .with_instructions("Pass the user's name")
5062 .with_parameters(json!({
5063 "type": "object",
5064 "properties": { "name": { "type": "string" } },
5065 "required": ["name"]
5066 }))
5067 .with_overrides_built_in_tool(true)
5068 .with_skip_permission(true);
5069 assert_eq!(tool.name, "greet");
5070 assert_eq!(tool.description, "Say hello");
5071 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
5072 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
5073 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
5074 assert!(tool.overrides_built_in_tool);
5075 assert!(tool.skip_permission);
5076 }
5077
5078 #[test]
5079 fn tool_defer_serialization() {
5080 let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
5081 assert_eq!(tool.defer, Some(super::DeferMode::Auto));
5082 let value = serde_json::to_value(&tool).unwrap();
5083 assert_eq!(value.get("defer").unwrap(), &json!("auto"));
5084
5085 let plain = Tool::new("plain");
5086 let value = serde_json::to_value(&plain).unwrap();
5087 assert!(value.get("defer").is_none());
5088 }
5089
5090 #[test]
5091 fn custom_agent_config_builder_with_model() {
5092 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
5093 .with_model("claude-haiku-4.5")
5094 .with_display_name("My Agent");
5095 assert_eq!(agent.name, "my-agent");
5096 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
5097 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
5098 }
5099
5100 #[test]
5101 fn custom_agent_config_serializes_model() {
5102 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
5103 let wire = serde_json::to_value(&agent).unwrap();
5104 assert_eq!(wire["model"], "claude-haiku-4.5");
5105 assert_eq!(wire["name"], "model-agent");
5106 }
5107
5108 #[test]
5109 fn custom_agent_config_omits_model_when_none() {
5110 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
5111 let wire = serde_json::to_value(&agent).unwrap();
5112 assert!(wire.get("model").is_none());
5113 }
5114
5115 #[test]
5116 #[should_panic(expected = "tool parameter schema must be a JSON object")]
5117 fn tool_with_parameters_panics_on_non_object_value() {
5118 let _ = Tool::new("noop").with_parameters(json!(null));
5119 }
5120
5121 #[test]
5122 fn tool_result_expanded_serializes_binary_results_for_llm() {
5123 let response = ToolResultResponse {
5124 result: ToolResult::Expanded(ToolResultExpanded {
5125 text_result_for_llm: "rendered chart".to_string(),
5126 result_type: "success".to_string(),
5127 binary_results_for_llm: Some(vec![ToolBinaryResult {
5128 data: "aW1n".to_string(),
5129 mime_type: "image/png".to_string(),
5130 r#type: "image".to_string(),
5131 description: Some("chart preview".to_string()),
5132 }]),
5133 session_log: None,
5134 error: None,
5135 tool_telemetry: None,
5136 }),
5137 };
5138
5139 let wire = serde_json::to_value(&response).unwrap();
5140
5141 assert_eq!(
5142 wire,
5143 json!({
5144 "result": {
5145 "textResultForLlm": "rendered chart",
5146 "resultType": "success",
5147 "binaryResultsForLlm": [
5148 {
5149 "data": "aW1n",
5150 "mimeType": "image/png",
5151 "type": "image",
5152 "description": "chart preview"
5153 }
5154 ]
5155 }
5156 })
5157 );
5158 }
5159
5160 #[test]
5161 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
5162 let response = ToolResultResponse {
5163 result: ToolResult::Expanded(ToolResultExpanded {
5164 text_result_for_llm: "ok".to_string(),
5165 result_type: "success".to_string(),
5166 binary_results_for_llm: None,
5167 session_log: None,
5168 error: None,
5169 tool_telemetry: None,
5170 }),
5171 };
5172
5173 let wire = serde_json::to_value(&response).unwrap();
5174
5175 assert_eq!(wire["result"]["textResultForLlm"], "ok");
5176 assert!(wire["result"].get("binaryResultsForLlm").is_none());
5177 }
5178
5179 #[test]
5180 fn session_config_default_wire_flags_off_without_handlers() {
5181 let cfg = SessionConfig::default();
5182 assert_eq!(cfg.mcp_oauth_token_storage, None);
5183 let (wire, _runtime) = cfg
5187 .into_wire(Some(SessionId::from("default-flags")))
5188 .expect("default config has no duplicate handlers");
5189 assert!(!wire.request_user_input);
5190 assert!(!wire.request_permission);
5191 assert!(!wire.request_elicitation);
5192 assert!(!wire.request_exit_plan_mode);
5193 assert!(!wire.request_auto_mode_switch);
5194 assert!(!wire.hooks);
5195 assert!(!wire.request_mcp_apps);
5196 }
5197
5198 #[test]
5199 fn resume_session_config_new_wire_flags_off_without_handlers() {
5200 let cfg = ResumeSessionConfig::new(SessionId::from("resume-flags"));
5201 assert_eq!(cfg.mcp_oauth_token_storage, None);
5202 let (wire, _runtime) = cfg
5203 .into_wire()
5204 .expect("default resume config has no duplicate handlers");
5205 assert!(!wire.request_user_input);
5206 assert!(!wire.request_permission);
5207 assert!(!wire.request_elicitation);
5208 assert!(!wire.request_exit_plan_mode);
5209 assert!(!wire.request_auto_mode_switch);
5210 assert!(!wire.hooks);
5211 assert!(!wire.request_mcp_apps);
5212 }
5213
5214 #[test]
5215 fn session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5216 let cfg = SessionConfig::default().with_enable_mcp_apps(true);
5217 assert_eq!(cfg.enable_mcp_apps, Some(true));
5218
5219 let (wire, _runtime) = cfg
5220 .into_wire(Some(SessionId::from("enable-mcp-apps")))
5221 .expect("enable_mcp_apps config has no duplicate handlers");
5222 assert!(wire.request_mcp_apps);
5223
5224 let json = serde_json::to_value(&wire).unwrap();
5225 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5226 }
5227
5228 #[test]
5229 fn resume_session_config_enable_mcp_apps_sets_wire_flag_and_serializes() {
5230 let cfg = ResumeSessionConfig::new(SessionId::from("resume-enable-mcp-apps"))
5231 .with_enable_mcp_apps(true);
5232 assert_eq!(cfg.enable_mcp_apps, Some(true));
5233
5234 let (wire, _runtime) = cfg
5235 .into_wire()
5236 .expect("resume enable_mcp_apps config has no duplicate handlers");
5237 assert!(wire.request_mcp_apps);
5238
5239 let json = serde_json::to_value(&wire).unwrap();
5240 assert_eq!(json["requestMcpApps"], serde_json::Value::Bool(true));
5241 }
5242
5243 #[test]
5244 fn memory_configuration_constructors_and_serde() {
5245 assert!(MemoryConfiguration::enabled().enabled);
5246 assert!(!MemoryConfiguration::disabled().enabled);
5247 assert!(MemoryConfiguration::disabled().with_enabled(true).enabled);
5248
5249 let json = serde_json::to_value(MemoryConfiguration::enabled()).unwrap();
5250 assert_eq!(json, serde_json::json!({ "enabled": true }));
5251 }
5252
5253 #[test]
5254 fn session_config_with_memory_serializes() {
5255 let (wire, _runtime) = SessionConfig::default()
5256 .with_memory(MemoryConfiguration::enabled())
5257 .into_wire(Some(SessionId::from("memory-on")))
5258 .expect("no duplicate handlers");
5259 let json = serde_json::to_value(&wire).unwrap();
5260 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5261
5262 let (wire_off, _) = SessionConfig::default()
5263 .with_memory(MemoryConfiguration::disabled())
5264 .into_wire(Some(SessionId::from("memory-off")))
5265 .expect("no duplicate handlers");
5266 let json_off = serde_json::to_value(&wire_off).unwrap();
5267 assert_eq!(json_off["memory"], serde_json::json!({ "enabled": false }));
5268
5269 let (empty_wire, _) = SessionConfig::default()
5271 .into_wire(Some(SessionId::from("memory-unset")))
5272 .expect("no duplicate handlers");
5273 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5274 assert!(empty_json.get("memory").is_none());
5275 }
5276
5277 #[test]
5278 fn resume_session_config_with_memory_serializes() {
5279 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-memory-on"))
5280 .with_memory(MemoryConfiguration::enabled())
5281 .into_wire()
5282 .expect("no duplicate handlers");
5283 let json = serde_json::to_value(&wire).unwrap();
5284 assert_eq!(json["memory"], serde_json::json!({ "enabled": true }));
5285
5286 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-memory-unset"))
5288 .into_wire()
5289 .expect("no duplicate handlers");
5290 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5291 assert!(empty_json.get("memory").is_none());
5292 }
5293
5294 #[test]
5295 fn session_config_with_exp_assignments_serializes() {
5296 let assignments = serde_json::json!({
5297 "Parameters": { "copilot_exp_flag": "treatment" },
5298 "AssignmentContext": "ctx-123",
5299 });
5300 let (wire, _runtime) = SessionConfig::default()
5301 .with_exp_assignments(assignments.clone())
5302 .into_wire(Some(SessionId::from("exp-on")))
5303 .expect("no duplicate handlers");
5304 let json = serde_json::to_value(&wire).unwrap();
5305 assert_eq!(json["expAssignments"], assignments);
5306
5307 let (empty_wire, _) = SessionConfig::default()
5309 .into_wire(Some(SessionId::from("exp-unset")))
5310 .expect("no duplicate handlers");
5311 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5312 assert!(empty_json.get("expAssignments").is_none());
5313 }
5314
5315 #[test]
5316 fn resume_session_config_with_exp_assignments_serializes() {
5317 let assignments = serde_json::json!({
5318 "Parameters": { "copilot_exp_flag": "treatment" },
5319 "AssignmentContext": "ctx-456",
5320 });
5321 let (wire, _runtime) = ResumeSessionConfig::new(SessionId::from("resume-exp-on"))
5322 .with_exp_assignments(assignments.clone())
5323 .into_wire()
5324 .expect("no duplicate handlers");
5325 let json = serde_json::to_value(&wire).unwrap();
5326 assert_eq!(json["expAssignments"], assignments);
5327
5328 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("resume-exp-unset"))
5330 .into_wire()
5331 .expect("no duplicate handlers");
5332 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5333 assert!(empty_json.get("expAssignments").is_none());
5334 }
5335
5336 #[test]
5337 fn session_config_clone_preserves_exp_assignments() {
5338 let assignments = serde_json::json!({
5339 "Parameters": { "copilot_exp_flag": "treatment" },
5340 "AssignmentContext": "ctx-clone",
5341 });
5342 let config = SessionConfig::default().with_exp_assignments(assignments.clone());
5343 let cloned = config.clone();
5344
5345 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5346
5347 let (wire, _runtime) = cloned
5348 .into_wire(Some(SessionId::from("exp-clone")))
5349 .expect("no duplicate handlers");
5350 let json = serde_json::to_value(&wire).unwrap();
5351 assert_eq!(json["expAssignments"], assignments);
5352 }
5353
5354 #[test]
5355 fn resume_session_config_clone_preserves_exp_assignments() {
5356 let assignments = serde_json::json!({
5357 "Parameters": { "copilot_exp_flag": "treatment" },
5358 "AssignmentContext": "ctx-clone-resume",
5359 });
5360 let config = ResumeSessionConfig::new(SessionId::from("resume-exp-clone"))
5361 .with_exp_assignments(assignments.clone());
5362 let cloned = config.clone();
5363
5364 assert_eq!(cloned.exp_assignments.as_ref(), Some(&assignments));
5365
5366 let (wire, _runtime) = cloned.into_wire().expect("no duplicate handlers");
5367 let json = serde_json::to_value(&wire).unwrap();
5368 assert_eq!(json["expAssignments"], assignments);
5369 }
5370
5371 #[test]
5372 #[allow(clippy::field_reassign_with_default)]
5373 fn session_config_into_wire_serializes_bucket_b_fields() {
5374 use std::path::PathBuf;
5375
5376 use super::{CloudSessionOptions, CloudSessionRepository};
5377
5378 let mut cfg = SessionConfig::default();
5379 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5380 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5381 cfg.github_token = Some("ghs_secret".to_string());
5382 cfg.include_sub_agent_streaming_events = Some(false);
5383 cfg.enable_session_telemetry = Some(false);
5384 cfg.reasoning_summary = Some(ReasoningSummary::Concise);
5385 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::Export);
5386 cfg.enable_on_demand_instruction_discovery = Some(false);
5387 cfg.cloud = Some(CloudSessionOptions::with_repository(
5388 CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
5389 ));
5390
5391 let (wire, _runtime) = cfg
5392 .into_wire(Some(SessionId::from("custom-id")))
5393 .expect("no duplicate handlers");
5394 let wire_json = serde_json::to_value(&wire).unwrap();
5395 assert_eq!(wire_json["sessionId"], "custom-id");
5396 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5397 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5398 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5399 assert_eq!(wire_json["includeSubAgentStreamingEvents"], false);
5400 assert_eq!(wire_json["enableSessionTelemetry"], false);
5401 assert_eq!(wire_json["reasoningSummary"], "concise");
5402 assert_eq!(wire_json["remoteSession"], "export");
5403 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5404 assert_eq!(wire_json["cloud"]["repository"]["owner"], "github");
5405 assert_eq!(wire_json["cloud"]["repository"]["name"], "copilot-sdk");
5406 assert_eq!(wire_json["cloud"]["repository"]["branch"], "main");
5407
5408 let (empty_wire, _) = SessionConfig::default()
5410 .into_wire(Some(SessionId::from("empty")))
5411 .expect("default has no duplicate handlers");
5412 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5413 assert!(empty_json.get("gitHubToken").is_none());
5414 assert!(empty_json.get("enableSessionTelemetry").is_none());
5415 assert!(empty_json.get("reasoningSummary").is_none());
5416 assert!(empty_json.get("remoteSession").is_none());
5417 assert!(
5418 empty_json
5419 .get("enableOnDemandInstructionDiscovery")
5420 .is_none()
5421 );
5422 assert!(empty_json.get("cloud").is_none());
5423 }
5424
5425 #[test]
5426 fn session_config_into_wire_serializes_named_providers_and_models() {
5427 let cfg = SessionConfig::default()
5428 .with_providers(vec![
5429 NamedProviderConfig::new("my-openai", "https://api.example.com/v1")
5430 .with_provider_type("openai")
5431 .with_wire_api("responses")
5432 .with_api_key("sk-test"),
5433 ])
5434 .with_models(vec![
5435 ProviderModelConfig::new("gpt-x", "my-openai")
5436 .with_wire_model("gpt-x-2025")
5437 .with_max_output_tokens(2048),
5438 ]);
5439
5440 let (wire, _) = cfg
5441 .into_wire(Some(SessionId::from("sess-providers")))
5442 .expect("no duplicate handlers");
5443 let wire_json = serde_json::to_value(&wire).unwrap();
5444 assert_eq!(wire_json["providers"][0]["name"], "my-openai");
5445 assert_eq!(
5446 wire_json["providers"][0]["baseUrl"],
5447 "https://api.example.com/v1"
5448 );
5449 assert_eq!(wire_json["providers"][0]["type"], "openai");
5450 assert_eq!(wire_json["providers"][0]["wireApi"], "responses");
5451 assert_eq!(wire_json["providers"][0]["apiKey"], "sk-test");
5452 assert_eq!(wire_json["models"][0]["id"], "gpt-x");
5453 assert_eq!(wire_json["models"][0]["provider"], "my-openai");
5454 assert_eq!(wire_json["models"][0]["wireModel"], "gpt-x-2025");
5455 assert_eq!(wire_json["models"][0]["maxOutputTokens"], 2048);
5456
5457 let (empty_wire, _) = SessionConfig::default()
5458 .into_wire(Some(SessionId::from("empty")))
5459 .expect("default has no duplicate handlers");
5460 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5461 assert!(empty_json.get("providers").is_none());
5462 assert!(empty_json.get("models").is_none());
5463 }
5464
5465 #[test]
5466 fn resume_config_into_wire_serializes_named_providers_and_models() {
5467 let cfg = ResumeSessionConfig::new(SessionId::from("sess-resume"))
5468 .with_providers(vec![
5469 NamedProviderConfig::new("my-azure", "https://example.openai.azure.com")
5470 .with_provider_type("azure")
5471 .with_azure(AzureProviderOptions {
5472 api_version: Some("2024-10-21".to_string()),
5473 }),
5474 ])
5475 .with_models(vec![
5476 ProviderModelConfig::new("deploy-1", "my-azure").with_model_id("gpt-4o"),
5477 ]);
5478
5479 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5480 let wire_json = serde_json::to_value(&wire).unwrap();
5481 assert_eq!(wire_json["providers"][0]["name"], "my-azure");
5482 assert_eq!(wire_json["providers"][0]["type"], "azure");
5483 assert_eq!(
5484 wire_json["providers"][0]["azure"]["apiVersion"],
5485 "2024-10-21"
5486 );
5487 assert_eq!(wire_json["models"][0]["id"], "deploy-1");
5488 assert_eq!(wire_json["models"][0]["provider"], "my-azure");
5489 assert_eq!(wire_json["models"][0]["modelId"], "gpt-4o");
5490
5491 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("empty"))
5492 .into_wire()
5493 .expect("default has no duplicate handlers");
5494 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5495 assert!(empty_json.get("providers").is_none());
5496 assert!(empty_json.get("models").is_none());
5497 }
5498
5499 #[test]
5500 fn session_config_into_wire_serializes_plugin_directories_and_large_output() {
5501 use std::path::PathBuf;
5502
5503 let cfg = SessionConfig {
5504 plugin_directories: Some(vec![PathBuf::from("/tmp/plugins")]),
5505 large_output: Some(
5506 LargeToolOutputConfig::new()
5507 .with_enabled(true)
5508 .with_max_size_bytes(1024)
5509 .with_output_directory(PathBuf::from("/tmp/large-output")),
5510 ),
5511 ..Default::default()
5512 };
5513
5514 let (wire, _) = cfg
5515 .into_wire(Some(SessionId::from("sess-1")))
5516 .expect("no duplicate handlers");
5517 let wire_json = serde_json::to_value(&wire).unwrap();
5518 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins");
5519 assert_eq!(wire_json["largeOutput"]["enabled"], true);
5520 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 1024);
5521 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output");
5522
5523 let (empty_wire, _) = SessionConfig::default()
5524 .into_wire(Some(SessionId::from("empty")))
5525 .expect("default has no duplicate handlers");
5526 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5527 assert!(empty_json.get("pluginDirectories").is_none());
5528 assert!(empty_json.get("largeOutput").is_none());
5529 }
5530
5531 #[test]
5532 fn resume_session_config_into_wire_serializes_bucket_b_fields() {
5533 use std::path::PathBuf;
5534
5535 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5536 cfg.working_directory = Some(PathBuf::from("/tmp/work"));
5537 cfg.config_directory = Some(PathBuf::from("/tmp/cfg"));
5538 cfg.github_token = Some("ghs_secret".to_string());
5539 cfg.include_sub_agent_streaming_events = Some(true);
5540 cfg.enable_session_telemetry = Some(false);
5541 cfg.reasoning_summary = Some(ReasoningSummary::Detailed);
5542 cfg.remote_session = Some(crate::generated::api_types::RemoteSessionMode::On);
5543 cfg.enable_on_demand_instruction_discovery = Some(false);
5544
5545 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5546 let wire_json = serde_json::to_value(&wire).unwrap();
5547 assert_eq!(wire_json["sessionId"], "sess-1");
5548 assert_eq!(wire_json["workingDirectory"], "/tmp/work");
5549 assert_eq!(wire_json["configDir"], "/tmp/cfg");
5550 assert_eq!(wire_json["gitHubToken"], "ghs_secret");
5551 assert_eq!(wire_json["includeSubAgentStreamingEvents"], true);
5552 assert_eq!(wire_json["enableSessionTelemetry"], false);
5553 assert_eq!(wire_json["reasoningSummary"], "detailed");
5554 assert_eq!(wire_json["remoteSession"], "on");
5555 assert_eq!(wire_json["enableOnDemandInstructionDiscovery"], false);
5556
5557 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5559 .into_wire()
5560 .expect("default resume has no duplicate handlers");
5561 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5562 assert!(empty_json.get("reasoningSummary").is_none());
5563 assert!(empty_json.get("remoteSession").is_none());
5564 assert!(
5565 empty_json
5566 .get("enableOnDemandInstructionDiscovery")
5567 .is_none()
5568 );
5569 }
5570
5571 #[test]
5572 fn resume_session_config_into_wire_serializes_plugin_directories_and_large_output() {
5573 use std::path::PathBuf;
5574
5575 let mut cfg = ResumeSessionConfig::new(SessionId::from("sess-1"));
5576 cfg.plugin_directories = Some(vec![PathBuf::from("/tmp/plugins-r")]);
5577 cfg.large_output = Some(
5578 LargeToolOutputConfig::new()
5579 .with_enabled(false)
5580 .with_max_size_bytes(2048)
5581 .with_output_directory(PathBuf::from("/tmp/large-output-r")),
5582 );
5583
5584 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5585 let wire_json = serde_json::to_value(&wire).unwrap();
5586 assert_eq!(wire_json["pluginDirectories"][0], "/tmp/plugins-r");
5587 assert_eq!(wire_json["largeOutput"]["enabled"], false);
5588 assert_eq!(wire_json["largeOutput"]["maxSizeBytes"], 2048);
5589 assert_eq!(wire_json["largeOutput"]["outputDir"], "/tmp/large-output-r");
5590
5591 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5592 .into_wire()
5593 .expect("default resume has no duplicate handlers");
5594 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5595 assert!(empty_json.get("pluginDirectories").is_none());
5596 assert!(empty_json.get("largeOutput").is_none());
5597 }
5598
5599 #[test]
5600 fn session_config_builder_composes() {
5601 use std::collections::HashMap;
5602
5603 let cfg = SessionConfig::default()
5604 .with_session_id(SessionId::from("sess-1"))
5605 .with_model("claude-sonnet-4")
5606 .with_client_name("test-app")
5607 .with_reasoning_effort("medium")
5608 .with_reasoning_summary(ReasoningSummary::Concise)
5609 .with_context_tier("long_context")
5610 .with_streaming(true)
5611 .with_tools([Tool::new("greet")])
5612 .with_available_tools(["bash", "view"])
5613 .with_excluded_tools(["dangerous"])
5614 .with_mcp_servers(HashMap::new())
5615 .with_mcp_oauth_token_storage("persistent")
5616 .with_enable_config_discovery(true)
5617 .with_enable_on_demand_instruction_discovery(true)
5618 .with_skill_directories([PathBuf::from("/tmp/skills")])
5619 .with_disabled_skills(["broken-skill"])
5620 .with_agent("researcher")
5621 .with_config_directory(PathBuf::from("/tmp/config"))
5622 .with_working_directory(PathBuf::from("/tmp/work"))
5623 .with_github_token("ghp_test")
5624 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5625 .with_enable_session_telemetry(false)
5626 .with_include_sub_agent_streaming_events(false)
5627 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5628
5629 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
5630 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
5631 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5632 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
5633 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::Concise));
5634 assert_eq!(cfg.context_tier.as_deref(), Some("long_context"));
5635 assert_eq!(cfg.streaming, Some(true));
5636 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5637 assert_eq!(
5638 cfg.available_tools.as_deref(),
5639 Some(&["bash".to_string(), "view".to_string()][..])
5640 );
5641 assert_eq!(
5642 cfg.excluded_tools.as_deref(),
5643 Some(&["dangerous".to_string()][..])
5644 );
5645 assert!(cfg.mcp_servers.is_some());
5646 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5647 assert_eq!(cfg.enable_config_discovery, Some(true));
5648 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(true));
5649 assert_eq!(
5650 cfg.skill_directories.as_deref(),
5651 Some(&[PathBuf::from("/tmp/skills")][..])
5652 );
5653 assert_eq!(
5654 cfg.disabled_skills.as_deref(),
5655 Some(&["broken-skill".to_string()][..])
5656 );
5657 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5658 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5659 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5660 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5661 assert_eq!(
5662 cfg.capi,
5663 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5664 );
5665 assert_eq!(cfg.enable_session_telemetry, Some(false));
5666 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
5667 assert_eq!(
5668 cfg.extension_info,
5669 Some(ExtensionInfo::new("github-app", "counter"))
5670 );
5671 }
5672
5673 #[test]
5674 fn resume_session_config_builder_composes() {
5675 use std::collections::HashMap;
5676
5677 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
5678 .with_client_name("test-app")
5679 .with_reasoning_summary(ReasoningSummary::None)
5680 .with_context_tier("default")
5681 .with_streaming(true)
5682 .with_tools([Tool::new("greet")])
5683 .with_available_tools(["bash", "view"])
5684 .with_excluded_tools(["dangerous"])
5685 .with_mcp_servers(HashMap::new())
5686 .with_mcp_oauth_token_storage("persistent")
5687 .with_enable_config_discovery(true)
5688 .with_enable_on_demand_instruction_discovery(false)
5689 .with_skill_directories([PathBuf::from("/tmp/skills")])
5690 .with_disabled_skills(["broken-skill"])
5691 .with_agent("researcher")
5692 .with_config_directory(PathBuf::from("/tmp/config"))
5693 .with_working_directory(PathBuf::from("/tmp/work"))
5694 .with_github_token("ghp_test")
5695 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5696 .with_enable_session_telemetry(false)
5697 .with_include_sub_agent_streaming_events(true)
5698 .with_suppress_resume_event(true)
5699 .with_continue_pending_work(true)
5700 .with_extension_info(ExtensionInfo::new("github-app", "counter"));
5701
5702 assert_eq!(cfg.session_id.as_str(), "sess-2");
5703 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
5704 assert_eq!(cfg.reasoning_summary, Some(ReasoningSummary::None));
5705 assert_eq!(cfg.context_tier.as_deref(), Some("default"));
5706 assert_eq!(cfg.streaming, Some(true));
5707 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
5708 assert_eq!(
5709 cfg.available_tools.as_deref(),
5710 Some(&["bash".to_string(), "view".to_string()][..])
5711 );
5712 assert_eq!(
5713 cfg.excluded_tools.as_deref(),
5714 Some(&["dangerous".to_string()][..])
5715 );
5716 assert!(cfg.mcp_servers.is_some());
5717 assert_eq!(cfg.mcp_oauth_token_storage.as_deref(), Some("persistent"));
5718 assert_eq!(cfg.enable_config_discovery, Some(true));
5719 assert_eq!(cfg.enable_on_demand_instruction_discovery, Some(false));
5720 assert_eq!(
5721 cfg.skill_directories.as_deref(),
5722 Some(&[PathBuf::from("/tmp/skills")][..])
5723 );
5724 assert_eq!(
5725 cfg.disabled_skills.as_deref(),
5726 Some(&["broken-skill".to_string()][..])
5727 );
5728 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
5729 assert_eq!(cfg.config_directory, Some(PathBuf::from("/tmp/config")));
5730 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
5731 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
5732 assert_eq!(
5733 cfg.capi,
5734 Some(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5735 );
5736 assert_eq!(cfg.enable_session_telemetry, Some(false));
5737 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
5738 assert_eq!(cfg.suppress_resume_event, Some(true));
5739 assert_eq!(cfg.continue_pending_work, Some(true));
5740 assert_eq!(
5741 cfg.extension_info,
5742 Some(ExtensionInfo::new("github-app", "counter"))
5743 );
5744 }
5745
5746 #[test]
5750 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
5751 let cfg =
5752 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
5753 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5754 let json = serde_json::to_value(&wire).unwrap();
5755 assert_eq!(json["continuePendingWork"], true);
5756
5757 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5759 .into_wire()
5760 .expect("no duplicate handlers");
5761 let json = serde_json::to_value(&wire).unwrap();
5762 assert!(json.get("continuePendingWork").is_none());
5763 }
5764
5765 #[test]
5769 fn resume_session_config_serializes_suppress_resume_event_to_disable_resume_on_wire() {
5770 let cfg =
5771 ResumeSessionConfig::new(SessionId::from("sess-1")).with_suppress_resume_event(true);
5772 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5773 let json = serde_json::to_value(&wire).unwrap();
5774 assert_eq!(json["disableResume"], true);
5775 assert!(json.get("suppressResumeEvent").is_none());
5776 }
5777
5778 #[test]
5781 fn session_config_serializes_instruction_directories_to_camel_case() {
5782 let cfg =
5783 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
5784 let (wire, _) = cfg
5785 .into_wire(Some(SessionId::from("instr-on")))
5786 .expect("no duplicate handlers");
5787 let json = serde_json::to_value(&wire).unwrap();
5788 assert_eq!(
5789 json["instructionDirectories"],
5790 serde_json::json!(["/tmp/instr"])
5791 );
5792
5793 let (wire, _) = SessionConfig::default()
5795 .into_wire(Some(SessionId::from("instr-off")))
5796 .expect("no duplicate handlers");
5797 let json = serde_json::to_value(&wire).unwrap();
5798 assert!(json.get("instructionDirectories").is_none());
5799 }
5800
5801 #[test]
5804 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
5805 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
5806 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
5807 let (wire, _) = cfg.into_wire().expect("no duplicate handlers");
5808 let json = serde_json::to_value(&wire).unwrap();
5809 assert_eq!(
5810 json["instructionDirectories"],
5811 serde_json::json!(["/tmp/instr"])
5812 );
5813
5814 let (wire, _) = ResumeSessionConfig::new(SessionId::from("sess-2"))
5815 .into_wire()
5816 .expect("no duplicate handlers");
5817 let json = serde_json::to_value(&wire).unwrap();
5818 assert!(json.get("instructionDirectories").is_none());
5819 }
5820
5821 #[test]
5822 fn custom_agent_config_builder_composes() {
5823 use std::collections::HashMap;
5824
5825 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
5826 .with_display_name("Research Assistant")
5827 .with_description("Investigates technical questions.")
5828 .with_tools(["bash", "view"])
5829 .with_mcp_servers(HashMap::new())
5830 .with_infer(true)
5831 .with_skills(["rust-coding-skill"]);
5832
5833 assert_eq!(cfg.name, "researcher");
5834 assert_eq!(cfg.prompt, "You are a research assistant.");
5835 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
5836 assert_eq!(
5837 cfg.description.as_deref(),
5838 Some("Investigates technical questions.")
5839 );
5840 assert_eq!(
5841 cfg.tools.as_deref(),
5842 Some(&["bash".to_string(), "view".to_string()][..])
5843 );
5844 assert!(cfg.mcp_servers.is_some());
5845 assert_eq!(cfg.infer, Some(true));
5846 assert_eq!(
5847 cfg.skills.as_deref(),
5848 Some(&["rust-coding-skill".to_string()][..])
5849 );
5850 }
5851
5852 #[test]
5853 fn infinite_session_config_builder_composes() {
5854 let cfg = InfiniteSessionConfig::new()
5855 .with_enabled(true)
5856 .with_background_compaction_threshold(0.75)
5857 .with_buffer_exhaustion_threshold(0.92);
5858
5859 assert_eq!(cfg.enabled, Some(true));
5860 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
5861 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
5862 }
5863
5864 #[test]
5865 fn provider_config_builder_composes() {
5866 use std::collections::HashMap;
5867
5868 let mut headers = HashMap::new();
5869 headers.insert("X-Custom".to_string(), "value".to_string());
5870
5871 let cfg = ProviderConfig::new("https://api.example.com")
5872 .with_provider_type("openai")
5873 .with_wire_api("completions")
5874 .with_transport("websockets")
5875 .with_api_key("sk-test")
5876 .with_bearer_token("bearer-test")
5877 .with_headers(headers)
5878 .with_model_id("gpt-4")
5879 .with_wire_model("azure-gpt-4-deployment")
5880 .with_max_prompt_tokens(8192)
5881 .with_max_output_tokens(2048);
5882
5883 assert_eq!(cfg.base_url, "https://api.example.com");
5884 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
5885 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
5886 assert_eq!(cfg.transport.as_deref(), Some("websockets"));
5887 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
5888 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
5889 assert_eq!(
5890 cfg.headers
5891 .as_ref()
5892 .and_then(|h| h.get("X-Custom"))
5893 .map(String::as_str),
5894 Some("value"),
5895 );
5896 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
5897 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
5898 assert_eq!(cfg.max_prompt_tokens, Some(8192));
5899 assert_eq!(cfg.max_output_tokens, Some(2048));
5900
5901 let wire = serde_json::to_value(&cfg).unwrap();
5903 assert_eq!(wire["modelId"], "gpt-4");
5904 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
5905 assert_eq!(wire["maxPromptTokens"], 8192);
5906 assert_eq!(wire["maxOutputTokens"], 2048);
5907
5908 let unset = ProviderConfig::new("https://api.example.com");
5909 let wire_unset = serde_json::to_value(&unset).unwrap();
5910 assert!(wire_unset.get("modelId").is_none());
5911 assert!(wire_unset.get("wireModel").is_none());
5912 assert!(wire_unset.get("maxPromptTokens").is_none());
5913 assert!(wire_unset.get("maxOutputTokens").is_none());
5914 }
5915
5916 #[test]
5917 fn capi_session_options_builder_composes_and_serializes() {
5918 let cfg = CapiSessionOptions::new().with_enable_web_socket_responses(false);
5919
5920 assert_eq!(cfg.enable_web_socket_responses, Some(false));
5921
5922 let wire = serde_json::to_value(&cfg).unwrap();
5923 assert_eq!(
5924 wire,
5925 serde_json::json!({ "enableWebSocketResponses": false })
5926 );
5927
5928 let unset = CapiSessionOptions::new();
5929 let wire_unset = serde_json::to_value(&unset).unwrap();
5930 assert!(wire_unset.get("enableWebSocketResponses").is_none());
5931 }
5932
5933 #[test]
5934 fn session_config_with_capi_serializes() {
5935 let (wire, _) = SessionConfig::default()
5936 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5937 .into_wire(Some(SessionId::from("capi-create")))
5938 .expect("no duplicate handlers");
5939 let json = serde_json::to_value(&wire).unwrap();
5940 assert_eq!(
5941 json["capi"],
5942 serde_json::json!({ "enableWebSocketResponses": false })
5943 );
5944
5945 let (empty_wire, _) = SessionConfig::default()
5946 .into_wire(Some(SessionId::from("capi-create-unset")))
5947 .expect("no duplicate handlers");
5948 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5949 assert!(empty_json.get("capi").is_none());
5950 }
5951
5952 #[test]
5953 fn resume_session_config_with_capi_serializes() {
5954 let (wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume"))
5955 .with_capi(CapiSessionOptions::new().with_enable_web_socket_responses(false))
5956 .into_wire()
5957 .expect("no duplicate handlers");
5958 let json = serde_json::to_value(&wire).unwrap();
5959 assert_eq!(
5960 json["capi"],
5961 serde_json::json!({ "enableWebSocketResponses": false })
5962 );
5963
5964 let (empty_wire, _) = ResumeSessionConfig::new(SessionId::from("capi-resume-unset"))
5965 .into_wire()
5966 .expect("no duplicate handlers");
5967 let empty_json = serde_json::to_value(&empty_wire).unwrap();
5968 assert!(empty_json.get("capi").is_none());
5969 }
5970
5971 #[test]
5972 fn system_message_config_builder_composes() {
5973 use std::collections::HashMap;
5974
5975 let cfg = SystemMessageConfig::new()
5976 .with_mode("replace")
5977 .with_content("Custom system message.")
5978 .with_sections(HashMap::new());
5979
5980 assert_eq!(cfg.mode.as_deref(), Some("replace"));
5981 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
5982 assert!(cfg.sections.is_some());
5983 }
5984
5985 #[test]
5986 fn delivery_mode_serializes_to_kebab_case_strings() {
5987 assert_eq!(
5988 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
5989 "\"enqueue\""
5990 );
5991 assert_eq!(
5992 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
5993 "\"immediate\""
5994 );
5995 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
5996 assert_eq!(parsed, DeliveryMode::Immediate);
5997 }
5998
5999 #[test]
6000 fn agent_mode_serializes_to_kebab_case_strings() {
6001 assert_eq!(
6002 serde_json::to_string(&AgentMode::Interactive).unwrap(),
6003 "\"interactive\""
6004 );
6005 assert_eq!(serde_json::to_string(&AgentMode::Plan).unwrap(), "\"plan\"");
6006 assert_eq!(
6007 serde_json::to_string(&AgentMode::Autopilot).unwrap(),
6008 "\"autopilot\""
6009 );
6010 assert_eq!(
6011 serde_json::to_string(&AgentMode::Shell).unwrap(),
6012 "\"shell\""
6013 );
6014 let parsed: AgentMode = serde_json::from_str("\"plan\"").unwrap();
6015 assert_eq!(parsed, AgentMode::Plan);
6016 }
6017
6018 #[test]
6019 fn connection_state_distinguishes_variants() {
6020 assert_ne!(ConnectionState::Connected, ConnectionState::Disconnected);
6023 }
6024
6025 #[test]
6031 fn session_event_round_trips_agent_id_on_envelope() {
6032 let wire = json!({
6033 "id": "evt-1",
6034 "timestamp": "2026-04-30T12:00:00Z",
6035 "parentId": null,
6036 "agentId": "sub-agent-42",
6037 "type": "assistant.message",
6038 "data": { "message": "hi" }
6039 });
6040
6041 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
6042 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6043
6044 let roundtripped = serde_json::to_value(&event).unwrap();
6046 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6047
6048 let main_agent_event: SessionEvent = serde_json::from_value(json!({
6050 "id": "evt-2",
6051 "timestamp": "2026-04-30T12:00:01Z",
6052 "parentId": null,
6053 "type": "session.idle",
6054 "data": {}
6055 }))
6056 .unwrap();
6057 assert!(main_agent_event.agent_id.is_none());
6058 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
6059 assert!(roundtripped.get("agentId").is_none());
6060 }
6061
6062 #[test]
6064 fn typed_session_event_round_trips_agent_id_on_envelope() {
6065 let wire = json!({
6066 "id": "evt-1",
6067 "timestamp": "2026-04-30T12:00:00Z",
6068 "parentId": null,
6069 "agentId": "sub-agent-42",
6070 "type": "session.idle",
6071 "data": {}
6072 });
6073
6074 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
6075 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
6076
6077 let roundtripped = serde_json::to_value(&event).unwrap();
6078 assert_eq!(roundtripped["agentId"], "sub-agent-42");
6079 }
6080
6081 #[test]
6082 fn connection_state_variants_compile() {
6083 let _ = ConnectionState::Disconnected;
6087 let _ = ConnectionState::Connecting;
6088 let _ = ConnectionState::Connected;
6089 let _ = ConnectionState::Error;
6090 }
6091
6092 #[test]
6093 fn deserializes_runtime_attachment_variants() {
6094 let attachments: Vec<Attachment> = serde_json::from_value(json!([
6095 {
6096 "type": "file",
6097 "path": "/tmp/file.rs",
6098 "displayName": "file.rs",
6099 "lineRange": { "start": 7, "end": 12 }
6100 },
6101 {
6102 "type": "directory",
6103 "path": "/tmp/project",
6104 "displayName": "project"
6105 },
6106 {
6107 "type": "selection",
6108 "filePath": "/tmp/lib.rs",
6109 "displayName": "lib.rs",
6110 "text": "fn main() {}",
6111 "selection": {
6112 "start": { "line": 1, "character": 2 },
6113 "end": { "line": 3, "character": 4 }
6114 }
6115 },
6116 {
6117 "type": "blob",
6118 "data": "Zm9v",
6119 "mimeType": "image/png",
6120 "displayName": "image.png"
6121 },
6122 {
6123 "type": "github_reference",
6124 "number": 42,
6125 "title": "Fix rendering",
6126 "referenceType": "issue",
6127 "state": "open",
6128 "url": "https://github.com/example/repo/issues/42"
6129 }
6130 ]))
6131 .expect("attachments should deserialize");
6132
6133 assert_eq!(attachments.len(), 5);
6134 assert!(matches!(
6135 &attachments[0],
6136 Attachment::File {
6137 path,
6138 display_name,
6139 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
6140 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
6141 ));
6142 assert!(matches!(
6143 &attachments[1],
6144 Attachment::Directory { path, display_name }
6145 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
6146 ));
6147 assert!(matches!(
6148 &attachments[2],
6149 Attachment::Selection {
6150 file_path,
6151 display_name,
6152 selection:
6153 AttachmentSelectionRange {
6154 start: AttachmentSelectionPosition { line: 1, character: 2 },
6155 end: AttachmentSelectionPosition { line: 3, character: 4 },
6156 },
6157 ..
6158 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
6159 ));
6160 assert!(matches!(
6161 &attachments[3],
6162 Attachment::Blob {
6163 data,
6164 mime_type,
6165 display_name,
6166 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
6167 ));
6168 assert!(matches!(
6169 &attachments[4],
6170 Attachment::GitHubReference {
6171 number: 42,
6172 title,
6173 reference_type: GitHubReferenceType::Issue,
6174 state,
6175 url,
6176 } if title == "Fix rendering"
6177 && state == "open"
6178 && url == "https://github.com/example/repo/issues/42"
6179 ));
6180 }
6181
6182 #[test]
6183 fn ensures_display_names_for_variants_that_support_them() {
6184 let mut attachments = vec![
6185 Attachment::File {
6186 path: PathBuf::from("/tmp/file.rs"),
6187 display_name: None,
6188 line_range: None,
6189 },
6190 Attachment::Selection {
6191 file_path: PathBuf::from("/tmp/src/lib.rs"),
6192 display_name: None,
6193 text: "fn main() {}".to_string(),
6194 selection: AttachmentSelectionRange {
6195 start: AttachmentSelectionPosition {
6196 line: 0,
6197 character: 0,
6198 },
6199 end: AttachmentSelectionPosition {
6200 line: 0,
6201 character: 10,
6202 },
6203 },
6204 },
6205 Attachment::Blob {
6206 data: "Zm9v".to_string(),
6207 mime_type: "image/png".to_string(),
6208 display_name: None,
6209 },
6210 Attachment::GitHubReference {
6211 number: 7,
6212 title: "Track regressions".to_string(),
6213 reference_type: GitHubReferenceType::Issue,
6214 state: "open".to_string(),
6215 url: "https://example.com/issues/7".to_string(),
6216 },
6217 ];
6218
6219 ensure_attachment_display_names(&mut attachments);
6220
6221 assert_eq!(attachments[0].display_name(), Some("file.rs"));
6222 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
6223 assert_eq!(attachments[2].display_name(), Some("attachment"));
6224 assert_eq!(attachments[3].display_name(), None);
6225 assert_eq!(
6226 attachments[3].label(),
6227 Some("Track regressions".to_string())
6228 );
6229 }
6230
6231 #[test]
6232 fn github_anchored_attachment_variants_round_trip() {
6233 let cases = vec![
6234 (
6235 "github_commit",
6236 json!({
6237 "type": "github_commit",
6238 "message": "Fix the thing",
6239 "oid": "abc123",
6240 "repo": { "id": 1, "name": "repo", "owner": "octocat" },
6241 "url": "https://github.com/octocat/repo/commit/abc123"
6242 }),
6243 ),
6244 (
6245 "github_release",
6246 json!({
6247 "type": "github_release",
6248 "name": "v1.2.3",
6249 "repo": { "name": "repo", "owner": "octocat" },
6250 "tagName": "v1.2.3",
6251 "url": "https://github.com/octocat/repo/releases/tag/v1.2.3"
6252 }),
6253 ),
6254 (
6255 "github_actions_job",
6256 json!({
6257 "type": "github_actions_job",
6258 "conclusion": "failure",
6259 "jobId": 99,
6260 "jobName": "build",
6261 "repo": { "name": "repo", "owner": "octocat" },
6262 "url": "https://github.com/octocat/repo/actions/runs/1/job/99",
6263 "workflowName": "CI"
6264 }),
6265 ),
6266 (
6267 "github_repository",
6268 json!({
6269 "type": "github_repository",
6270 "description": "An example repository",
6271 "ref": "main",
6272 "repo": { "name": "repo", "owner": "octocat" },
6273 "url": "https://github.com/octocat/repo"
6274 }),
6275 ),
6276 (
6277 "github_file_diff",
6278 json!({
6279 "type": "github_file_diff",
6280 "base": {
6281 "path": "src/lib.rs",
6282 "ref": "main",
6283 "repo": { "name": "repo", "owner": "octocat" }
6284 },
6285 "head": {
6286 "path": "src/lib.rs",
6287 "ref": "feature",
6288 "repo": { "name": "repo", "owner": "octocat" }
6289 },
6290 "url": "https://github.com/octocat/repo/compare/main...feature"
6291 }),
6292 ),
6293 (
6294 "github_tree_comparison",
6295 json!({
6296 "type": "github_tree_comparison",
6297 "base": {
6298 "repo": { "name": "repo", "owner": "octocat" },
6299 "revision": "main"
6300 },
6301 "head": {
6302 "repo": { "name": "repo", "owner": "octocat" },
6303 "revision": "feature"
6304 },
6305 "url": "https://github.com/octocat/repo/compare/main...feature"
6306 }),
6307 ),
6308 (
6309 "github_url",
6310 json!({
6311 "type": "github_url",
6312 "url": "https://github.com/octocat/repo/wiki"
6313 }),
6314 ),
6315 (
6316 "github_file",
6317 json!({
6318 "type": "github_file",
6319 "path": "src/main.rs",
6320 "ref": "main",
6321 "repo": { "name": "repo", "owner": "octocat" },
6322 "url": "https://github.com/octocat/repo/blob/main/src/main.rs"
6323 }),
6324 ),
6325 (
6326 "github_snippet",
6327 json!({
6328 "type": "github_snippet",
6329 "lineRange": { "start": 10, "end": 20 },
6330 "path": "src/main.rs",
6331 "ref": "main",
6332 "repo": { "name": "repo", "owner": "octocat" },
6333 "url": "https://github.com/octocat/repo/blob/main/src/main.rs#L10-L20"
6334 }),
6335 ),
6336 ];
6337
6338 for (expected_type, input) in cases {
6339 let attachment: Attachment = serde_json::from_value(input.clone())
6340 .unwrap_or_else(|err| panic!("{expected_type} should deserialize: {err}"));
6341
6342 let serialized_string = serde_json::to_string(&attachment)
6347 .unwrap_or_else(|err| panic!("{expected_type} should serialize: {err}"));
6348
6349 assert_eq!(
6351 serialized_string.matches("\"type\":").count(),
6352 1,
6353 "{expected_type} must serialize a single `type` key"
6354 );
6355
6356 let serialized: serde_json::Value = serde_json::from_str(&serialized_string)
6357 .unwrap_or_else(|err| panic!("{expected_type} should reparse: {err}"));
6358 assert_eq!(
6359 serialized.get("type").and_then(|value| value.as_str()),
6360 Some(expected_type),
6361 "{expected_type} must serialize the correct discriminator"
6362 );
6363
6364 assert_eq!(
6366 serialized, input,
6367 "{expected_type} should round-trip without data loss"
6368 );
6369 let reparsed: Attachment = serde_json::from_value(serialized)
6370 .unwrap_or_else(|err| panic!("{expected_type} should re-deserialize: {err}"));
6371 assert_eq!(
6372 reparsed, attachment,
6373 "{expected_type} should re-deserialize to the same value"
6374 );
6375 }
6376 }
6377}
6378
6379#[cfg(test)]
6380mod permission_builder_tests {
6381 use std::sync::Arc;
6382
6383 use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult};
6384 use crate::permission;
6385 use crate::types::{
6386 PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig,
6387 SessionId,
6388 };
6389
6390 fn data() -> PermissionRequestData {
6391 PermissionRequestData {
6392 extra: serde_json::json!({"tool": "shell"}),
6393 ..Default::default()
6394 }
6395 }
6396
6397 fn resolve_create(mut cfg: SessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6400 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6401 }
6402
6403 fn resolve_resume(mut cfg: ResumeSessionConfig) -> Option<Arc<dyn PermissionHandler>> {
6404 permission::resolve_handler(cfg.permission_handler.take(), cfg.permission_policy.take())
6405 }
6406
6407 async fn dispatch(handler: &Arc<dyn PermissionHandler>) -> PermissionResult {
6408 handler
6409 .handle(SessionId::from("s1"), RequestId::new("1"), data())
6410 .await
6411 }
6412
6413 #[tokio::test]
6414 async fn approve_all_with_handler_present_approves() {
6415 let cfg = SessionConfig::default()
6416 .with_permission_handler(Arc::new(ApproveAllHandler))
6417 .approve_all_permissions();
6418 let h = resolve_create(cfg).expect("policy + handler yields handler");
6419 assert!(matches!(
6420 dispatch(&h).await,
6421 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6422 ));
6423 }
6424
6425 #[tokio::test]
6426 async fn approve_all_standalone_produces_handler() {
6427 let cfg = SessionConfig::default().approve_all_permissions();
6428 let h = resolve_create(cfg).expect("policy alone yields handler");
6429 assert!(matches!(
6430 dispatch(&h).await,
6431 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6432 ));
6433 }
6434
6435 #[tokio::test]
6438 async fn approve_all_is_order_independent() {
6439 let a = SessionConfig::default()
6440 .with_permission_handler(Arc::new(ApproveAllHandler))
6441 .approve_all_permissions();
6442 let b = SessionConfig::default()
6443 .approve_all_permissions()
6444 .with_permission_handler(Arc::new(ApproveAllHandler));
6445 let ha = resolve_create(a).unwrap();
6446 let hb = resolve_create(b).unwrap();
6447 assert!(matches!(
6448 dispatch(&ha).await,
6449 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6450 ));
6451 assert!(matches!(
6452 dispatch(&hb).await,
6453 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6454 ));
6455 }
6456
6457 #[tokio::test]
6458 async fn deny_all_is_order_independent() {
6459 let a = SessionConfig::default()
6460 .with_permission_handler(Arc::new(ApproveAllHandler))
6461 .deny_all_permissions();
6462 let b = SessionConfig::default()
6463 .deny_all_permissions()
6464 .with_permission_handler(Arc::new(ApproveAllHandler));
6465 let ha = resolve_create(a).unwrap();
6466 let hb = resolve_create(b).unwrap();
6467 assert!(matches!(
6468 dispatch(&ha).await,
6469 PermissionResult::Decision(PermissionDecision::Reject(_))
6470 ));
6471 assert!(matches!(
6472 dispatch(&hb).await,
6473 PermissionResult::Decision(PermissionDecision::Reject(_))
6474 ));
6475 }
6476
6477 #[tokio::test]
6478 async fn approve_permissions_if_consults_predicate() {
6479 let cfg = SessionConfig::default().approve_permissions_if(|d| {
6480 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6481 });
6482 let h = resolve_create(cfg).unwrap();
6483 assert!(matches!(
6484 dispatch(&h).await,
6485 PermissionResult::Decision(PermissionDecision::Reject(_))
6486 ));
6487 }
6488
6489 #[tokio::test]
6490 async fn approve_permissions_if_is_order_independent() {
6491 let predicate = |d: &PermissionRequestData| {
6492 d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
6493 };
6494 let a = SessionConfig::default()
6495 .with_permission_handler(Arc::new(ApproveAllHandler))
6496 .approve_permissions_if(predicate);
6497 let b = SessionConfig::default()
6498 .approve_permissions_if(predicate)
6499 .with_permission_handler(Arc::new(ApproveAllHandler));
6500 let ha = resolve_create(a).unwrap();
6501 let hb = resolve_create(b).unwrap();
6502 assert!(matches!(
6503 dispatch(&ha).await,
6504 PermissionResult::Decision(PermissionDecision::Reject(_))
6505 ));
6506 assert!(matches!(
6507 dispatch(&hb).await,
6508 PermissionResult::Decision(PermissionDecision::Reject(_))
6509 ));
6510 }
6511
6512 #[tokio::test]
6513 async fn resume_session_config_approve_all_works() {
6514 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
6515 .with_permission_handler(Arc::new(ApproveAllHandler))
6516 .approve_all_permissions();
6517 let h = resolve_resume(cfg).unwrap();
6518 assert!(matches!(
6519 dispatch(&h).await,
6520 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6521 ));
6522 }
6523
6524 #[tokio::test]
6525 async fn resume_session_config_approve_all_is_order_independent() {
6526 let a = ResumeSessionConfig::new(SessionId::from("s1"))
6527 .with_permission_handler(Arc::new(ApproveAllHandler))
6528 .approve_all_permissions();
6529 let b = ResumeSessionConfig::new(SessionId::from("s1"))
6530 .approve_all_permissions()
6531 .with_permission_handler(Arc::new(ApproveAllHandler));
6532 let ha = resolve_resume(a).unwrap();
6533 let hb = resolve_resume(b).unwrap();
6534 assert!(matches!(
6535 dispatch(&ha).await,
6536 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6537 ));
6538 assert!(matches!(
6539 dispatch(&hb).await,
6540 PermissionResult::Decision(PermissionDecision::ApproveOnce(_))
6541 ));
6542 }
6543}