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::handler::SessionHandler;
16use crate::hooks::SessionHooks;
17pub use crate::session_fs::{
18 DirEntry, DirEntryKind, FileInfo, FsError, SessionFsCapabilities, SessionFsConfig,
19 SessionFsConventions, SessionFsProvider, SessionFsSqliteProvider, SessionFsSqliteQueryResult,
20 SessionFsSqliteQueryType,
21};
22pub use crate::trace_context::{TraceContext, TraceContextProvider};
23use crate::transforms::SystemMessageTransform;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33#[serde(rename_all = "lowercase")]
34#[non_exhaustive]
35pub enum ConnectionState {
36 Disconnected,
38 Connecting,
40 Connected,
42 Error,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
51#[non_exhaustive]
52pub enum SessionLifecycleEventType {
53 #[serde(rename = "session.created")]
55 Created,
56 #[serde(rename = "session.deleted")]
58 Deleted,
59 #[serde(rename = "session.updated")]
61 Updated,
62 #[serde(rename = "session.foreground")]
64 Foreground,
65 #[serde(rename = "session.background")]
67 Background,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct SessionLifecycleEventMetadata {
73 #[serde(rename = "startTime")]
75 pub start_time: String,
76 #[serde(rename = "modifiedTime")]
78 pub modified_time: String,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub summary: Option<String>,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct SessionLifecycleEvent {
88 #[serde(rename = "type")]
90 pub event_type: SessionLifecycleEventType,
91 #[serde(rename = "sessionId")]
93 pub session_id: SessionId,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub metadata: Option<SessionLifecycleEventMetadata>,
97}
98
99#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
105#[serde(transparent)]
106pub struct SessionId(String);
107
108impl SessionId {
109 pub fn new(id: impl Into<String>) -> Self {
111 Self(id.into())
112 }
113
114 pub fn as_str(&self) -> &str {
116 &self.0
117 }
118
119 pub fn into_inner(self) -> String {
121 self.0
122 }
123}
124
125impl std::ops::Deref for SessionId {
126 type Target = str;
127
128 fn deref(&self) -> &str {
129 &self.0
130 }
131}
132
133impl std::fmt::Display for SessionId {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 f.write_str(&self.0)
136 }
137}
138
139impl From<String> for SessionId {
140 fn from(s: String) -> Self {
141 Self(s)
142 }
143}
144
145impl From<&str> for SessionId {
146 fn from(s: &str) -> Self {
147 Self(s.to_owned())
148 }
149}
150
151impl AsRef<str> for SessionId {
152 fn as_ref(&self) -> &str {
153 &self.0
154 }
155}
156
157impl std::borrow::Borrow<str> for SessionId {
158 fn borrow(&self) -> &str {
159 &self.0
160 }
161}
162
163impl From<SessionId> for String {
164 fn from(id: SessionId) -> String {
165 id.0
166 }
167}
168
169impl PartialEq<str> for SessionId {
170 fn eq(&self, other: &str) -> bool {
171 self.0 == other
172 }
173}
174
175impl PartialEq<String> for SessionId {
176 fn eq(&self, other: &String) -> bool {
177 &self.0 == other
178 }
179}
180
181impl PartialEq<SessionId> for String {
182 fn eq(&self, other: &SessionId) -> bool {
183 self == &other.0
184 }
185}
186
187impl PartialEq<&str> for SessionId {
188 fn eq(&self, other: &&str) -> bool {
189 self.0 == *other
190 }
191}
192
193impl PartialEq<&SessionId> for SessionId {
194 fn eq(&self, other: &&SessionId) -> bool {
195 self.0 == other.0
196 }
197}
198
199impl PartialEq<SessionId> for &SessionId {
200 fn eq(&self, other: &SessionId) -> bool {
201 self.0 == other.0
202 }
203}
204
205#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
211#[serde(transparent)]
212pub struct RequestId(String);
213
214impl RequestId {
215 pub fn new(id: impl Into<String>) -> Self {
217 Self(id.into())
218 }
219
220 pub fn into_inner(self) -> String {
222 self.0
223 }
224}
225
226impl std::ops::Deref for RequestId {
227 type Target = str;
228
229 fn deref(&self) -> &str {
230 &self.0
231 }
232}
233
234impl std::fmt::Display for RequestId {
235 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236 f.write_str(&self.0)
237 }
238}
239
240impl From<String> for RequestId {
241 fn from(s: String) -> Self {
242 Self(s)
243 }
244}
245
246impl From<&str> for RequestId {
247 fn from(s: &str) -> Self {
248 Self(s.to_owned())
249 }
250}
251
252impl AsRef<str> for RequestId {
253 fn as_ref(&self) -> &str {
254 &self.0
255 }
256}
257
258impl std::borrow::Borrow<str> for RequestId {
259 fn borrow(&self) -> &str {
260 &self.0
261 }
262}
263
264impl From<RequestId> for String {
265 fn from(id: RequestId) -> String {
266 id.0
267 }
268}
269
270impl PartialEq<str> for RequestId {
271 fn eq(&self, other: &str) -> bool {
272 self.0 == other
273 }
274}
275
276impl PartialEq<String> for RequestId {
277 fn eq(&self, other: &String) -> bool {
278 &self.0 == other
279 }
280}
281
282impl PartialEq<RequestId> for String {
283 fn eq(&self, other: &RequestId) -> bool {
284 self == &other.0
285 }
286}
287
288impl PartialEq<&str> for RequestId {
289 fn eq(&self, other: &&str) -> bool {
290 self.0 == *other
291 }
292}
293
294#[derive(Debug, Clone, Default, Serialize, Deserialize)]
302#[serde(rename_all = "camelCase")]
303#[non_exhaustive]
304pub struct Tool {
305 pub name: String,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub namespaced_name: Option<String>,
311 #[serde(default)]
313 pub description: String,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
316 pub instructions: Option<String>,
317 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
319 pub parameters: HashMap<String, Value>,
320 #[serde(default, skip_serializing_if = "is_false")]
324 pub overrides_built_in_tool: bool,
325 #[serde(default, skip_serializing_if = "is_false")]
329 pub skip_permission: bool,
330}
331
332#[inline]
333fn is_false(b: &bool) -> bool {
334 !*b
335}
336
337impl Tool {
338 pub fn new(name: impl Into<String>) -> Self {
358 Self {
359 name: name.into(),
360 ..Default::default()
361 }
362 }
363
364 pub fn with_namespaced_name(mut self, namespaced_name: impl Into<String>) -> Self {
367 self.namespaced_name = Some(namespaced_name.into());
368 self
369 }
370
371 pub fn with_description(mut self, description: impl Into<String>) -> Self {
373 self.description = description.into();
374 self
375 }
376
377 pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
379 self.instructions = Some(instructions.into());
380 self
381 }
382
383 pub fn with_parameters(mut self, parameters: Value) -> Self {
391 self.parameters = parameters
392 .as_object()
393 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
394 .unwrap_or_default();
395 self
396 }
397
398 pub fn with_overrides_built_in_tool(mut self, overrides: bool) -> Self {
402 self.overrides_built_in_tool = overrides;
403 self
404 }
405
406 pub fn with_skip_permission(mut self, skip: bool) -> Self {
410 self.skip_permission = skip;
411 self
412 }
413}
414
415#[non_exhaustive]
418#[derive(Debug, Clone)]
419pub struct CommandContext {
420 pub session_id: SessionId,
422 pub command: String,
424 pub command_name: String,
426 pub args: String,
428}
429
430#[async_trait::async_trait]
436pub trait CommandHandler: Send + Sync {
437 async fn on_command(&self, ctx: CommandContext) -> Result<(), crate::Error>;
439}
440
441#[non_exhaustive]
447#[derive(Clone)]
448pub struct CommandDefinition {
449 pub name: String,
451 pub description: Option<String>,
453 pub handler: Arc<dyn CommandHandler>,
455}
456
457impl CommandDefinition {
458 pub fn new(name: impl Into<String>, handler: Arc<dyn CommandHandler>) -> Self {
461 Self {
462 name: name.into(),
463 description: None,
464 handler,
465 }
466 }
467
468 pub fn with_description(mut self, description: impl Into<String>) -> Self {
470 self.description = Some(description.into());
471 self
472 }
473}
474
475impl std::fmt::Debug for CommandDefinition {
476 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
477 f.debug_struct("CommandDefinition")
478 .field("name", &self.name)
479 .field("description", &self.description)
480 .field("handler", &"<set>")
481 .finish()
482 }
483}
484
485impl Serialize for CommandDefinition {
486 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
487 use serde::ser::SerializeStruct;
488 let len = if self.description.is_some() { 2 } else { 1 };
489 let mut state = serializer.serialize_struct("CommandDefinition", len)?;
490 state.serialize_field("name", &self.name)?;
491 if let Some(description) = &self.description {
492 state.serialize_field("description", description)?;
493 }
494 state.end()
495 }
496}
497
498#[derive(Debug, Clone, Default, Serialize, Deserialize)]
505#[serde(rename_all = "camelCase")]
506#[non_exhaustive]
507pub struct CustomAgentConfig {
508 pub name: String,
510 #[serde(default, skip_serializing_if = "Option::is_none")]
512 pub display_name: Option<String>,
513 #[serde(default, skip_serializing_if = "Option::is_none")]
515 pub description: Option<String>,
516 #[serde(default, skip_serializing_if = "Option::is_none")]
518 pub tools: Option<Vec<String>>,
519 pub prompt: String,
521 #[serde(default, skip_serializing_if = "Option::is_none")]
523 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
524 #[serde(default, skip_serializing_if = "Option::is_none")]
526 pub infer: Option<bool>,
527 #[serde(default, skip_serializing_if = "Option::is_none")]
529 pub skills: Option<Vec<String>>,
530 #[serde(default, skip_serializing_if = "Option::is_none")]
535 pub model: Option<String>,
536}
537
538impl CustomAgentConfig {
539 pub fn new(name: impl Into<String>, prompt: impl Into<String>) -> Self {
546 Self {
547 name: name.into(),
548 prompt: prompt.into(),
549 ..Self::default()
550 }
551 }
552
553 pub fn with_display_name(mut self, display_name: impl Into<String>) -> Self {
555 self.display_name = Some(display_name.into());
556 self
557 }
558
559 pub fn with_description(mut self, description: impl Into<String>) -> Self {
561 self.description = Some(description.into());
562 self
563 }
564
565 pub fn with_tools<I, S>(mut self, tools: I) -> Self
568 where
569 I: IntoIterator<Item = S>,
570 S: Into<String>,
571 {
572 self.tools = Some(tools.into_iter().map(Into::into).collect());
573 self
574 }
575
576 pub fn with_mcp_servers(mut self, mcp_servers: HashMap<String, McpServerConfig>) -> Self {
578 self.mcp_servers = Some(mcp_servers);
579 self
580 }
581
582 pub fn with_infer(mut self, infer: bool) -> Self {
584 self.infer = Some(infer);
585 self
586 }
587
588 pub fn with_skills<I, S>(mut self, skills: I) -> Self
590 where
591 I: IntoIterator<Item = S>,
592 S: Into<String>,
593 {
594 self.skills = Some(skills.into_iter().map(Into::into).collect());
595 self
596 }
597
598 pub fn with_model(mut self, model: impl Into<String>) -> Self {
600 self.model = Some(model.into());
601 self
602 }
603}
604
605#[derive(Debug, Clone, Default, Serialize, Deserialize)]
612#[serde(rename_all = "camelCase")]
613pub struct DefaultAgentConfig {
614 #[serde(default, skip_serializing_if = "Option::is_none")]
616 pub excluded_tools: Option<Vec<String>>,
617}
618
619#[derive(Debug, Clone, Default, Serialize, Deserialize)]
626#[serde(rename_all = "camelCase")]
627#[non_exhaustive]
628pub struct InfiniteSessionConfig {
629 #[serde(default, skip_serializing_if = "Option::is_none")]
631 pub enabled: Option<bool>,
632 #[serde(default, skip_serializing_if = "Option::is_none")]
635 pub background_compaction_threshold: Option<f64>,
636 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub buffer_exhaustion_threshold: Option<f64>,
640}
641
642impl InfiniteSessionConfig {
643 pub fn new() -> Self {
646 Self::default()
647 }
648
649 pub fn with_enabled(mut self, enabled: bool) -> Self {
652 self.enabled = Some(enabled);
653 self
654 }
655
656 pub fn with_background_compaction_threshold(mut self, threshold: f64) -> Self {
659 self.background_compaction_threshold = Some(threshold);
660 self
661 }
662
663 pub fn with_buffer_exhaustion_threshold(mut self, threshold: f64) -> Self {
666 self.buffer_exhaustion_threshold = Some(threshold);
667 self
668 }
669}
670
671#[derive(Debug, Clone, Serialize, Deserialize)]
673#[serde(rename_all = "camelCase")]
674#[non_exhaustive]
675pub struct CloudSessionRepository {
676 pub owner: String,
678 pub name: String,
680 #[serde(skip_serializing_if = "Option::is_none")]
682 pub branch: Option<String>,
683}
684
685impl CloudSessionRepository {
686 pub fn new(owner: impl Into<String>, name: impl Into<String>) -> Self {
688 Self {
689 owner: owner.into(),
690 name: name.into(),
691 branch: None,
692 }
693 }
694
695 pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
697 self.branch = Some(branch.into());
698 self
699 }
700}
701
702#[derive(Debug, Clone, Default, Serialize, Deserialize)]
704#[serde(rename_all = "camelCase")]
705#[non_exhaustive]
706pub struct CloudSessionOptions {
707 #[serde(skip_serializing_if = "Option::is_none")]
709 pub repository: Option<CloudSessionRepository>,
710}
711
712impl CloudSessionOptions {
713 pub fn with_repository(repository: CloudSessionRepository) -> Self {
715 Self {
716 repository: Some(repository),
717 }
718 }
719}
720
721#[derive(Debug, Clone, Serialize, Deserialize)]
755#[serde(tag = "type", rename_all = "lowercase")]
756#[non_exhaustive]
757pub enum McpServerConfig {
758 #[serde(alias = "local")]
762 Stdio(McpStdioServerConfig),
763 Http(McpHttpServerConfig),
765 Sse(McpHttpServerConfig),
767}
768
769#[derive(Debug, Clone, Default, Serialize, Deserialize)]
773#[serde(rename_all = "camelCase")]
774pub struct McpStdioServerConfig {
775 #[serde(default)]
777 pub tools: Vec<String>,
778 #[serde(default, skip_serializing_if = "Option::is_none")]
780 pub timeout: Option<i64>,
781 pub command: String,
783 #[serde(default, skip_serializing_if = "Vec::is_empty")]
785 pub args: Vec<String>,
786 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
789 pub env: HashMap<String, String>,
790 #[serde(default, skip_serializing_if = "Option::is_none")]
792 pub cwd: Option<String>,
793}
794
795#[derive(Debug, Clone, Default, Serialize, Deserialize)]
799#[serde(rename_all = "camelCase")]
800pub struct McpHttpServerConfig {
801 #[serde(default)]
803 pub tools: Vec<String>,
804 #[serde(default, skip_serializing_if = "Option::is_none")]
806 pub timeout: Option<i64>,
807 pub url: String,
809 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
811 pub headers: HashMap<String, String>,
812}
813
814#[derive(Debug, Clone, Default, Serialize, Deserialize)]
820#[serde(rename_all = "camelCase")]
821#[non_exhaustive]
822pub struct ProviderConfig {
823 #[serde(default, skip_serializing_if = "Option::is_none", rename = "type")]
826 pub provider_type: Option<String>,
827 #[serde(default, skip_serializing_if = "Option::is_none")]
830 pub wire_api: Option<String>,
831 pub base_url: String,
833 #[serde(default, skip_serializing_if = "Option::is_none")]
835 pub api_key: Option<String>,
836 #[serde(default, skip_serializing_if = "Option::is_none")]
840 pub bearer_token: Option<String>,
841 #[serde(default, skip_serializing_if = "Option::is_none")]
843 pub azure: Option<AzureProviderOptions>,
844 #[serde(default, skip_serializing_if = "Option::is_none")]
846 pub headers: Option<HashMap<String, String>>,
847 #[serde(default, skip_serializing_if = "Option::is_none")]
851 pub model_id: Option<String>,
852 #[serde(default, skip_serializing_if = "Option::is_none")]
859 pub wire_model: Option<String>,
860 #[serde(default, skip_serializing_if = "Option::is_none")]
865 pub max_prompt_tokens: Option<i64>,
866 #[serde(default, skip_serializing_if = "Option::is_none")]
869 pub max_output_tokens: Option<i64>,
870}
871
872impl ProviderConfig {
873 pub fn new(base_url: impl Into<String>) -> Self {
876 Self {
877 base_url: base_url.into(),
878 ..Self::default()
879 }
880 }
881
882 pub fn with_provider_type(mut self, provider_type: impl Into<String>) -> Self {
884 self.provider_type = Some(provider_type.into());
885 self
886 }
887
888 pub fn with_wire_api(mut self, wire_api: impl Into<String>) -> Self {
890 self.wire_api = Some(wire_api.into());
891 self
892 }
893
894 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
896 self.api_key = Some(api_key.into());
897 self
898 }
899
900 pub fn with_bearer_token(mut self, bearer_token: impl Into<String>) -> Self {
903 self.bearer_token = Some(bearer_token.into());
904 self
905 }
906
907 pub fn with_azure(mut self, azure: AzureProviderOptions) -> Self {
909 self.azure = Some(azure);
910 self
911 }
912
913 pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
915 self.headers = Some(headers);
916 self
917 }
918
919 pub fn with_model_id(mut self, model_id: impl Into<String>) -> Self {
922 self.model_id = Some(model_id.into());
923 self
924 }
925
926 pub fn with_wire_model(mut self, wire_model: impl Into<String>) -> Self {
931 self.wire_model = Some(wire_model.into());
932 self
933 }
934
935 pub fn with_max_prompt_tokens(mut self, max: i64) -> Self {
939 self.max_prompt_tokens = Some(max);
940 self
941 }
942
943 pub fn with_max_output_tokens(mut self, max: i64) -> Self {
946 self.max_output_tokens = Some(max);
947 self
948 }
949}
950
951#[derive(Debug, Clone, Default, Serialize, Deserialize)]
953#[serde(rename_all = "camelCase")]
954pub struct AzureProviderOptions {
955 #[serde(default, skip_serializing_if = "Option::is_none")]
957 pub api_version: Option<String>,
958}
959
960fn default_env_value_mode() -> String {
965 "direct".into()
966}
967
968#[derive(Clone, Serialize, Deserialize)]
1015#[serde(rename_all = "camelCase")]
1016#[non_exhaustive]
1017pub struct SessionConfig {
1018 #[serde(skip_serializing_if = "Option::is_none")]
1020 pub session_id: Option<SessionId>,
1021 #[serde(skip_serializing_if = "Option::is_none")]
1023 pub model: Option<String>,
1024 #[serde(skip_serializing_if = "Option::is_none")]
1026 pub client_name: Option<String>,
1027 #[serde(skip_serializing_if = "Option::is_none")]
1029 pub reasoning_effort: Option<String>,
1030 #[serde(skip_serializing_if = "Option::is_none")]
1032 pub streaming: Option<bool>,
1033 #[serde(skip_serializing_if = "Option::is_none")]
1035 pub system_message: Option<SystemMessageConfig>,
1036 #[serde(skip_serializing_if = "Option::is_none")]
1038 pub tools: Option<Vec<Tool>>,
1039 #[serde(skip_serializing_if = "Option::is_none")]
1041 pub available_tools: Option<Vec<String>>,
1042 #[serde(skip_serializing_if = "Option::is_none")]
1044 pub excluded_tools: Option<Vec<String>>,
1045 #[serde(skip_serializing_if = "Option::is_none")]
1047 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
1048 #[serde(default = "default_env_value_mode", skip_deserializing)]
1052 pub(crate) env_value_mode: String,
1053 #[serde(skip_serializing_if = "Option::is_none")]
1055 pub enable_config_discovery: Option<bool>,
1056 #[serde(skip_serializing_if = "Option::is_none")]
1059 pub request_user_input: Option<bool>,
1060 #[serde(skip_serializing_if = "Option::is_none")]
1065 pub request_permission: Option<bool>,
1066 #[serde(skip_serializing_if = "Option::is_none")]
1069 pub request_exit_plan_mode: Option<bool>,
1070 #[serde(skip_serializing_if = "Option::is_none")]
1076 pub request_auto_mode_switch: Option<bool>,
1077 #[serde(skip_serializing_if = "Option::is_none")]
1081 pub request_elicitation: Option<bool>,
1082 #[serde(skip_serializing_if = "Option::is_none")]
1084 pub skill_directories: Option<Vec<PathBuf>>,
1085 #[serde(skip_serializing_if = "Option::is_none")]
1088 pub instruction_directories: Option<Vec<PathBuf>>,
1089 #[serde(skip_serializing_if = "Option::is_none")]
1092 pub disabled_skills: Option<Vec<String>>,
1093 #[serde(skip_serializing_if = "Option::is_none")]
1097 pub hooks: Option<bool>,
1098 #[serde(skip_serializing_if = "Option::is_none")]
1100 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1101 #[serde(skip_serializing_if = "Option::is_none")]
1105 pub default_agent: Option<DefaultAgentConfig>,
1106 #[serde(skip_serializing_if = "Option::is_none")]
1109 pub agent: Option<String>,
1110 #[serde(skip_serializing_if = "Option::is_none")]
1113 pub infinite_sessions: Option<InfiniteSessionConfig>,
1114 #[serde(skip_serializing_if = "Option::is_none")]
1118 pub provider: Option<ProviderConfig>,
1119 #[serde(skip_serializing_if = "Option::is_none")]
1127 pub enable_session_telemetry: Option<bool>,
1128 #[serde(skip_serializing_if = "Option::is_none")]
1131 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1132 #[serde(skip_serializing_if = "Option::is_none")]
1135 pub config_dir: Option<PathBuf>,
1136 #[serde(skip_serializing_if = "Option::is_none")]
1139 pub working_directory: Option<PathBuf>,
1140 #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")]
1146 pub github_token: Option<String>,
1147 #[serde(skip_serializing_if = "Option::is_none")]
1153 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
1154 #[serde(skip_serializing_if = "Option::is_none")]
1157 pub cloud: Option<CloudSessionOptions>,
1158 #[serde(skip_serializing_if = "Option::is_none")]
1162 pub include_sub_agent_streaming_events: Option<bool>,
1163 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
1167 pub commands: Option<Vec<CommandDefinition>>,
1168 #[serde(skip)]
1173 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1174 #[serde(skip)]
1179 pub handler: Option<Arc<dyn SessionHandler>>,
1180 #[serde(skip)]
1184 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1185 #[serde(skip)]
1190 pub transform: Option<Arc<dyn SystemMessageTransform>>,
1191}
1192
1193impl std::fmt::Debug for SessionConfig {
1194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1195 f.debug_struct("SessionConfig")
1196 .field("session_id", &self.session_id)
1197 .field("model", &self.model)
1198 .field("client_name", &self.client_name)
1199 .field("reasoning_effort", &self.reasoning_effort)
1200 .field("streaming", &self.streaming)
1201 .field("system_message", &self.system_message)
1202 .field("tools", &self.tools)
1203 .field("available_tools", &self.available_tools)
1204 .field("excluded_tools", &self.excluded_tools)
1205 .field("mcp_servers", &self.mcp_servers)
1206 .field("enable_config_discovery", &self.enable_config_discovery)
1207 .field("request_user_input", &self.request_user_input)
1208 .field("request_permission", &self.request_permission)
1209 .field("request_exit_plan_mode", &self.request_exit_plan_mode)
1210 .field("request_auto_mode_switch", &self.request_auto_mode_switch)
1211 .field("request_elicitation", &self.request_elicitation)
1212 .field("skill_directories", &self.skill_directories)
1213 .field("instruction_directories", &self.instruction_directories)
1214 .field("disabled_skills", &self.disabled_skills)
1215 .field("hooks", &self.hooks)
1216 .field("custom_agents", &self.custom_agents)
1217 .field("default_agent", &self.default_agent)
1218 .field("agent", &self.agent)
1219 .field("infinite_sessions", &self.infinite_sessions)
1220 .field("provider", &self.provider)
1221 .field("enable_session_telemetry", &self.enable_session_telemetry)
1222 .field("model_capabilities", &self.model_capabilities)
1223 .field("config_dir", &self.config_dir)
1224 .field("working_directory", &self.working_directory)
1225 .field(
1226 "github_token",
1227 &self.github_token.as_ref().map(|_| "<redacted>"),
1228 )
1229 .field("remote_session", &self.remote_session)
1230 .field("cloud", &self.cloud)
1231 .field(
1232 "include_sub_agent_streaming_events",
1233 &self.include_sub_agent_streaming_events,
1234 )
1235 .field("commands", &self.commands)
1236 .field(
1237 "session_fs_provider",
1238 &self.session_fs_provider.as_ref().map(|_| "<set>"),
1239 )
1240 .field("handler", &self.handler.as_ref().map(|_| "<set>"))
1241 .field(
1242 "hooks_handler",
1243 &self.hooks_handler.as_ref().map(|_| "<set>"),
1244 )
1245 .field("transform", &self.transform.as_ref().map(|_| "<set>"))
1246 .finish()
1247 }
1248}
1249
1250impl Default for SessionConfig {
1251 fn default() -> Self {
1257 Self {
1258 session_id: None,
1259 model: None,
1260 client_name: None,
1261 reasoning_effort: None,
1262 streaming: None,
1263 system_message: None,
1264 tools: None,
1265 available_tools: None,
1266 excluded_tools: None,
1267 mcp_servers: None,
1268 env_value_mode: default_env_value_mode(),
1269 enable_config_discovery: None,
1270 request_user_input: Some(true),
1271 request_permission: Some(true),
1272 request_exit_plan_mode: Some(true),
1273 request_auto_mode_switch: Some(true),
1274 request_elicitation: Some(true),
1275 skill_directories: None,
1276 instruction_directories: None,
1277 disabled_skills: None,
1278 hooks: None,
1279 custom_agents: None,
1280 default_agent: None,
1281 agent: None,
1282 infinite_sessions: None,
1283 provider: None,
1284 enable_session_telemetry: None,
1285 model_capabilities: None,
1286 config_dir: None,
1287 working_directory: None,
1288 github_token: None,
1289 remote_session: None,
1290 cloud: None,
1291 include_sub_agent_streaming_events: None,
1292 commands: None,
1293 session_fs_provider: None,
1294 handler: None,
1295 hooks_handler: None,
1296 transform: None,
1297 }
1298 }
1299}
1300
1301impl SessionConfig {
1302 pub fn with_handler(mut self, handler: Arc<dyn SessionHandler>) -> Self {
1304 self.handler = Some(handler);
1305 self
1306 }
1307
1308 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
1313 self.commands = Some(commands);
1314 self
1315 }
1316
1317 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
1321 self.session_fs_provider = Some(provider);
1322 self
1323 }
1324
1325 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
1328 self.hooks_handler = Some(hooks);
1329 self
1330 }
1331
1332 pub fn with_transform(mut self, transform: Arc<dyn SystemMessageTransform>) -> Self {
1336 self.transform = Some(transform);
1337 self
1338 }
1339
1340 pub fn approve_all_permissions(mut self) -> Self {
1354 let inner = self
1355 .handler
1356 .take()
1357 .unwrap_or_else(|| Arc::new(crate::handler::NoopHandler));
1358 self.handler = Some(crate::permission::approve_all(inner));
1359 self
1360 }
1361
1362 pub fn deny_all_permissions(mut self) -> Self {
1366 let inner = self
1367 .handler
1368 .take()
1369 .unwrap_or_else(|| Arc::new(crate::handler::NoopHandler));
1370 self.handler = Some(crate::permission::deny_all(inner));
1371 self
1372 }
1373
1374 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
1380 where
1381 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
1382 {
1383 let inner = self
1384 .handler
1385 .take()
1386 .unwrap_or_else(|| Arc::new(crate::handler::NoopHandler));
1387 self.handler = Some(crate::permission::approve_if(inner, predicate));
1388 self
1389 }
1390
1391 pub fn with_session_id(mut self, id: impl Into<SessionId>) -> Self {
1393 self.session_id = Some(id.into());
1394 self
1395 }
1396
1397 pub fn with_model(mut self, model: impl Into<String>) -> Self {
1399 self.model = Some(model.into());
1400 self
1401 }
1402
1403 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
1405 self.client_name = Some(name.into());
1406 self
1407 }
1408
1409 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
1411 self.reasoning_effort = Some(effort.into());
1412 self
1413 }
1414
1415 pub fn with_streaming(mut self, streaming: bool) -> Self {
1417 self.streaming = Some(streaming);
1418 self
1419 }
1420
1421 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
1423 self.system_message = Some(system_message);
1424 self
1425 }
1426
1427 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
1429 self.tools = Some(tools.into_iter().collect());
1430 self
1431 }
1432
1433 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
1435 where
1436 I: IntoIterator<Item = S>,
1437 S: Into<String>,
1438 {
1439 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
1440 self
1441 }
1442
1443 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
1445 where
1446 I: IntoIterator<Item = S>,
1447 S: Into<String>,
1448 {
1449 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
1450 self
1451 }
1452
1453 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
1455 self.mcp_servers = Some(servers);
1456 self
1457 }
1458
1459 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
1461 self.enable_config_discovery = Some(enable);
1462 self
1463 }
1464
1465 pub fn with_request_user_input(mut self, enable: bool) -> Self {
1467 self.request_user_input = Some(enable);
1468 self
1469 }
1470
1471 pub fn with_request_permission(mut self, enable: bool) -> Self {
1473 self.request_permission = Some(enable);
1474 self
1475 }
1476
1477 pub fn with_request_exit_plan_mode(mut self, enable: bool) -> Self {
1479 self.request_exit_plan_mode = Some(enable);
1480 self
1481 }
1482
1483 pub fn with_request_auto_mode_switch(mut self, enable: bool) -> Self {
1485 self.request_auto_mode_switch = Some(enable);
1486 self
1487 }
1488
1489 pub fn with_request_elicitation(mut self, enable: bool) -> Self {
1491 self.request_elicitation = Some(enable);
1492 self
1493 }
1494
1495 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
1497 where
1498 I: IntoIterator<Item = P>,
1499 P: Into<PathBuf>,
1500 {
1501 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
1502 self
1503 }
1504
1505 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
1509 where
1510 I: IntoIterator<Item = P>,
1511 P: Into<PathBuf>,
1512 {
1513 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
1514 self
1515 }
1516
1517 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
1519 where
1520 I: IntoIterator<Item = S>,
1521 S: Into<String>,
1522 {
1523 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
1524 self
1525 }
1526
1527 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
1529 mut self,
1530 agents: I,
1531 ) -> Self {
1532 self.custom_agents = Some(agents.into_iter().collect());
1533 self
1534 }
1535
1536 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
1538 self.default_agent = Some(agent);
1539 self
1540 }
1541
1542 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
1545 self.agent = Some(name.into());
1546 self
1547 }
1548
1549 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
1552 self.infinite_sessions = Some(config);
1553 self
1554 }
1555
1556 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
1558 self.provider = Some(provider);
1559 self
1560 }
1561
1562 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
1566 self.enable_session_telemetry = Some(enable);
1567 self
1568 }
1569
1570 pub fn with_model_capabilities(
1572 mut self,
1573 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
1574 ) -> Self {
1575 self.model_capabilities = Some(capabilities);
1576 self
1577 }
1578
1579 pub fn with_config_dir(mut self, dir: impl Into<PathBuf>) -> Self {
1581 self.config_dir = Some(dir.into());
1582 self
1583 }
1584
1585 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
1588 self.working_directory = Some(dir.into());
1589 self
1590 }
1591
1592 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
1597 self.github_token = Some(token.into());
1598 self
1599 }
1600
1601 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
1604 self.include_sub_agent_streaming_events = Some(include);
1605 self
1606 }
1607
1608 pub fn with_remote_session(
1610 mut self,
1611 mode: crate::generated::api_types::RemoteSessionMode,
1612 ) -> Self {
1613 self.remote_session = Some(mode);
1614 self
1615 }
1616
1617 pub fn with_cloud(mut self, cloud: CloudSessionOptions) -> Self {
1619 self.cloud = Some(cloud);
1620 self
1621 }
1622}
1623
1624#[derive(Clone, Serialize, Deserialize)]
1630#[serde(rename_all = "camelCase")]
1631#[non_exhaustive]
1632pub struct ResumeSessionConfig {
1633 pub session_id: SessionId,
1635 #[serde(skip_serializing_if = "Option::is_none")]
1637 pub client_name: Option<String>,
1638 #[serde(skip_serializing_if = "Option::is_none")]
1640 pub reasoning_effort: Option<String>,
1641 #[serde(skip_serializing_if = "Option::is_none")]
1643 pub streaming: Option<bool>,
1644 #[serde(skip_serializing_if = "Option::is_none")]
1647 pub system_message: Option<SystemMessageConfig>,
1648 #[serde(skip_serializing_if = "Option::is_none")]
1650 pub tools: Option<Vec<Tool>>,
1651 #[serde(skip_serializing_if = "Option::is_none")]
1653 pub available_tools: Option<Vec<String>>,
1654 #[serde(skip_serializing_if = "Option::is_none")]
1656 pub excluded_tools: Option<Vec<String>>,
1657 #[serde(skip_serializing_if = "Option::is_none")]
1659 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
1660 #[serde(default = "default_env_value_mode", skip_deserializing)]
1662 pub(crate) env_value_mode: String,
1663 #[serde(skip_serializing_if = "Option::is_none")]
1665 pub enable_config_discovery: Option<bool>,
1666 #[serde(skip_serializing_if = "Option::is_none")]
1668 pub request_user_input: Option<bool>,
1669 #[serde(skip_serializing_if = "Option::is_none")]
1672 pub request_permission: Option<bool>,
1673 #[serde(skip_serializing_if = "Option::is_none")]
1675 pub request_exit_plan_mode: Option<bool>,
1676 #[serde(skip_serializing_if = "Option::is_none")]
1680 pub request_auto_mode_switch: Option<bool>,
1681 #[serde(skip_serializing_if = "Option::is_none")]
1683 pub request_elicitation: Option<bool>,
1684 #[serde(skip_serializing_if = "Option::is_none")]
1686 pub skill_directories: Option<Vec<PathBuf>>,
1687 #[serde(skip_serializing_if = "Option::is_none")]
1690 pub instruction_directories: Option<Vec<PathBuf>>,
1691 #[serde(skip_serializing_if = "Option::is_none")]
1693 pub disabled_skills: Option<Vec<String>>,
1694 #[serde(skip_serializing_if = "Option::is_none")]
1696 pub hooks: Option<bool>,
1697 #[serde(skip_serializing_if = "Option::is_none")]
1699 pub custom_agents: Option<Vec<CustomAgentConfig>>,
1700 #[serde(skip_serializing_if = "Option::is_none")]
1702 pub default_agent: Option<DefaultAgentConfig>,
1703 #[serde(skip_serializing_if = "Option::is_none")]
1705 pub agent: Option<String>,
1706 #[serde(skip_serializing_if = "Option::is_none")]
1708 pub infinite_sessions: Option<InfiniteSessionConfig>,
1709 #[serde(skip_serializing_if = "Option::is_none")]
1711 pub provider: Option<ProviderConfig>,
1712 #[serde(skip_serializing_if = "Option::is_none")]
1720 pub enable_session_telemetry: Option<bool>,
1721 #[serde(skip_serializing_if = "Option::is_none")]
1723 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
1724 #[serde(skip_serializing_if = "Option::is_none")]
1726 pub config_dir: Option<PathBuf>,
1727 #[serde(skip_serializing_if = "Option::is_none")]
1729 pub working_directory: Option<PathBuf>,
1730 #[serde(rename = "gitHubToken", skip_serializing_if = "Option::is_none")]
1733 pub github_token: Option<String>,
1734 #[serde(skip_serializing_if = "Option::is_none")]
1737 pub remote_session: Option<crate::generated::api_types::RemoteSessionMode>,
1738 #[serde(skip_serializing_if = "Option::is_none")]
1740 pub include_sub_agent_streaming_events: Option<bool>,
1741 #[serde(skip_serializing_if = "Option::is_none", skip_deserializing)]
1745 pub commands: Option<Vec<CommandDefinition>>,
1746 #[serde(skip)]
1751 pub session_fs_provider: Option<Arc<dyn SessionFsProvider>>,
1752 #[serde(skip_serializing_if = "Option::is_none")]
1755 pub disable_resume: Option<bool>,
1756 #[serde(skip_serializing_if = "Option::is_none")]
1764 pub continue_pending_work: Option<bool>,
1765 #[serde(skip)]
1767 pub handler: Option<Arc<dyn SessionHandler>>,
1768 #[serde(skip)]
1770 pub hooks_handler: Option<Arc<dyn SessionHooks>>,
1771 #[serde(skip)]
1773 pub transform: Option<Arc<dyn SystemMessageTransform>>,
1774}
1775
1776impl std::fmt::Debug for ResumeSessionConfig {
1777 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1778 f.debug_struct("ResumeSessionConfig")
1779 .field("session_id", &self.session_id)
1780 .field("client_name", &self.client_name)
1781 .field("reasoning_effort", &self.reasoning_effort)
1782 .field("streaming", &self.streaming)
1783 .field("system_message", &self.system_message)
1784 .field("tools", &self.tools)
1785 .field("available_tools", &self.available_tools)
1786 .field("excluded_tools", &self.excluded_tools)
1787 .field("mcp_servers", &self.mcp_servers)
1788 .field("enable_config_discovery", &self.enable_config_discovery)
1789 .field("request_user_input", &self.request_user_input)
1790 .field("request_permission", &self.request_permission)
1791 .field("request_exit_plan_mode", &self.request_exit_plan_mode)
1792 .field("request_auto_mode_switch", &self.request_auto_mode_switch)
1793 .field("request_elicitation", &self.request_elicitation)
1794 .field("skill_directories", &self.skill_directories)
1795 .field("instruction_directories", &self.instruction_directories)
1796 .field("disabled_skills", &self.disabled_skills)
1797 .field("hooks", &self.hooks)
1798 .field("custom_agents", &self.custom_agents)
1799 .field("default_agent", &self.default_agent)
1800 .field("agent", &self.agent)
1801 .field("infinite_sessions", &self.infinite_sessions)
1802 .field("provider", &self.provider)
1803 .field("enable_session_telemetry", &self.enable_session_telemetry)
1804 .field("model_capabilities", &self.model_capabilities)
1805 .field("config_dir", &self.config_dir)
1806 .field("working_directory", &self.working_directory)
1807 .field(
1808 "github_token",
1809 &self.github_token.as_ref().map(|_| "<redacted>"),
1810 )
1811 .field("remote_session", &self.remote_session)
1812 .field(
1813 "include_sub_agent_streaming_events",
1814 &self.include_sub_agent_streaming_events,
1815 )
1816 .field("commands", &self.commands)
1817 .field(
1818 "session_fs_provider",
1819 &self.session_fs_provider.as_ref().map(|_| "<set>"),
1820 )
1821 .field("handler", &self.handler.as_ref().map(|_| "<set>"))
1822 .field(
1823 "hooks_handler",
1824 &self.hooks_handler.as_ref().map(|_| "<set>"),
1825 )
1826 .field("transform", &self.transform.as_ref().map(|_| "<set>"))
1827 .field("disable_resume", &self.disable_resume)
1828 .field("continue_pending_work", &self.continue_pending_work)
1829 .finish()
1830 }
1831}
1832
1833impl ResumeSessionConfig {
1834 pub fn new(session_id: SessionId) -> Self {
1839 Self {
1840 session_id,
1841 client_name: None,
1842 reasoning_effort: None,
1843 streaming: None,
1844 system_message: None,
1845 tools: None,
1846 available_tools: None,
1847 excluded_tools: None,
1848 mcp_servers: None,
1849 env_value_mode: default_env_value_mode(),
1850 enable_config_discovery: None,
1851 request_user_input: Some(true),
1852 request_permission: Some(true),
1853 request_exit_plan_mode: Some(true),
1854 request_auto_mode_switch: Some(true),
1855 request_elicitation: Some(true),
1856 skill_directories: None,
1857 instruction_directories: None,
1858 disabled_skills: None,
1859 hooks: None,
1860 custom_agents: None,
1861 default_agent: None,
1862 agent: None,
1863 infinite_sessions: None,
1864 provider: None,
1865 enable_session_telemetry: None,
1866 model_capabilities: None,
1867 config_dir: None,
1868 working_directory: None,
1869 github_token: None,
1870 remote_session: None,
1871 include_sub_agent_streaming_events: None,
1872 commands: None,
1873 session_fs_provider: None,
1874 disable_resume: None,
1875 continue_pending_work: None,
1876 handler: None,
1877 hooks_handler: None,
1878 transform: None,
1879 }
1880 }
1881
1882 pub fn with_handler(mut self, handler: Arc<dyn SessionHandler>) -> Self {
1884 self.handler = Some(handler);
1885 self
1886 }
1887
1888 pub fn with_hooks(mut self, hooks: Arc<dyn SessionHooks>) -> Self {
1891 self.hooks_handler = Some(hooks);
1892 self
1893 }
1894
1895 pub fn with_transform(mut self, transform: Arc<dyn SystemMessageTransform>) -> Self {
1897 self.transform = Some(transform);
1898 self
1899 }
1900
1901 pub fn with_commands(mut self, commands: Vec<CommandDefinition>) -> Self {
1905 self.commands = Some(commands);
1906 self
1907 }
1908
1909 pub fn with_session_fs_provider(mut self, provider: Arc<dyn SessionFsProvider>) -> Self {
1912 self.session_fs_provider = Some(provider);
1913 self
1914 }
1915
1916 pub fn approve_all_permissions(mut self) -> Self {
1920 let inner = self
1921 .handler
1922 .take()
1923 .unwrap_or_else(|| Arc::new(crate::handler::NoopHandler));
1924 self.handler = Some(crate::permission::approve_all(inner));
1925 self
1926 }
1927
1928 pub fn deny_all_permissions(mut self) -> Self {
1932 let inner = self
1933 .handler
1934 .take()
1935 .unwrap_or_else(|| Arc::new(crate::handler::NoopHandler));
1936 self.handler = Some(crate::permission::deny_all(inner));
1937 self
1938 }
1939
1940 pub fn approve_permissions_if<F>(mut self, predicate: F) -> Self
1943 where
1944 F: Fn(&crate::types::PermissionRequestData) -> bool + Send + Sync + 'static,
1945 {
1946 let inner = self
1947 .handler
1948 .take()
1949 .unwrap_or_else(|| Arc::new(crate::handler::NoopHandler));
1950 self.handler = Some(crate::permission::approve_if(inner, predicate));
1951 self
1952 }
1953
1954 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
1956 self.client_name = Some(name.into());
1957 self
1958 }
1959
1960 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
1962 self.reasoning_effort = Some(effort.into());
1963 self
1964 }
1965
1966 pub fn with_streaming(mut self, streaming: bool) -> Self {
1968 self.streaming = Some(streaming);
1969 self
1970 }
1971
1972 pub fn with_system_message(mut self, system_message: SystemMessageConfig) -> Self {
1975 self.system_message = Some(system_message);
1976 self
1977 }
1978
1979 pub fn with_tools<I: IntoIterator<Item = Tool>>(mut self, tools: I) -> Self {
1981 self.tools = Some(tools.into_iter().collect());
1982 self
1983 }
1984
1985 pub fn with_available_tools<I, S>(mut self, tools: I) -> Self
1987 where
1988 I: IntoIterator<Item = S>,
1989 S: Into<String>,
1990 {
1991 self.available_tools = Some(tools.into_iter().map(Into::into).collect());
1992 self
1993 }
1994
1995 pub fn with_excluded_tools<I, S>(mut self, tools: I) -> Self
1997 where
1998 I: IntoIterator<Item = S>,
1999 S: Into<String>,
2000 {
2001 self.excluded_tools = Some(tools.into_iter().map(Into::into).collect());
2002 self
2003 }
2004
2005 pub fn with_mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
2007 self.mcp_servers = Some(servers);
2008 self
2009 }
2010
2011 pub fn with_enable_config_discovery(mut self, enable: bool) -> Self {
2013 self.enable_config_discovery = Some(enable);
2014 self
2015 }
2016
2017 pub fn with_request_user_input(mut self, enable: bool) -> Self {
2019 self.request_user_input = Some(enable);
2020 self
2021 }
2022
2023 pub fn with_request_permission(mut self, enable: bool) -> Self {
2025 self.request_permission = Some(enable);
2026 self
2027 }
2028
2029 pub fn with_request_exit_plan_mode(mut self, enable: bool) -> Self {
2031 self.request_exit_plan_mode = Some(enable);
2032 self
2033 }
2034
2035 pub fn with_request_auto_mode_switch(mut self, enable: bool) -> Self {
2037 self.request_auto_mode_switch = Some(enable);
2038 self
2039 }
2040
2041 pub fn with_request_elicitation(mut self, enable: bool) -> Self {
2043 self.request_elicitation = Some(enable);
2044 self
2045 }
2046
2047 pub fn with_skill_directories<I, P>(mut self, paths: I) -> Self
2049 where
2050 I: IntoIterator<Item = P>,
2051 P: Into<PathBuf>,
2052 {
2053 self.skill_directories = Some(paths.into_iter().map(Into::into).collect());
2054 self
2055 }
2056
2057 pub fn with_instruction_directories<I, P>(mut self, paths: I) -> Self
2061 where
2062 I: IntoIterator<Item = P>,
2063 P: Into<PathBuf>,
2064 {
2065 self.instruction_directories = Some(paths.into_iter().map(Into::into).collect());
2066 self
2067 }
2068
2069 pub fn with_disabled_skills<I, S>(mut self, names: I) -> Self
2071 where
2072 I: IntoIterator<Item = S>,
2073 S: Into<String>,
2074 {
2075 self.disabled_skills = Some(names.into_iter().map(Into::into).collect());
2076 self
2077 }
2078
2079 pub fn with_custom_agents<I: IntoIterator<Item = CustomAgentConfig>>(
2081 mut self,
2082 agents: I,
2083 ) -> Self {
2084 self.custom_agents = Some(agents.into_iter().collect());
2085 self
2086 }
2087
2088 pub fn with_default_agent(mut self, agent: DefaultAgentConfig) -> Self {
2090 self.default_agent = Some(agent);
2091 self
2092 }
2093
2094 pub fn with_agent(mut self, name: impl Into<String>) -> Self {
2096 self.agent = Some(name.into());
2097 self
2098 }
2099
2100 pub fn with_infinite_sessions(mut self, config: InfiniteSessionConfig) -> Self {
2102 self.infinite_sessions = Some(config);
2103 self
2104 }
2105
2106 pub fn with_provider(mut self, provider: ProviderConfig) -> Self {
2108 self.provider = Some(provider);
2109 self
2110 }
2111
2112 pub fn with_enable_session_telemetry(mut self, enable: bool) -> Self {
2116 self.enable_session_telemetry = Some(enable);
2117 self
2118 }
2119
2120 pub fn with_model_capabilities(
2122 mut self,
2123 capabilities: crate::generated::api_types::ModelCapabilitiesOverride,
2124 ) -> Self {
2125 self.model_capabilities = Some(capabilities);
2126 self
2127 }
2128
2129 pub fn with_config_dir(mut self, dir: impl Into<PathBuf>) -> Self {
2131 self.config_dir = Some(dir.into());
2132 self
2133 }
2134
2135 pub fn with_working_directory(mut self, dir: impl Into<PathBuf>) -> Self {
2137 self.working_directory = Some(dir.into());
2138 self
2139 }
2140
2141 pub fn with_github_token(mut self, token: impl Into<String>) -> Self {
2145 self.github_token = Some(token.into());
2146 self
2147 }
2148
2149 pub fn with_include_sub_agent_streaming_events(mut self, include: bool) -> Self {
2151 self.include_sub_agent_streaming_events = Some(include);
2152 self
2153 }
2154
2155 pub fn with_remote_session(
2157 mut self,
2158 mode: crate::generated::api_types::RemoteSessionMode,
2159 ) -> Self {
2160 self.remote_session = Some(mode);
2161 self
2162 }
2163
2164 pub fn with_disable_resume(mut self, disable: bool) -> Self {
2167 self.disable_resume = Some(disable);
2168 self
2169 }
2170
2171 pub fn with_continue_pending_work(mut self, continue_pending: bool) -> Self {
2177 self.continue_pending_work = Some(continue_pending);
2178 self
2179 }
2180}
2181
2182#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2188#[serde(rename_all = "camelCase")]
2189#[non_exhaustive]
2190pub struct SystemMessageConfig {
2191 #[serde(skip_serializing_if = "Option::is_none")]
2193 pub mode: Option<String>,
2194 #[serde(skip_serializing_if = "Option::is_none")]
2196 pub content: Option<String>,
2197 #[serde(skip_serializing_if = "Option::is_none")]
2199 pub sections: Option<HashMap<String, SectionOverride>>,
2200}
2201
2202impl SystemMessageConfig {
2203 pub fn new() -> Self {
2206 Self::default()
2207 }
2208
2209 pub fn with_mode(mut self, mode: impl Into<String>) -> Self {
2212 self.mode = Some(mode.into());
2213 self
2214 }
2215
2216 pub fn with_content(mut self, content: impl Into<String>) -> Self {
2219 self.content = Some(content.into());
2220 self
2221 }
2222
2223 pub fn with_sections(mut self, sections: HashMap<String, SectionOverride>) -> Self {
2225 self.sections = Some(sections);
2226 self
2227 }
2228}
2229
2230#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2236#[serde(rename_all = "camelCase")]
2237pub struct SectionOverride {
2238 #[serde(skip_serializing_if = "Option::is_none")]
2240 pub action: Option<String>,
2241 #[serde(skip_serializing_if = "Option::is_none")]
2243 pub content: Option<String>,
2244}
2245
2246#[derive(Debug, Clone, Serialize, Deserialize)]
2248#[serde(rename_all = "camelCase")]
2249pub struct CreateSessionResult {
2250 pub session_id: SessionId,
2252 #[serde(skip_serializing_if = "Option::is_none")]
2254 pub workspace_path: Option<PathBuf>,
2255 #[serde(default, alias = "remote_url")]
2257 pub remote_url: Option<String>,
2258 #[serde(skip_serializing_if = "Option::is_none")]
2260 pub capabilities: Option<SessionCapabilities>,
2261}
2262
2263#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
2265#[serde(rename_all = "lowercase")]
2266pub enum LogLevel {
2267 #[default]
2269 Info,
2270 Warning,
2272 Error,
2274}
2275
2276#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
2281#[serde(rename_all = "camelCase")]
2282pub struct LogOptions {
2283 #[serde(skip_serializing_if = "Option::is_none")]
2285 pub level: Option<LogLevel>,
2286 #[serde(skip_serializing_if = "Option::is_none")]
2289 pub ephemeral: Option<bool>,
2290}
2291
2292impl LogOptions {
2293 pub fn with_level(mut self, level: LogLevel) -> Self {
2295 self.level = Some(level);
2296 self
2297 }
2298
2299 pub fn with_ephemeral(mut self, ephemeral: bool) -> Self {
2301 self.ephemeral = Some(ephemeral);
2302 self
2303 }
2304}
2305
2306#[derive(Debug, Clone, Default)]
2310pub struct SetModelOptions {
2311 pub reasoning_effort: Option<String>,
2314 pub model_capabilities: Option<crate::generated::api_types::ModelCapabilitiesOverride>,
2318}
2319
2320impl SetModelOptions {
2321 pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
2323 self.reasoning_effort = Some(effort.into());
2324 self
2325 }
2326
2327 pub fn with_model_capabilities(
2329 mut self,
2330 caps: crate::generated::api_types::ModelCapabilitiesOverride,
2331 ) -> Self {
2332 self.model_capabilities = Some(caps);
2333 self
2334 }
2335}
2336
2337#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
2344#[serde(rename_all = "camelCase")]
2345pub struct PingResponse {
2346 #[serde(default)]
2348 pub message: String,
2349 #[serde(default)]
2351 pub timestamp: String,
2352 #[serde(skip_serializing_if = "Option::is_none")]
2354 pub protocol_version: Option<u32>,
2355}
2356
2357#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2359#[serde(rename_all = "camelCase")]
2360pub struct AttachmentLineRange {
2361 pub start: u32,
2363 pub end: u32,
2365}
2366
2367#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2369#[serde(rename_all = "camelCase")]
2370pub struct AttachmentSelectionPosition {
2371 pub line: u32,
2373 pub character: u32,
2375}
2376
2377#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2379#[serde(rename_all = "camelCase")]
2380pub struct AttachmentSelectionRange {
2381 pub start: AttachmentSelectionPosition,
2383 pub end: AttachmentSelectionPosition,
2385}
2386
2387#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2389#[serde(rename_all = "snake_case")]
2390#[non_exhaustive]
2391pub enum GitHubReferenceType {
2392 Issue,
2394 Pr,
2396 Discussion,
2398}
2399
2400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2402#[serde(
2403 tag = "type",
2404 rename_all = "camelCase",
2405 rename_all_fields = "camelCase"
2406)]
2407#[non_exhaustive]
2408pub enum Attachment {
2409 File {
2411 path: PathBuf,
2413 #[serde(skip_serializing_if = "Option::is_none")]
2415 display_name: Option<String>,
2416 #[serde(skip_serializing_if = "Option::is_none")]
2418 line_range: Option<AttachmentLineRange>,
2419 },
2420 Directory {
2422 path: PathBuf,
2424 #[serde(skip_serializing_if = "Option::is_none")]
2426 display_name: Option<String>,
2427 },
2428 Selection {
2430 file_path: PathBuf,
2432 text: String,
2434 #[serde(skip_serializing_if = "Option::is_none")]
2436 display_name: Option<String>,
2437 selection: AttachmentSelectionRange,
2439 },
2440 Blob {
2442 data: String,
2444 mime_type: String,
2446 #[serde(skip_serializing_if = "Option::is_none")]
2448 display_name: Option<String>,
2449 },
2450 #[serde(rename = "github_reference")]
2452 GitHubReference {
2453 number: u64,
2455 title: String,
2457 reference_type: GitHubReferenceType,
2459 state: String,
2461 url: String,
2463 },
2464}
2465
2466impl Attachment {
2467 pub fn display_name(&self) -> Option<&str> {
2469 match self {
2470 Self::File { display_name, .. }
2471 | Self::Directory { display_name, .. }
2472 | Self::Selection { display_name, .. }
2473 | Self::Blob { display_name, .. } => display_name.as_deref(),
2474 Self::GitHubReference { .. } => None,
2475 }
2476 }
2477
2478 pub fn label(&self) -> Option<String> {
2480 if let Some(display_name) = self
2481 .display_name()
2482 .map(str::trim)
2483 .filter(|name| !name.is_empty())
2484 {
2485 return Some(display_name.to_string());
2486 }
2487
2488 match self {
2489 Self::GitHubReference { number, title, .. } => Some(if title.trim().is_empty() {
2490 format!("#{}", number)
2491 } else {
2492 title.trim().to_string()
2493 }),
2494 _ => self.derived_display_name(),
2495 }
2496 }
2497
2498 pub fn ensure_display_name(&mut self) {
2500 if self
2501 .display_name()
2502 .map(str::trim)
2503 .is_some_and(|name| !name.is_empty())
2504 {
2505 return;
2506 }
2507
2508 let Some(derived_display_name) = self.derived_display_name() else {
2509 return;
2510 };
2511
2512 match self {
2513 Self::File { display_name, .. }
2514 | Self::Directory { display_name, .. }
2515 | Self::Selection { display_name, .. }
2516 | Self::Blob { display_name, .. } => *display_name = Some(derived_display_name),
2517 Self::GitHubReference { .. } => {}
2518 }
2519 }
2520
2521 fn derived_display_name(&self) -> Option<String> {
2522 match self {
2523 Self::File { path, .. } | Self::Directory { path, .. } => {
2524 Some(attachment_name_from_path(path))
2525 }
2526 Self::Selection { file_path, .. } => Some(attachment_name_from_path(file_path)),
2527 Self::Blob { .. } => Some("attachment".to_string()),
2528 Self::GitHubReference { .. } => None,
2529 }
2530 }
2531}
2532
2533fn attachment_name_from_path(path: &Path) -> String {
2534 path.file_name()
2535 .map(|name| name.to_string_lossy().into_owned())
2536 .filter(|name| !name.is_empty())
2537 .unwrap_or_else(|| {
2538 let full = path.to_string_lossy();
2539 if full.is_empty() {
2540 "attachment".to_string()
2541 } else {
2542 full.into_owned()
2543 }
2544 })
2545}
2546
2547pub fn ensure_attachment_display_names(attachments: &mut [Attachment]) {
2549 for attachment in attachments {
2550 attachment.ensure_display_name();
2551 }
2552}
2553
2554#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2559#[serde(rename_all = "lowercase")]
2560#[non_exhaustive]
2561pub enum DeliveryMode {
2562 Enqueue,
2564 Immediate,
2566}
2567
2568#[derive(Debug, Clone)]
2597#[non_exhaustive]
2598pub struct MessageOptions {
2599 pub prompt: String,
2601 pub mode: Option<DeliveryMode>,
2607 pub attachments: Option<Vec<Attachment>>,
2609 pub wait_timeout: Option<Duration>,
2612 pub request_headers: Option<HashMap<String, String>>,
2616 pub traceparent: Option<String>,
2623 pub tracestate: Option<String>,
2627}
2628
2629impl MessageOptions {
2630 pub fn new(prompt: impl Into<String>) -> Self {
2632 Self {
2633 prompt: prompt.into(),
2634 mode: None,
2635 attachments: None,
2636 wait_timeout: None,
2637 request_headers: None,
2638 traceparent: None,
2639 tracestate: None,
2640 }
2641 }
2642
2643 pub fn with_mode(mut self, mode: DeliveryMode) -> Self {
2649 self.mode = Some(mode);
2650 self
2651 }
2652
2653 pub fn with_attachments(mut self, attachments: Vec<Attachment>) -> Self {
2655 self.attachments = Some(attachments);
2656 self
2657 }
2658
2659 pub fn with_wait_timeout(mut self, timeout: Duration) -> Self {
2661 self.wait_timeout = Some(timeout);
2662 self
2663 }
2664
2665 pub fn with_request_headers(mut self, headers: HashMap<String, String>) -> Self {
2667 self.request_headers = Some(headers);
2668 self
2669 }
2670
2671 pub fn with_trace_context(mut self, ctx: TraceContext) -> Self {
2676 self.traceparent = ctx.traceparent;
2677 self.tracestate = ctx.tracestate;
2678 self
2679 }
2680
2681 pub fn with_traceparent(mut self, traceparent: impl Into<String>) -> Self {
2683 self.traceparent = Some(traceparent.into());
2684 self
2685 }
2686
2687 pub fn with_tracestate(mut self, tracestate: impl Into<String>) -> Self {
2689 self.tracestate = Some(tracestate.into());
2690 self
2691 }
2692}
2693
2694impl From<&str> for MessageOptions {
2695 fn from(prompt: &str) -> Self {
2696 Self::new(prompt)
2697 }
2698}
2699
2700impl From<String> for MessageOptions {
2701 fn from(prompt: String) -> Self {
2702 Self::new(prompt)
2703 }
2704}
2705
2706impl From<&String> for MessageOptions {
2707 fn from(prompt: &String) -> Self {
2708 Self::new(prompt.clone())
2709 }
2710}
2711
2712#[derive(Debug, Clone, Serialize, Deserialize)]
2714#[serde(rename_all = "camelCase")]
2715#[non_exhaustive]
2716pub struct GetStatusResponse {
2717 pub version: String,
2719 pub protocol_version: u32,
2721}
2722
2723#[derive(Debug, Clone, Serialize, Deserialize)]
2725#[serde(rename_all = "camelCase")]
2726#[non_exhaustive]
2727pub struct GetAuthStatusResponse {
2728 pub is_authenticated: bool,
2730 #[serde(skip_serializing_if = "Option::is_none")]
2733 pub auth_type: Option<String>,
2734 #[serde(skip_serializing_if = "Option::is_none")]
2736 pub host: Option<String>,
2737 #[serde(skip_serializing_if = "Option::is_none")]
2739 pub login: Option<String>,
2740 #[serde(skip_serializing_if = "Option::is_none")]
2742 pub status_message: Option<String>,
2743}
2744
2745#[derive(Debug, Clone, Serialize, Deserialize)]
2749#[serde(rename_all = "camelCase")]
2750pub struct SessionEventNotification {
2751 pub session_id: SessionId,
2753 pub event: SessionEvent,
2755}
2756
2757#[derive(Debug, Clone, Serialize, Deserialize)]
2764#[serde(rename_all = "camelCase")]
2765pub struct SessionEvent {
2766 pub id: String,
2768 pub timestamp: String,
2770 pub parent_id: Option<String>,
2772 #[serde(skip_serializing_if = "Option::is_none")]
2774 pub ephemeral: Option<bool>,
2775 #[serde(skip_serializing_if = "Option::is_none")]
2778 pub agent_id: Option<String>,
2779 #[serde(skip_serializing_if = "Option::is_none")]
2781 pub debug_cli_received_at_ms: Option<i64>,
2782 #[serde(skip_serializing_if = "Option::is_none")]
2784 pub debug_ws_forwarded_at_ms: Option<i64>,
2785 #[serde(rename = "type")]
2787 pub event_type: String,
2788 pub data: Value,
2790}
2791
2792impl SessionEvent {
2793 pub fn parsed_type(&self) -> crate::generated::SessionEventType {
2798 use serde::de::IntoDeserializer;
2799 let deserializer: serde::de::value::StrDeserializer<'_, serde::de::value::Error> =
2800 self.event_type.as_str().into_deserializer();
2801 crate::generated::SessionEventType::deserialize(deserializer)
2802 .unwrap_or(crate::generated::SessionEventType::Unknown)
2803 }
2804
2805 pub fn typed_data<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
2811 serde_json::from_value(self.data.clone()).ok()
2812 }
2813
2814 pub fn is_transient_error(&self) -> bool {
2818 self.event_type == "session.error"
2819 && self.data.get("errorType").and_then(|v| v.as_str()) == Some("model_call")
2820 }
2821}
2822
2823#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2828#[serde(rename_all = "camelCase")]
2829#[non_exhaustive]
2830pub struct ToolInvocation {
2831 pub session_id: SessionId,
2833 pub tool_call_id: String,
2835 pub tool_name: String,
2837 pub arguments: Value,
2839 #[serde(default, skip_serializing_if = "Option::is_none")]
2844 pub traceparent: Option<String>,
2845 #[serde(default, skip_serializing_if = "Option::is_none")]
2848 pub tracestate: Option<String>,
2849}
2850
2851impl ToolInvocation {
2852 pub fn params<P: serde::de::DeserializeOwned>(&self) -> Result<P, crate::Error> {
2873 serde_json::from_value(self.arguments.clone()).map_err(crate::Error::from)
2874 }
2875
2876 pub fn trace_context(&self) -> TraceContext {
2879 TraceContext {
2880 traceparent: self.traceparent.clone(),
2881 tracestate: self.tracestate.clone(),
2882 }
2883 }
2884}
2885
2886#[derive(Debug, Clone, Serialize, Deserialize)]
2888#[serde(rename_all = "camelCase")]
2889pub struct ToolBinaryResult {
2890 pub data: String,
2892 pub mime_type: String,
2894 pub r#type: String,
2896 #[serde(default, skip_serializing_if = "Option::is_none")]
2898 pub description: Option<String>,
2899}
2900
2901#[derive(Debug, Clone, Serialize, Deserialize)]
2903#[serde(rename_all = "camelCase")]
2904pub struct ToolResultExpanded {
2905 pub text_result_for_llm: String,
2907 pub result_type: String,
2909 #[serde(default, skip_serializing_if = "Option::is_none")]
2911 pub binary_results_for_llm: Option<Vec<ToolBinaryResult>>,
2912 #[serde(skip_serializing_if = "Option::is_none")]
2914 pub session_log: Option<String>,
2915 #[serde(skip_serializing_if = "Option::is_none")]
2917 pub error: Option<String>,
2918 #[serde(default, skip_serializing_if = "Option::is_none")]
2920 pub tool_telemetry: Option<HashMap<String, Value>>,
2921}
2922
2923#[derive(Debug, Clone, Serialize, Deserialize)]
2925#[serde(untagged)]
2926#[non_exhaustive]
2927pub enum ToolResult {
2928 Text(String),
2930 Expanded(ToolResultExpanded),
2932}
2933
2934#[derive(Debug, Clone, Serialize, Deserialize)]
2936#[serde(rename_all = "camelCase")]
2937pub struct ToolResultResponse {
2938 pub result: ToolResult,
2940}
2941
2942#[derive(Debug, Clone, Serialize, Deserialize)]
2944#[serde(rename_all = "camelCase")]
2945pub struct SessionMetadata {
2946 pub session_id: SessionId,
2948 pub start_time: String,
2950 pub modified_time: String,
2952 #[serde(skip_serializing_if = "Option::is_none")]
2954 pub summary: Option<String>,
2955 pub is_remote: bool,
2957}
2958
2959#[derive(Debug, Clone, Serialize, Deserialize)]
2961#[serde(rename_all = "camelCase")]
2962pub struct ListSessionsResponse {
2963 pub sessions: Vec<SessionMetadata>,
2965}
2966
2967#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2971#[serde(rename_all = "camelCase")]
2972pub struct SessionListFilter {
2973 #[serde(default, skip_serializing_if = "Option::is_none")]
2975 pub cwd: Option<String>,
2976 #[serde(default, skip_serializing_if = "Option::is_none")]
2978 pub git_root: Option<String>,
2979 #[serde(default, skip_serializing_if = "Option::is_none")]
2981 pub repository: Option<String>,
2982 #[serde(default, skip_serializing_if = "Option::is_none")]
2984 pub branch: Option<String>,
2985}
2986
2987#[derive(Debug, Clone, Serialize, Deserialize)]
2989#[serde(rename_all = "camelCase")]
2990pub struct GetSessionMetadataResponse {
2991 #[serde(skip_serializing_if = "Option::is_none")]
2993 pub session: Option<SessionMetadata>,
2994}
2995
2996#[derive(Debug, Clone, Serialize, Deserialize)]
2998#[serde(rename_all = "camelCase")]
2999pub struct GetLastSessionIdResponse {
3000 #[serde(skip_serializing_if = "Option::is_none")]
3002 pub session_id: Option<SessionId>,
3003}
3004
3005#[derive(Debug, Clone, Serialize, Deserialize)]
3007#[serde(rename_all = "camelCase")]
3008pub struct GetForegroundSessionResponse {
3009 #[serde(skip_serializing_if = "Option::is_none")]
3011 pub session_id: Option<SessionId>,
3012}
3013
3014#[derive(Debug, Clone, Serialize, Deserialize)]
3016#[serde(rename_all = "camelCase")]
3017pub struct GetMessagesResponse {
3018 pub events: Vec<SessionEvent>,
3020}
3021
3022#[derive(Debug, Clone, Serialize, Deserialize)]
3024#[serde(rename_all = "camelCase")]
3025pub struct ElicitationResult {
3026 pub action: String,
3028 #[serde(skip_serializing_if = "Option::is_none")]
3030 pub content: Option<Value>,
3031}
3032
3033#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3039#[serde(rename_all = "camelCase")]
3040#[non_exhaustive]
3041pub enum ElicitationMode {
3042 Form,
3044 Url,
3046 #[serde(other)]
3048 Unknown,
3049}
3050
3051#[derive(Debug, Clone, Serialize, Deserialize)]
3057#[serde(rename_all = "camelCase")]
3058pub struct ElicitationRequest {
3059 pub message: String,
3061 #[serde(skip_serializing_if = "Option::is_none")]
3063 pub requested_schema: Option<Value>,
3064 #[serde(skip_serializing_if = "Option::is_none")]
3066 pub mode: Option<ElicitationMode>,
3067 #[serde(skip_serializing_if = "Option::is_none")]
3069 pub elicitation_source: Option<String>,
3070 #[serde(skip_serializing_if = "Option::is_none")]
3072 pub url: Option<String>,
3073}
3074
3075#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3080#[serde(rename_all = "camelCase")]
3081pub struct SessionCapabilities {
3082 #[serde(skip_serializing_if = "Option::is_none")]
3084 pub ui: Option<UiCapabilities>,
3085}
3086
3087#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3089#[serde(rename_all = "camelCase")]
3090pub struct UiCapabilities {
3091 #[serde(skip_serializing_if = "Option::is_none")]
3093 pub elicitation: Option<bool>,
3094}
3095
3096#[derive(Debug, Clone, Default)]
3098pub struct InputOptions<'a> {
3099 pub title: Option<&'a str>,
3101 pub description: Option<&'a str>,
3103 pub min_length: Option<u64>,
3105 pub max_length: Option<u64>,
3107 pub format: Option<InputFormat>,
3109 pub default: Option<&'a str>,
3111}
3112
3113#[derive(Debug, Clone, Copy)]
3115#[non_exhaustive]
3116pub enum InputFormat {
3117 Email,
3119 Uri,
3121 Date,
3123 DateTime,
3125}
3126
3127impl InputFormat {
3128 pub fn as_str(&self) -> &'static str {
3130 match self {
3131 Self::Email => "email",
3132 Self::Uri => "uri",
3133 Self::Date => "date",
3134 Self::DateTime => "date-time",
3135 }
3136 }
3137}
3138
3139pub use crate::generated::api_types::{
3144 Model, ModelBilling, ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision,
3145 ModelCapabilitiesSupports, ModelList, ModelPolicy,
3146};
3147
3148#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3154#[serde(rename_all = "kebab-case")]
3155#[non_exhaustive]
3156pub enum PermissionRequestKind {
3157 Shell,
3159 Write,
3161 Read,
3163 Url,
3165 Mcp,
3167 CustomTool,
3169 Memory,
3171 Hook,
3173 #[serde(other)]
3176 Unknown,
3177}
3178
3179#[derive(Debug, Clone, Default, Serialize, Deserialize)]
3186#[serde(rename_all = "camelCase")]
3187pub struct PermissionRequestData {
3188 #[serde(default, skip_serializing_if = "Option::is_none")]
3192 pub kind: Option<PermissionRequestKind>,
3193 #[serde(default, skip_serializing_if = "Option::is_none")]
3196 pub tool_call_id: Option<String>,
3197 #[serde(flatten)]
3200 pub extra: Value,
3201}
3202
3203#[derive(Debug, Clone, Serialize, Deserialize)]
3205#[serde(rename_all = "camelCase")]
3206pub struct ExitPlanModeData {
3207 #[serde(default)]
3209 pub summary: String,
3210 #[serde(default, skip_serializing_if = "Option::is_none")]
3212 pub plan_content: Option<String>,
3213 #[serde(default)]
3215 pub actions: Vec<String>,
3216 #[serde(default = "default_recommended_action")]
3218 pub recommended_action: String,
3219}
3220
3221fn default_recommended_action() -> String {
3222 "autopilot".to_string()
3223}
3224
3225impl Default for ExitPlanModeData {
3226 fn default() -> Self {
3227 Self {
3228 summary: String::new(),
3229 plan_content: None,
3230 actions: Vec::new(),
3231 recommended_action: default_recommended_action(),
3232 }
3233 }
3234}
3235
3236#[cfg(test)]
3237mod tests {
3238 use std::path::PathBuf;
3239
3240 use serde_json::json;
3241
3242 use super::{
3243 Attachment, AttachmentLineRange, AttachmentSelectionPosition, AttachmentSelectionRange,
3244 ConnectionState, CustomAgentConfig, DeliveryMode, GitHubReferenceType,
3245 InfiniteSessionConfig, ProviderConfig, ResumeSessionConfig, SessionConfig, SessionEvent,
3246 SessionId, SystemMessageConfig, Tool, ToolBinaryResult, ToolResult, ToolResultExpanded,
3247 ToolResultResponse, ensure_attachment_display_names,
3248 };
3249 use crate::generated::session_events::TypedSessionEvent;
3250
3251 #[test]
3252 fn tool_builder_composes() {
3253 let tool = Tool::new("greet")
3254 .with_description("Say hello")
3255 .with_namespaced_name("hello/greet")
3256 .with_instructions("Pass the user's name")
3257 .with_parameters(json!({
3258 "type": "object",
3259 "properties": { "name": { "type": "string" } },
3260 "required": ["name"]
3261 }))
3262 .with_overrides_built_in_tool(true)
3263 .with_skip_permission(true);
3264 assert_eq!(tool.name, "greet");
3265 assert_eq!(tool.description, "Say hello");
3266 assert_eq!(tool.namespaced_name.as_deref(), Some("hello/greet"));
3267 assert_eq!(tool.instructions.as_deref(), Some("Pass the user's name"));
3268 assert_eq!(tool.parameters.get("type").unwrap(), &json!("object"));
3269 assert!(tool.overrides_built_in_tool);
3270 assert!(tool.skip_permission);
3271 }
3272
3273 #[test]
3274 fn custom_agent_config_builder_with_model() {
3275 let agent = CustomAgentConfig::new("my-agent", "You are helpful.")
3276 .with_model("claude-haiku-4.5")
3277 .with_display_name("My Agent");
3278 assert_eq!(agent.name, "my-agent");
3279 assert_eq!(agent.model.as_deref(), Some("claude-haiku-4.5"));
3280 assert_eq!(agent.display_name.as_deref(), Some("My Agent"));
3281 }
3282
3283 #[test]
3284 fn custom_agent_config_serializes_model() {
3285 let agent = CustomAgentConfig::new("model-agent", "prompt").with_model("claude-haiku-4.5");
3286 let wire = serde_json::to_value(&agent).unwrap();
3287 assert_eq!(wire["model"], "claude-haiku-4.5");
3288 assert_eq!(wire["name"], "model-agent");
3289 }
3290
3291 #[test]
3292 fn custom_agent_config_omits_model_when_none() {
3293 let agent = CustomAgentConfig::new("no-model-agent", "prompt");
3294 let wire = serde_json::to_value(&agent).unwrap();
3295 assert!(wire.get("model").is_none());
3296 }
3297
3298 #[test]
3299 fn tool_with_parameters_handles_non_object_value() {
3300 let tool = Tool::new("noop").with_parameters(json!(null));
3301 assert!(tool.parameters.is_empty());
3302 }
3303
3304 #[test]
3305 fn tool_result_expanded_serializes_binary_results_for_llm() {
3306 let response = ToolResultResponse {
3307 result: ToolResult::Expanded(ToolResultExpanded {
3308 text_result_for_llm: "rendered chart".to_string(),
3309 result_type: "success".to_string(),
3310 binary_results_for_llm: Some(vec![ToolBinaryResult {
3311 data: "aW1n".to_string(),
3312 mime_type: "image/png".to_string(),
3313 r#type: "image".to_string(),
3314 description: Some("chart preview".to_string()),
3315 }]),
3316 session_log: None,
3317 error: None,
3318 tool_telemetry: None,
3319 }),
3320 };
3321
3322 let wire = serde_json::to_value(&response).unwrap();
3323
3324 assert_eq!(
3325 wire,
3326 json!({
3327 "result": {
3328 "textResultForLlm": "rendered chart",
3329 "resultType": "success",
3330 "binaryResultsForLlm": [
3331 {
3332 "data": "aW1n",
3333 "mimeType": "image/png",
3334 "type": "image",
3335 "description": "chart preview"
3336 }
3337 ]
3338 }
3339 })
3340 );
3341 }
3342
3343 #[test]
3344 fn tool_result_expanded_omits_binary_results_for_llm_when_none() {
3345 let response = ToolResultResponse {
3346 result: ToolResult::Expanded(ToolResultExpanded {
3347 text_result_for_llm: "ok".to_string(),
3348 result_type: "success".to_string(),
3349 binary_results_for_llm: None,
3350 session_log: None,
3351 error: None,
3352 tool_telemetry: None,
3353 }),
3354 };
3355
3356 let wire = serde_json::to_value(&response).unwrap();
3357
3358 assert_eq!(wire["result"]["textResultForLlm"], "ok");
3359 assert!(wire["result"].get("binaryResultsForLlm").is_none());
3360 }
3361
3362 #[test]
3363 fn session_config_default_enables_permission_flow_flags() {
3364 let cfg = SessionConfig::default();
3365 assert_eq!(cfg.request_user_input, Some(true));
3366 assert_eq!(cfg.request_permission, Some(true));
3367 assert_eq!(cfg.request_elicitation, Some(true));
3368 assert_eq!(cfg.request_exit_plan_mode, Some(true));
3369 assert_eq!(cfg.request_auto_mode_switch, Some(true));
3370 }
3371
3372 #[test]
3373 fn resume_session_config_new_enables_permission_flow_flags() {
3374 let cfg = ResumeSessionConfig::new(SessionId::from("test-id"));
3375 assert_eq!(cfg.request_user_input, Some(true));
3376 assert_eq!(cfg.request_permission, Some(true));
3377 assert_eq!(cfg.request_elicitation, Some(true));
3378 assert_eq!(cfg.request_exit_plan_mode, Some(true));
3379 assert_eq!(cfg.request_auto_mode_switch, Some(true));
3380 }
3381
3382 #[test]
3383 fn session_config_builder_composes() {
3384 use std::collections::HashMap;
3385
3386 let cfg = SessionConfig::default()
3387 .with_session_id(SessionId::from("sess-1"))
3388 .with_model("claude-sonnet-4")
3389 .with_client_name("test-app")
3390 .with_reasoning_effort("medium")
3391 .with_streaming(true)
3392 .with_tools([Tool::new("greet")])
3393 .with_available_tools(["bash", "view"])
3394 .with_excluded_tools(["dangerous"])
3395 .with_mcp_servers(HashMap::new())
3396 .with_enable_config_discovery(true)
3397 .with_request_user_input(false)
3398 .with_request_exit_plan_mode(false)
3399 .with_request_auto_mode_switch(false)
3400 .with_skill_directories([PathBuf::from("/tmp/skills")])
3401 .with_disabled_skills(["broken-skill"])
3402 .with_agent("researcher")
3403 .with_config_dir(PathBuf::from("/tmp/config"))
3404 .with_working_directory(PathBuf::from("/tmp/work"))
3405 .with_github_token("ghp_test")
3406 .with_enable_session_telemetry(false)
3407 .with_include_sub_agent_streaming_events(false);
3408
3409 assert_eq!(cfg.session_id.as_ref().map(|s| s.as_str()), Some("sess-1"));
3410 assert_eq!(cfg.model.as_deref(), Some("claude-sonnet-4"));
3411 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
3412 assert_eq!(cfg.reasoning_effort.as_deref(), Some("medium"));
3413 assert_eq!(cfg.streaming, Some(true));
3414 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
3415 assert_eq!(
3416 cfg.available_tools.as_deref(),
3417 Some(&["bash".to_string(), "view".to_string()][..])
3418 );
3419 assert_eq!(
3420 cfg.excluded_tools.as_deref(),
3421 Some(&["dangerous".to_string()][..])
3422 );
3423 assert!(cfg.mcp_servers.is_some());
3424 assert_eq!(cfg.enable_config_discovery, Some(true));
3425 assert_eq!(cfg.request_user_input, Some(false)); assert_eq!(cfg.request_permission, Some(true)); assert_eq!(cfg.request_exit_plan_mode, Some(false));
3428 assert_eq!(cfg.request_auto_mode_switch, Some(false));
3429 assert_eq!(
3430 cfg.skill_directories.as_deref(),
3431 Some(&[PathBuf::from("/tmp/skills")][..])
3432 );
3433 assert_eq!(
3434 cfg.disabled_skills.as_deref(),
3435 Some(&["broken-skill".to_string()][..])
3436 );
3437 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
3438 assert_eq!(cfg.config_dir, Some(PathBuf::from("/tmp/config")));
3439 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
3440 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
3441 assert_eq!(cfg.enable_session_telemetry, Some(false));
3442 assert_eq!(cfg.include_sub_agent_streaming_events, Some(false));
3443 }
3444
3445 #[test]
3446 fn resume_session_config_builder_composes() {
3447 use std::collections::HashMap;
3448
3449 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"))
3450 .with_client_name("test-app")
3451 .with_streaming(true)
3452 .with_tools([Tool::new("greet")])
3453 .with_available_tools(["bash", "view"])
3454 .with_excluded_tools(["dangerous"])
3455 .with_mcp_servers(HashMap::new())
3456 .with_enable_config_discovery(true)
3457 .with_request_user_input(false)
3458 .with_request_exit_plan_mode(false)
3459 .with_request_auto_mode_switch(false)
3460 .with_skill_directories([PathBuf::from("/tmp/skills")])
3461 .with_disabled_skills(["broken-skill"])
3462 .with_agent("researcher")
3463 .with_config_dir(PathBuf::from("/tmp/config"))
3464 .with_working_directory(PathBuf::from("/tmp/work"))
3465 .with_github_token("ghp_test")
3466 .with_enable_session_telemetry(false)
3467 .with_include_sub_agent_streaming_events(true)
3468 .with_disable_resume(true)
3469 .with_continue_pending_work(true);
3470
3471 assert_eq!(cfg.session_id.as_str(), "sess-2");
3472 assert_eq!(cfg.client_name.as_deref(), Some("test-app"));
3473 assert_eq!(cfg.streaming, Some(true));
3474 assert_eq!(cfg.tools.as_ref().map(|t| t.len()), Some(1));
3475 assert_eq!(
3476 cfg.available_tools.as_deref(),
3477 Some(&["bash".to_string(), "view".to_string()][..])
3478 );
3479 assert_eq!(
3480 cfg.excluded_tools.as_deref(),
3481 Some(&["dangerous".to_string()][..])
3482 );
3483 assert!(cfg.mcp_servers.is_some());
3484 assert_eq!(cfg.enable_config_discovery, Some(true));
3485 assert_eq!(cfg.request_user_input, Some(false)); assert_eq!(cfg.request_permission, Some(true)); assert_eq!(cfg.request_exit_plan_mode, Some(false));
3488 assert_eq!(cfg.request_auto_mode_switch, Some(false));
3489 assert_eq!(
3490 cfg.skill_directories.as_deref(),
3491 Some(&[PathBuf::from("/tmp/skills")][..])
3492 );
3493 assert_eq!(
3494 cfg.disabled_skills.as_deref(),
3495 Some(&["broken-skill".to_string()][..])
3496 );
3497 assert_eq!(cfg.agent.as_deref(), Some("researcher"));
3498 assert_eq!(cfg.config_dir, Some(PathBuf::from("/tmp/config")));
3499 assert_eq!(cfg.working_directory, Some(PathBuf::from("/tmp/work")));
3500 assert_eq!(cfg.github_token.as_deref(), Some("ghp_test"));
3501 assert_eq!(cfg.enable_session_telemetry, Some(false));
3502 assert_eq!(cfg.include_sub_agent_streaming_events, Some(true));
3503 assert_eq!(cfg.disable_resume, Some(true));
3504 assert_eq!(cfg.continue_pending_work, Some(true));
3505 }
3506
3507 #[test]
3511 fn resume_session_config_serializes_continue_pending_work_to_camel_case() {
3512 let cfg =
3513 ResumeSessionConfig::new(SessionId::from("sess-1")).with_continue_pending_work(true);
3514 let wire = serde_json::to_value(&cfg).unwrap();
3515 assert_eq!(wire["continuePendingWork"], true);
3516
3517 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"));
3519 let wire = serde_json::to_value(&cfg).unwrap();
3520 assert!(wire.get("continuePendingWork").is_none());
3521 }
3522
3523 #[test]
3526 fn session_config_serializes_instruction_directories_to_camel_case() {
3527 let cfg =
3528 SessionConfig::default().with_instruction_directories([PathBuf::from("/tmp/instr")]);
3529 let wire = serde_json::to_value(&cfg).unwrap();
3530 assert_eq!(
3531 wire["instructionDirectories"],
3532 serde_json::json!(["/tmp/instr"])
3533 );
3534
3535 let cfg = SessionConfig::default();
3537 let wire = serde_json::to_value(&cfg).unwrap();
3538 assert!(wire.get("instructionDirectories").is_none());
3539 }
3540
3541 #[test]
3544 fn resume_session_config_serializes_instruction_directories_to_camel_case() {
3545 let cfg = ResumeSessionConfig::new(SessionId::from("sess-1"))
3546 .with_instruction_directories([PathBuf::from("/tmp/instr")]);
3547 let wire = serde_json::to_value(&cfg).unwrap();
3548 assert_eq!(
3549 wire["instructionDirectories"],
3550 serde_json::json!(["/tmp/instr"])
3551 );
3552
3553 let cfg = ResumeSessionConfig::new(SessionId::from("sess-2"));
3554 let wire = serde_json::to_value(&cfg).unwrap();
3555 assert!(wire.get("instructionDirectories").is_none());
3556 }
3557
3558 #[test]
3559 fn custom_agent_config_builder_composes() {
3560 use std::collections::HashMap;
3561
3562 let cfg = CustomAgentConfig::new("researcher", "You are a research assistant.")
3563 .with_display_name("Research Assistant")
3564 .with_description("Investigates technical questions.")
3565 .with_tools(["bash", "view"])
3566 .with_mcp_servers(HashMap::new())
3567 .with_infer(true)
3568 .with_skills(["rust-coding-skill"]);
3569
3570 assert_eq!(cfg.name, "researcher");
3571 assert_eq!(cfg.prompt, "You are a research assistant.");
3572 assert_eq!(cfg.display_name.as_deref(), Some("Research Assistant"));
3573 assert_eq!(
3574 cfg.description.as_deref(),
3575 Some("Investigates technical questions.")
3576 );
3577 assert_eq!(
3578 cfg.tools.as_deref(),
3579 Some(&["bash".to_string(), "view".to_string()][..])
3580 );
3581 assert!(cfg.mcp_servers.is_some());
3582 assert_eq!(cfg.infer, Some(true));
3583 assert_eq!(
3584 cfg.skills.as_deref(),
3585 Some(&["rust-coding-skill".to_string()][..])
3586 );
3587 }
3588
3589 #[test]
3590 fn infinite_session_config_builder_composes() {
3591 let cfg = InfiniteSessionConfig::new()
3592 .with_enabled(true)
3593 .with_background_compaction_threshold(0.75)
3594 .with_buffer_exhaustion_threshold(0.92);
3595
3596 assert_eq!(cfg.enabled, Some(true));
3597 assert_eq!(cfg.background_compaction_threshold, Some(0.75));
3598 assert_eq!(cfg.buffer_exhaustion_threshold, Some(0.92));
3599 }
3600
3601 #[test]
3602 fn provider_config_builder_composes() {
3603 use std::collections::HashMap;
3604
3605 let mut headers = HashMap::new();
3606 headers.insert("X-Custom".to_string(), "value".to_string());
3607
3608 let cfg = ProviderConfig::new("https://api.example.com")
3609 .with_provider_type("openai")
3610 .with_wire_api("completions")
3611 .with_api_key("sk-test")
3612 .with_bearer_token("bearer-test")
3613 .with_headers(headers)
3614 .with_model_id("gpt-4")
3615 .with_wire_model("azure-gpt-4-deployment")
3616 .with_max_prompt_tokens(8192)
3617 .with_max_output_tokens(2048);
3618
3619 assert_eq!(cfg.base_url, "https://api.example.com");
3620 assert_eq!(cfg.provider_type.as_deref(), Some("openai"));
3621 assert_eq!(cfg.wire_api.as_deref(), Some("completions"));
3622 assert_eq!(cfg.api_key.as_deref(), Some("sk-test"));
3623 assert_eq!(cfg.bearer_token.as_deref(), Some("bearer-test"));
3624 assert_eq!(
3625 cfg.headers
3626 .as_ref()
3627 .and_then(|h| h.get("X-Custom"))
3628 .map(String::as_str),
3629 Some("value"),
3630 );
3631 assert_eq!(cfg.model_id.as_deref(), Some("gpt-4"));
3632 assert_eq!(cfg.wire_model.as_deref(), Some("azure-gpt-4-deployment"));
3633 assert_eq!(cfg.max_prompt_tokens, Some(8192));
3634 assert_eq!(cfg.max_output_tokens, Some(2048));
3635
3636 let wire = serde_json::to_value(&cfg).unwrap();
3638 assert_eq!(wire["modelId"], "gpt-4");
3639 assert_eq!(wire["wireModel"], "azure-gpt-4-deployment");
3640 assert_eq!(wire["maxPromptTokens"], 8192);
3641 assert_eq!(wire["maxOutputTokens"], 2048);
3642
3643 let unset = ProviderConfig::new("https://api.example.com");
3644 let wire_unset = serde_json::to_value(&unset).unwrap();
3645 assert!(wire_unset.get("modelId").is_none());
3646 assert!(wire_unset.get("wireModel").is_none());
3647 assert!(wire_unset.get("maxPromptTokens").is_none());
3648 assert!(wire_unset.get("maxOutputTokens").is_none());
3649 }
3650
3651 #[test]
3652 fn system_message_config_builder_composes() {
3653 use std::collections::HashMap;
3654
3655 let cfg = SystemMessageConfig::new()
3656 .with_mode("replace")
3657 .with_content("Custom system message.")
3658 .with_sections(HashMap::new());
3659
3660 assert_eq!(cfg.mode.as_deref(), Some("replace"));
3661 assert_eq!(cfg.content.as_deref(), Some("Custom system message."));
3662 assert!(cfg.sections.is_some());
3663 }
3664
3665 #[test]
3666 fn delivery_mode_serializes_to_kebab_case_strings() {
3667 assert_eq!(
3668 serde_json::to_string(&DeliveryMode::Enqueue).unwrap(),
3669 "\"enqueue\""
3670 );
3671 assert_eq!(
3672 serde_json::to_string(&DeliveryMode::Immediate).unwrap(),
3673 "\"immediate\""
3674 );
3675 let parsed: DeliveryMode = serde_json::from_str("\"immediate\"").unwrap();
3676 assert_eq!(parsed, DeliveryMode::Immediate);
3677 }
3678
3679 #[test]
3680 fn connection_state_error_serializes_to_match_go() {
3681 let json = serde_json::to_string(&ConnectionState::Error).unwrap();
3682 assert_eq!(json, "\"error\"");
3683 let parsed: ConnectionState = serde_json::from_str("\"error\"").unwrap();
3684 assert_eq!(parsed, ConnectionState::Error);
3685 }
3686
3687 #[test]
3693 fn session_event_round_trips_agent_id_on_envelope() {
3694 let wire = json!({
3695 "id": "evt-1",
3696 "timestamp": "2026-04-30T12:00:00Z",
3697 "parentId": null,
3698 "agentId": "sub-agent-42",
3699 "type": "assistant.message",
3700 "data": { "message": "hi" }
3701 });
3702
3703 let event: SessionEvent = serde_json::from_value(wire.clone()).unwrap();
3704 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
3705
3706 let roundtripped = serde_json::to_value(&event).unwrap();
3708 assert_eq!(roundtripped["agentId"], "sub-agent-42");
3709
3710 let main_agent_event: SessionEvent = serde_json::from_value(json!({
3712 "id": "evt-2",
3713 "timestamp": "2026-04-30T12:00:01Z",
3714 "parentId": null,
3715 "type": "session.idle",
3716 "data": {}
3717 }))
3718 .unwrap();
3719 assert!(main_agent_event.agent_id.is_none());
3720 let roundtripped = serde_json::to_value(&main_agent_event).unwrap();
3721 assert!(roundtripped.get("agentId").is_none());
3722 }
3723
3724 #[test]
3726 fn typed_session_event_round_trips_agent_id_on_envelope() {
3727 let wire = json!({
3728 "id": "evt-1",
3729 "timestamp": "2026-04-30T12:00:00Z",
3730 "parentId": null,
3731 "agentId": "sub-agent-42",
3732 "type": "session.idle",
3733 "data": {}
3734 });
3735
3736 let event: TypedSessionEvent = serde_json::from_value(wire).unwrap();
3737 assert_eq!(event.agent_id.as_deref(), Some("sub-agent-42"));
3738
3739 let roundtripped = serde_json::to_value(&event).unwrap();
3740 assert_eq!(roundtripped["agentId"], "sub-agent-42");
3741 }
3742
3743 #[test]
3744 fn connection_state_other_variants_serialize_as_lowercase() {
3745 assert_eq!(
3746 serde_json::to_string(&ConnectionState::Disconnected).unwrap(),
3747 "\"disconnected\""
3748 );
3749 assert_eq!(
3750 serde_json::to_string(&ConnectionState::Connecting).unwrap(),
3751 "\"connecting\""
3752 );
3753 assert_eq!(
3754 serde_json::to_string(&ConnectionState::Connected).unwrap(),
3755 "\"connected\""
3756 );
3757 }
3758
3759 #[test]
3760 fn deserializes_runtime_attachment_variants() {
3761 let attachments: Vec<Attachment> = serde_json::from_value(json!([
3762 {
3763 "type": "file",
3764 "path": "/tmp/file.rs",
3765 "displayName": "file.rs",
3766 "lineRange": { "start": 7, "end": 12 }
3767 },
3768 {
3769 "type": "directory",
3770 "path": "/tmp/project",
3771 "displayName": "project"
3772 },
3773 {
3774 "type": "selection",
3775 "filePath": "/tmp/lib.rs",
3776 "displayName": "lib.rs",
3777 "text": "fn main() {}",
3778 "selection": {
3779 "start": { "line": 1, "character": 2 },
3780 "end": { "line": 3, "character": 4 }
3781 }
3782 },
3783 {
3784 "type": "blob",
3785 "data": "Zm9v",
3786 "mimeType": "image/png",
3787 "displayName": "image.png"
3788 },
3789 {
3790 "type": "github_reference",
3791 "number": 42,
3792 "title": "Fix rendering",
3793 "referenceType": "issue",
3794 "state": "open",
3795 "url": "https://github.com/example/repo/issues/42"
3796 }
3797 ]))
3798 .expect("attachments should deserialize");
3799
3800 assert_eq!(attachments.len(), 5);
3801 assert!(matches!(
3802 &attachments[0],
3803 Attachment::File {
3804 path,
3805 display_name,
3806 line_range: Some(AttachmentLineRange { start: 7, end: 12 }),
3807 } if path == &PathBuf::from("/tmp/file.rs") && display_name.as_deref() == Some("file.rs")
3808 ));
3809 assert!(matches!(
3810 &attachments[1],
3811 Attachment::Directory { path, display_name }
3812 if path == &PathBuf::from("/tmp/project") && display_name.as_deref() == Some("project")
3813 ));
3814 assert!(matches!(
3815 &attachments[2],
3816 Attachment::Selection {
3817 file_path,
3818 display_name,
3819 selection:
3820 AttachmentSelectionRange {
3821 start: AttachmentSelectionPosition { line: 1, character: 2 },
3822 end: AttachmentSelectionPosition { line: 3, character: 4 },
3823 },
3824 ..
3825 } if file_path == &PathBuf::from("/tmp/lib.rs") && display_name.as_deref() == Some("lib.rs")
3826 ));
3827 assert!(matches!(
3828 &attachments[3],
3829 Attachment::Blob {
3830 data,
3831 mime_type,
3832 display_name,
3833 } if data == "Zm9v" && mime_type == "image/png" && display_name.as_deref() == Some("image.png")
3834 ));
3835 assert!(matches!(
3836 &attachments[4],
3837 Attachment::GitHubReference {
3838 number: 42,
3839 title,
3840 reference_type: GitHubReferenceType::Issue,
3841 state,
3842 url,
3843 } if title == "Fix rendering"
3844 && state == "open"
3845 && url == "https://github.com/example/repo/issues/42"
3846 ));
3847 }
3848
3849 #[test]
3850 fn ensures_display_names_for_variants_that_support_them() {
3851 let mut attachments = vec![
3852 Attachment::File {
3853 path: PathBuf::from("/tmp/file.rs"),
3854 display_name: None,
3855 line_range: None,
3856 },
3857 Attachment::Selection {
3858 file_path: PathBuf::from("/tmp/src/lib.rs"),
3859 display_name: None,
3860 text: "fn main() {}".to_string(),
3861 selection: AttachmentSelectionRange {
3862 start: AttachmentSelectionPosition {
3863 line: 0,
3864 character: 0,
3865 },
3866 end: AttachmentSelectionPosition {
3867 line: 0,
3868 character: 10,
3869 },
3870 },
3871 },
3872 Attachment::Blob {
3873 data: "Zm9v".to_string(),
3874 mime_type: "image/png".to_string(),
3875 display_name: None,
3876 },
3877 Attachment::GitHubReference {
3878 number: 7,
3879 title: "Track regressions".to_string(),
3880 reference_type: GitHubReferenceType::Issue,
3881 state: "open".to_string(),
3882 url: "https://example.com/issues/7".to_string(),
3883 },
3884 ];
3885
3886 ensure_attachment_display_names(&mut attachments);
3887
3888 assert_eq!(attachments[0].display_name(), Some("file.rs"));
3889 assert_eq!(attachments[1].display_name(), Some("lib.rs"));
3890 assert_eq!(attachments[2].display_name(), Some("attachment"));
3891 assert_eq!(attachments[3].display_name(), None);
3892 assert_eq!(
3893 attachments[3].label(),
3894 Some("Track regressions".to_string())
3895 );
3896 }
3897}
3898
3899#[cfg(test)]
3900mod permission_builder_tests {
3901 use std::sync::Arc;
3902
3903 use crate::handler::{
3904 ApproveAllHandler, HandlerEvent, HandlerResponse, PermissionResult, SessionHandler,
3905 };
3906 use crate::types::{
3907 PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, SessionId,
3908 };
3909
3910 fn permission_event() -> HandlerEvent {
3911 HandlerEvent::PermissionRequest {
3912 session_id: SessionId::from("s1"),
3913 request_id: RequestId::new("1"),
3914 data: PermissionRequestData {
3915 extra: serde_json::json!({"tool": "shell"}),
3916 ..Default::default()
3917 },
3918 }
3919 }
3920
3921 async fn dispatch(handler: &Arc<dyn SessionHandler>) -> HandlerResponse {
3922 handler.on_event(permission_event()).await
3923 }
3924
3925 #[tokio::test]
3926 async fn session_config_approve_all_wraps_existing_handler() {
3927 let cfg = SessionConfig::default()
3928 .with_handler(Arc::new(ApproveAllHandler))
3929 .approve_all_permissions();
3930 let handler = cfg.handler.expect("handler should be set");
3931 match dispatch(&handler).await {
3932 HandlerResponse::Permission(PermissionResult::Approved) => {}
3933 other => panic!("expected Approved, got {other:?}"),
3934 }
3935 }
3936
3937 #[tokio::test]
3938 async fn session_config_approve_all_defaults_to_noop_inner() {
3939 let cfg = SessionConfig::default().approve_all_permissions();
3943 let handler = cfg.handler.expect("handler should be set");
3944 match dispatch(&handler).await {
3945 HandlerResponse::Permission(PermissionResult::Approved) => {}
3946 other => panic!("expected Approved, got {other:?}"),
3947 }
3948 }
3949
3950 #[tokio::test]
3951 async fn session_config_deny_all_denies() {
3952 let cfg = SessionConfig::default()
3953 .with_handler(Arc::new(ApproveAllHandler))
3954 .deny_all_permissions();
3955 let handler = cfg.handler.expect("handler should be set");
3956 match dispatch(&handler).await {
3957 HandlerResponse::Permission(PermissionResult::Denied) => {}
3958 other => panic!("expected Denied, got {other:?}"),
3959 }
3960 }
3961
3962 #[tokio::test]
3963 async fn session_config_approve_permissions_if_consults_predicate() {
3964 let cfg = SessionConfig::default()
3965 .with_handler(Arc::new(ApproveAllHandler))
3966 .approve_permissions_if(|data| {
3967 data.extra.get("tool").and_then(|v| v.as_str()) != Some("shell")
3968 });
3969 let handler = cfg.handler.expect("handler should be set");
3970 match dispatch(&handler).await {
3971 HandlerResponse::Permission(PermissionResult::Denied) => {}
3972 other => panic!("expected Denied for shell, got {other:?}"),
3973 }
3974 }
3975
3976 #[tokio::test]
3977 async fn resume_session_config_approve_all_wraps_existing_handler() {
3978 let cfg = ResumeSessionConfig::new(SessionId::from("s1"))
3979 .with_handler(Arc::new(ApproveAllHandler))
3980 .approve_all_permissions();
3981 let handler = cfg.handler.expect("handler should be set");
3982 match dispatch(&handler).await {
3983 HandlerResponse::Permission(PermissionResult::Approved) => {}
3984 other => panic!("expected Approved, got {other:?}"),
3985 }
3986 }
3987}