1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::future::Future;
4use std::path::PathBuf;
5use std::pin::Pin;
6use tokio::sync::mpsc;
7
8use crate::hooks::{HookCallbackMatcher, HookEvent};
9use crate::mcp::McpServerConfig;
10use crate::provider::LlmProvider;
11use crate::tools::executor::ToolResult;
12use crate::types::agent::AgentDefinition;
13use crate::types::permissions::{CanUseToolOptions, PermissionResult};
14
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "camelCase")]
18pub enum PermissionMode {
19 Default,
21 AcceptEdits,
23 BypassPermissions,
25 Plan,
27 DontAsk,
29}
30
31impl std::fmt::Display for PermissionMode {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 PermissionMode::Default => write!(f, "default"),
35 PermissionMode::AcceptEdits => write!(f, "acceptEdits"),
36 PermissionMode::BypassPermissions => write!(f, "bypassPermissions"),
37 PermissionMode::Plan => write!(f, "plan"),
38 PermissionMode::DontAsk => write!(f, "dontAsk"),
39 }
40 }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
45#[serde(rename_all = "lowercase")]
46pub enum Effort {
47 Low,
48 Medium,
49 High,
50 Max,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
55#[serde(rename_all = "lowercase")]
56pub enum SettingSource {
57 User,
59 Project,
61 Local,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(tag = "type")]
68pub enum ThinkingConfig {
69 #[serde(rename = "adaptive")]
71 Adaptive,
72 #[serde(rename = "disabled")]
74 Disabled,
75 #[serde(rename = "enabled")]
77 Enabled { budget_tokens: u64 },
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(untagged)]
83pub enum SystemPrompt {
84 Custom(String),
86 Preset {
88 #[serde(rename = "type")]
89 prompt_type: String,
90 preset: String,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 append: Option<String>,
93 },
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct SandboxSettings {
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub enabled: Option<bool>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub allow_network: Option<bool>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ToolConfig {
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub ask_user_question: Option<AskUserQuestionConfig>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct AskUserQuestionConfig {
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub preview_format: Option<PreviewFormat>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119#[serde(rename_all = "lowercase")]
120pub enum PreviewFormat {
121 Markdown,
122 Html,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct PluginConfig {
128 #[serde(rename = "type")]
129 pub plugin_type: String,
130 pub path: String,
131}
132
133pub struct Options {
135 pub allowed_tools: Vec<String>,
137
138 pub disallowed_tools: Vec<String>,
140
141 pub permission_mode: PermissionMode,
143
144 pub can_use_tool: Option<CanUseToolFn>,
146
147 pub cwd: Option<String>,
149
150 pub model: Option<String>,
152
153 pub fallback_model: Option<String>,
155
156 pub effort: Option<Effort>,
158
159 pub max_turns: Option<u32>,
161
162 pub max_budget_usd: Option<f64>,
164
165 pub context_budget: Option<u64>,
169
170 pub compaction_model: Option<String>,
173
174 pub compaction_provider: Option<Box<dyn LlmProvider>>,
177
178 pub system_prompt: Option<SystemPrompt>,
180
181 pub thinking: Option<ThinkingConfig>,
183
184 pub hooks: HashMap<HookEvent, Vec<HookCallbackMatcher>>,
186
187 pub hook_dirs: Vec<PathBuf>,
190
191 pub mcp_servers: HashMap<String, McpServerConfig>,
193
194 pub agents: HashMap<String, AgentDefinition>,
196
197 pub continue_session: bool,
199
200 pub resume: Option<String>,
202
203 pub fork_session: bool,
205
206 pub session_id: Option<String>,
208
209 pub setting_sources: Vec<SettingSource>,
211
212 pub debug: bool,
214
215 pub debug_file: Option<String>,
217
218 pub include_partial_messages: bool,
220
221 pub persist_session: bool,
223
224 pub enable_file_checkpointing: bool,
226
227 pub env: HashMap<String, String>,
229
230 pub additional_directories: Vec<String>,
232
233 pub env_blocklist: Vec<String>,
237
238 #[cfg(unix)]
242 pub pre_exec_fn: Option<Box<dyn Fn() -> std::io::Result<()> + Send + Sync>>,
243
244 pub output_format: Option<serde_json::Value>,
246
247 pub sandbox: Option<SandboxSettings>,
249
250 pub tool_config: Option<ToolConfig>,
252
253 pub plugins: Vec<PluginConfig>,
255
256 pub prompt_suggestions: bool,
258
259 pub external_tool_handler: Option<ExternalToolHandlerFn>,
264
265 pub custom_tool_definitions: Vec<CustomToolDefinition>,
270
271 pub followup_rx: Option<mpsc::UnboundedReceiver<String>>,
276
277 pub api_key: Option<String>,
279
280 pub attachments: Vec<QueryAttachment>,
282
283 pub provider: Option<Box<dyn LlmProvider>>,
285
286 pub pre_compact_handler: Option<PreCompactHandlerFn>,
289
290 pub max_tokens: Option<u32>,
292
293 pub summary_max_tokens: Option<u32>,
295
296 pub min_keep_messages: Option<usize>,
298
299 pub max_tool_result_bytes: Option<usize>,
303
304 pub prune_threshold_pct: Option<u8>,
307
308 pub prune_tool_result_max_chars: Option<usize>,
311}
312
313#[derive(Debug, Clone)]
315pub struct CustomToolDefinition {
316 pub name: String,
317 pub description: String,
318 pub input_schema: serde_json::Value,
319}
320
321#[derive(Debug, Clone)]
323pub struct QueryAttachment {
324 pub file_name: String,
326 pub mime_type: String,
328 pub base64_data: String,
330}
331
332pub type PreCompactHandlerFn = Box<
338 dyn Fn(Vec<crate::client::ApiMessage>) -> Pin<Box<dyn Future<Output = ()> + Send>>
339 + Send
340 + Sync,
341>;
342
343pub type ExternalToolHandlerFn = Box<
350 dyn Fn(String, serde_json::Value) -> Pin<Box<dyn Future<Output = Option<ToolResult>> + Send>>
351 + Send
352 + Sync,
353>;
354
355pub type CanUseToolFn = Box<
357 dyn Fn(
358 String,
359 serde_json::Value,
360 CanUseToolOptions,
361 ) -> std::pin::Pin<
362 Box<dyn std::future::Future<Output = crate::error::Result<PermissionResult>> + Send>,
363 > + Send
364 + Sync,
365>;
366
367impl Default for Options {
368 fn default() -> Self {
369 Self {
370 allowed_tools: Vec::new(),
371 disallowed_tools: Vec::new(),
372 permission_mode: PermissionMode::Default,
373 can_use_tool: None,
374 cwd: None,
375 model: None,
376 fallback_model: None,
377 effort: None,
378 max_turns: None,
379 max_budget_usd: None,
380 context_budget: None,
381 compaction_model: None,
382 compaction_provider: None,
383 system_prompt: None,
384 thinking: None,
385 hooks: HashMap::new(),
386 hook_dirs: Vec::new(),
387 mcp_servers: HashMap::new(),
388 agents: HashMap::new(),
389 continue_session: false,
390 resume: None,
391 fork_session: false,
392 session_id: None,
393 setting_sources: Vec::new(),
394 debug: false,
395 debug_file: None,
396 include_partial_messages: false,
397 persist_session: true,
398 enable_file_checkpointing: false,
399 env: HashMap::new(),
400 additional_directories: Vec::new(),
401 env_blocklist: Vec::new(),
402 #[cfg(unix)]
403 pre_exec_fn: None,
404 output_format: None,
405 sandbox: None,
406 tool_config: None,
407 plugins: Vec::new(),
408 prompt_suggestions: false,
409 external_tool_handler: None,
410 custom_tool_definitions: Vec::new(),
411 followup_rx: None,
412 api_key: None,
413 attachments: Vec::new(),
414 provider: None,
415 pre_compact_handler: None,
416 max_tokens: None,
417 summary_max_tokens: None,
418 min_keep_messages: None,
419 max_tool_result_bytes: None,
420 prune_threshold_pct: None,
421 prune_tool_result_max_chars: None,
422 }
423 }
424}
425
426impl Options {
427 pub fn builder() -> OptionsBuilder {
429 OptionsBuilder::default()
430 }
431}
432
433#[derive(Default)]
435pub struct OptionsBuilder {
436 options: Options,
437}
438
439impl OptionsBuilder {
440 pub fn allowed_tools(mut self, tools: Vec<String>) -> Self {
441 self.options.allowed_tools = tools;
442 self
443 }
444
445 pub fn disallowed_tools(mut self, tools: Vec<String>) -> Self {
446 self.options.disallowed_tools = tools;
447 self
448 }
449
450 pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
451 self.options.permission_mode = mode;
452 self
453 }
454
455 pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
456 self.options.cwd = Some(cwd.into());
457 self
458 }
459
460 pub fn additional_directories(mut self, dirs: Vec<String>) -> Self {
461 self.options.additional_directories = dirs;
462 self
463 }
464
465 pub fn env_blocklist(mut self, keys: Vec<String>) -> Self {
466 self.options.env_blocklist = keys;
467 self
468 }
469
470 #[cfg(unix)]
473 pub fn pre_exec_fn(mut self, f: Box<dyn Fn() -> std::io::Result<()> + Send + Sync>) -> Self {
474 self.options.pre_exec_fn = Some(f);
475 self
476 }
477
478 pub fn model(mut self, model: impl Into<String>) -> Self {
479 self.options.model = Some(model.into());
480 self
481 }
482
483 pub fn fallback_model(mut self, model: impl Into<String>) -> Self {
484 self.options.fallback_model = Some(model.into());
485 self
486 }
487
488 pub fn effort(mut self, effort: Effort) -> Self {
489 self.options.effort = Some(effort);
490 self
491 }
492
493 pub fn max_turns(mut self, max_turns: u32) -> Self {
494 self.options.max_turns = Some(max_turns);
495 self
496 }
497
498 pub fn max_budget_usd(mut self, budget: f64) -> Self {
499 self.options.max_budget_usd = Some(budget);
500 self
501 }
502
503 pub fn context_budget(mut self, budget: u64) -> Self {
504 self.options.context_budget = Some(budget);
505 self
506 }
507
508 pub fn compaction_model(mut self, model: impl Into<String>) -> Self {
509 self.options.compaction_model = Some(model.into());
510 self
511 }
512
513 pub fn compaction_provider(mut self, provider: Box<dyn LlmProvider>) -> Self {
514 self.options.compaction_provider = Some(provider);
515 self
516 }
517
518 pub fn system_prompt(mut self, prompt: SystemPrompt) -> Self {
519 self.options.system_prompt = Some(prompt);
520 self
521 }
522
523 pub fn thinking(mut self, config: ThinkingConfig) -> Self {
524 self.options.thinking = Some(config);
525 self
526 }
527
528 pub fn hook(mut self, event: HookEvent, matchers: Vec<HookCallbackMatcher>) -> Self {
529 self.options.hooks.insert(event, matchers);
530 self
531 }
532
533 pub fn hook_dirs(mut self, dirs: Vec<PathBuf>) -> Self {
534 self.options.hook_dirs = dirs;
535 self
536 }
537
538 pub fn mcp_server(mut self, name: impl Into<String>, config: McpServerConfig) -> Self {
539 self.options.mcp_servers.insert(name.into(), config);
540 self
541 }
542
543 pub fn agent(mut self, name: impl Into<String>, definition: AgentDefinition) -> Self {
544 self.options.agents.insert(name.into(), definition);
545 self
546 }
547
548 pub fn continue_session(mut self, value: bool) -> Self {
549 self.options.continue_session = value;
550 self
551 }
552
553 pub fn resume(mut self, session_id: impl Into<String>) -> Self {
554 self.options.resume = Some(session_id.into());
555 self
556 }
557
558 pub fn session_id(mut self, id: impl Into<String>) -> Self {
559 self.options.session_id = Some(id.into());
560 self
561 }
562
563 pub fn fork_session(mut self, value: bool) -> Self {
564 self.options.fork_session = value;
565 self
566 }
567
568 pub fn setting_sources(mut self, sources: Vec<SettingSource>) -> Self {
569 self.options.setting_sources = sources;
570 self
571 }
572
573 pub fn debug(mut self, value: bool) -> Self {
574 self.options.debug = value;
575 self
576 }
577
578 pub fn include_partial_messages(mut self, value: bool) -> Self {
579 self.options.include_partial_messages = value;
580 self
581 }
582
583 pub fn persist_session(mut self, value: bool) -> Self {
584 self.options.persist_session = value;
585 self
586 }
587
588 pub fn enable_file_checkpointing(mut self, value: bool) -> Self {
589 self.options.enable_file_checkpointing = value;
590 self
591 }
592
593 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
594 self.options.env.insert(key.into(), value.into());
595 self
596 }
597
598 pub fn output_format(mut self, schema: serde_json::Value) -> Self {
599 self.options.output_format = Some(schema);
600 self
601 }
602
603 pub fn sandbox(mut self, settings: SandboxSettings) -> Self {
604 self.options.sandbox = Some(settings);
605 self
606 }
607
608 pub fn external_tool_handler(mut self, handler: ExternalToolHandlerFn) -> Self {
609 self.options.external_tool_handler = Some(handler);
610 self
611 }
612
613 pub fn custom_tool(mut self, def: CustomToolDefinition) -> Self {
614 self.options.custom_tool_definitions.push(def);
615 self
616 }
617
618 pub fn custom_tools(mut self, defs: Vec<CustomToolDefinition>) -> Self {
619 self.options.custom_tool_definitions.extend(defs);
620 self
621 }
622
623 pub fn followup_rx(mut self, rx: mpsc::UnboundedReceiver<String>) -> Self {
624 self.options.followup_rx = Some(rx);
625 self
626 }
627
628 pub fn api_key(mut self, key: impl Into<String>) -> Self {
629 self.options.api_key = Some(key.into());
630 self
631 }
632
633 pub fn attachments(mut self, attachments: Vec<QueryAttachment>) -> Self {
634 self.options.attachments = attachments;
635 self
636 }
637
638 pub fn provider(mut self, provider: Box<dyn LlmProvider>) -> Self {
639 self.options.provider = Some(provider);
640 self
641 }
642
643 pub fn pre_compact_handler(mut self, handler: PreCompactHandlerFn) -> Self {
644 self.options.pre_compact_handler = Some(handler);
645 self
646 }
647
648 pub fn max_tokens(mut self, max_tokens: u32) -> Self {
649 self.options.max_tokens = Some(max_tokens);
650 self
651 }
652
653 pub fn summary_max_tokens(mut self, tokens: u32) -> Self {
654 self.options.summary_max_tokens = Some(tokens);
655 self
656 }
657
658 pub fn min_keep_messages(mut self, count: usize) -> Self {
659 self.options.min_keep_messages = Some(count);
660 self
661 }
662
663 pub fn max_tool_result_bytes(mut self, bytes: usize) -> Self {
664 self.options.max_tool_result_bytes = Some(bytes);
665 self
666 }
667
668 pub fn prune_threshold_pct(mut self, pct: u8) -> Self {
669 self.options.prune_threshold_pct = Some(pct);
670 self
671 }
672
673 pub fn prune_tool_result_max_chars(mut self, chars: usize) -> Self {
674 self.options.prune_tool_result_max_chars = Some(chars);
675 self
676 }
677
678 pub fn build(self) -> Options {
679 self.options
680 }
681}
682
683impl std::fmt::Debug for Options {
684 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685 f.debug_struct("Options")
686 .field("allowed_tools", &self.allowed_tools)
687 .field("disallowed_tools", &self.disallowed_tools)
688 .field("permission_mode", &self.permission_mode)
689 .field("cwd", &self.cwd)
690 .field("model", &self.model)
691 .field("effort", &self.effort)
692 .field("max_turns", &self.max_turns)
693 .field("max_budget_usd", &self.max_budget_usd)
694 .field("context_budget", &self.context_budget)
695 .field("compaction_model", &self.compaction_model)
696 .field("hooks_count", &self.hooks.len())
697 .field("mcp_servers_count", &self.mcp_servers.len())
698 .field("agents_count", &self.agents.len())
699 .field("continue_session", &self.continue_session)
700 .field("resume", &self.resume)
701 .field("persist_session", &self.persist_session)
702 .finish()
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709
710 #[test]
711 fn builder_api_key_sets_field() {
712 let opts = Options::builder().api_key("sk-ant-test-key").build();
713 assert_eq!(opts.api_key.as_deref(), Some("sk-ant-test-key"));
714 }
715
716 #[test]
717 fn builder_api_key_default_is_none() {
718 let opts = Options::builder().build();
719 assert!(opts.api_key.is_none());
720 }
721
722 #[test]
723 fn builder_api_key_with_other_options() {
724 let opts = Options::builder()
725 .model("claude-haiku-4-5")
726 .api_key("sk-ant-combined")
727 .max_turns(10)
728 .build();
729 assert_eq!(opts.api_key.as_deref(), Some("sk-ant-combined"));
730 assert_eq!(opts.model.as_deref(), Some("claude-haiku-4-5"));
731 assert_eq!(opts.max_turns, Some(10));
732 }
733
734 #[test]
735 fn builder_max_tokens_sets_field() {
736 let opts = Options::builder().max_tokens(8192).build();
737 assert_eq!(opts.max_tokens, Some(8192));
738 }
739
740 #[test]
741 fn builder_summary_max_tokens_sets_field() {
742 let opts = Options::builder().summary_max_tokens(2048).build();
743 assert_eq!(opts.summary_max_tokens, Some(2048));
744 }
745
746 #[test]
747 fn builder_min_keep_messages_sets_field() {
748 let opts = Options::builder().min_keep_messages(6).build();
749 assert_eq!(opts.min_keep_messages, Some(6));
750 }
751
752 #[test]
753 fn builder_output_format_default_is_none() {
754 let opts = Options::builder().build();
755 assert!(opts.output_format.is_none());
756 }
757
758 #[test]
759 fn builder_output_format_sets_field() {
760 let schema = serde_json::json!({
761 "type": "object",
762 "properties": {
763 "name": { "type": "string" }
764 }
765 });
766 let opts = Options::builder().output_format(schema.clone()).build();
767 assert_eq!(opts.output_format, Some(schema));
768 }
769
770 #[test]
771 fn builder_output_format_with_other_options() {
772 let schema = serde_json::json!({"type": "object"});
773 let opts = Options::builder()
774 .output_format(schema.clone())
775 .max_turns(1)
776 .model("claude-haiku-4-5")
777 .build();
778 assert_eq!(opts.output_format, Some(schema));
779 assert_eq!(opts.max_turns, Some(1));
780 assert_eq!(opts.model.as_deref(), Some("claude-haiku-4-5"));
781 }
782
783 #[test]
784 fn builder_pre_compact_handler_default_is_none() {
785 let opts = Options::builder().build();
786 assert!(opts.pre_compact_handler.is_none());
787 }
788
789 #[test]
790 fn builder_pre_compact_handler_sets_field() {
791 let handler: PreCompactHandlerFn = Box::new(|_msgs| Box::pin(async {}));
792 let opts = Options::builder().pre_compact_handler(handler).build();
793 assert!(opts.pre_compact_handler.is_some());
794 }
795
796 #[test]
797 fn builder_max_tool_result_bytes_sets_field() {
798 let opts = Options::builder().max_tool_result_bytes(100_000).build();
799 assert_eq!(opts.max_tool_result_bytes, Some(100_000));
800 }
801
802 #[test]
803 fn builder_max_tool_result_bytes_default_is_none() {
804 let opts = Options::builder().build();
805 assert!(opts.max_tool_result_bytes.is_none());
806 }
807
808 #[test]
809 fn builder_prune_threshold_pct_sets_field() {
810 let opts = Options::builder().prune_threshold_pct(80).build();
811 assert_eq!(opts.prune_threshold_pct, Some(80));
812 }
813
814 #[test]
815 fn builder_prune_threshold_pct_default_is_none() {
816 let opts = Options::builder().build();
817 assert!(opts.prune_threshold_pct.is_none());
818 }
819
820 #[test]
821 fn builder_prune_tool_result_max_chars_sets_field() {
822 let opts = Options::builder().prune_tool_result_max_chars(5000).build();
823 assert_eq!(opts.prune_tool_result_max_chars, Some(5000));
824 }
825
826 #[test]
827 fn builder_prune_tool_result_max_chars_default_is_none() {
828 let opts = Options::builder().build();
829 assert!(opts.prune_tool_result_max_chars.is_none());
830 }
831
832 #[test]
833 fn builder_sanitization_and_pruning_combined() {
834 let opts = Options::builder()
835 .max_tool_result_bytes(75_000)
836 .prune_threshold_pct(60)
837 .prune_tool_result_max_chars(3000)
838 .context_budget(200_000)
839 .build();
840 assert_eq!(opts.max_tool_result_bytes, Some(75_000));
841 assert_eq!(opts.prune_threshold_pct, Some(60));
842 assert_eq!(opts.prune_tool_result_max_chars, Some(3000));
843 assert_eq!(opts.context_budget, Some(200_000));
844 }
845}