1use std::collections::HashMap;
2use std::future::Future;
3use std::path::PathBuf;
4use std::pin::Pin;
5use serde::{Deserialize, Serialize};
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 {
78 budget_tokens: u64,
79 },
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(untagged)]
85pub enum SystemPrompt {
86 Custom(String),
88 Preset {
90 #[serde(rename = "type")]
91 prompt_type: String,
92 preset: String,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 append: Option<String>,
95 },
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct SandboxSettings {
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub enabled: Option<bool>,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub allow_network: Option<bool>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ToolConfig {
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub ask_user_question: Option<AskUserQuestionConfig>,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct AskUserQuestionConfig {
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub preview_format: Option<PreviewFormat>,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
121#[serde(rename_all = "lowercase")]
122pub enum PreviewFormat {
123 Markdown,
124 Html,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct PluginConfig {
130 #[serde(rename = "type")]
131 pub plugin_type: String,
132 pub path: String,
133}
134
135pub struct Options {
137 pub allowed_tools: Vec<String>,
139
140 pub disallowed_tools: Vec<String>,
142
143 pub permission_mode: PermissionMode,
145
146 pub can_use_tool: Option<CanUseToolFn>,
148
149 pub cwd: Option<String>,
151
152 pub model: Option<String>,
154
155 pub fallback_model: Option<String>,
157
158 pub effort: Option<Effort>,
160
161 pub max_turns: Option<u32>,
163
164 pub max_budget_usd: Option<f64>,
166
167 pub context_budget: Option<u64>,
171
172 pub compaction_model: Option<String>,
175
176 pub compaction_provider: Option<Box<dyn LlmProvider>>,
179
180 pub system_prompt: Option<SystemPrompt>,
182
183 pub thinking: Option<ThinkingConfig>,
185
186 pub hooks: HashMap<HookEvent, Vec<HookCallbackMatcher>>,
188
189 pub hook_dirs: Vec<PathBuf>,
192
193 pub mcp_servers: HashMap<String, McpServerConfig>,
195
196 pub agents: HashMap<String, AgentDefinition>,
198
199 pub continue_session: bool,
201
202 pub resume: Option<String>,
204
205 pub fork_session: bool,
207
208 pub session_id: Option<String>,
210
211 pub setting_sources: Vec<SettingSource>,
213
214 pub debug: bool,
216
217 pub debug_file: Option<String>,
219
220 pub include_partial_messages: bool,
222
223 pub persist_session: bool,
225
226 pub enable_file_checkpointing: bool,
228
229 pub env: HashMap<String, String>,
231
232 pub additional_directories: Vec<String>,
234
235 pub env_blocklist: Vec<String>,
239
240 pub output_format: Option<serde_json::Value>,
242
243 pub sandbox: Option<SandboxSettings>,
245
246 pub tool_config: Option<ToolConfig>,
248
249 pub plugins: Vec<PluginConfig>,
251
252 pub prompt_suggestions: bool,
254
255 pub external_tool_handler: Option<ExternalToolHandlerFn>,
260
261 pub custom_tool_definitions: Vec<CustomToolDefinition>,
266
267 pub followup_rx: Option<mpsc::UnboundedReceiver<String>>,
272
273 pub api_key: Option<String>,
275
276 pub attachments: Vec<QueryAttachment>,
278
279 pub provider: Option<Box<dyn LlmProvider>>,
281
282 pub pre_compact_handler: Option<PreCompactHandlerFn>,
285
286 pub max_tokens: Option<u32>,
288
289 pub summary_max_tokens: Option<u32>,
291
292 pub min_keep_messages: Option<usize>,
294
295 pub max_tool_result_bytes: Option<usize>,
299
300 pub prune_threshold_pct: Option<u8>,
303
304 pub prune_tool_result_max_chars: Option<usize>,
307}
308
309#[derive(Debug, Clone)]
311pub struct CustomToolDefinition {
312 pub name: String,
313 pub description: String,
314 pub input_schema: serde_json::Value,
315}
316
317#[derive(Debug, Clone)]
319pub struct QueryAttachment {
320 pub file_name: String,
322 pub mime_type: String,
324 pub base64_data: String,
326}
327
328pub type PreCompactHandlerFn = Box<
334 dyn Fn(
335 Vec<crate::client::ApiMessage>,
336 ) -> Pin<Box<dyn Future<Output = ()> + Send>>
337 + Send
338 + Sync,
339>;
340
341pub type ExternalToolHandlerFn = Box<
348 dyn Fn(
349 String,
350 serde_json::Value,
351 ) -> Pin<Box<dyn Future<Output = Option<ToolResult>> + Send>>
352 + Send
353 + Sync,
354>;
355
356pub type CanUseToolFn = Box<
358 dyn Fn(
359 String,
360 serde_json::Value,
361 CanUseToolOptions,
362 ) -> std::pin::Pin<
363 Box<dyn std::future::Future<Output = crate::error::Result<PermissionResult>> + Send>,
364 > + Send
365 + Sync,
366>;
367
368impl Default for Options {
369 fn default() -> Self {
370 Self {
371 allowed_tools: Vec::new(),
372 disallowed_tools: Vec::new(),
373 permission_mode: PermissionMode::Default,
374 can_use_tool: None,
375 cwd: None,
376 model: None,
377 fallback_model: None,
378 effort: None,
379 max_turns: None,
380 max_budget_usd: None,
381 context_budget: None,
382 compaction_model: None,
383 compaction_provider: None,
384 system_prompt: None,
385 thinking: None,
386 hooks: HashMap::new(),
387 hook_dirs: Vec::new(),
388 mcp_servers: HashMap::new(),
389 agents: HashMap::new(),
390 continue_session: false,
391 resume: None,
392 fork_session: false,
393 session_id: None,
394 setting_sources: Vec::new(),
395 debug: false,
396 debug_file: None,
397 include_partial_messages: false,
398 persist_session: true,
399 enable_file_checkpointing: false,
400 env: HashMap::new(),
401 additional_directories: Vec::new(),
402 env_blocklist: Vec::new(),
403 output_format: None,
404 sandbox: None,
405 tool_config: None,
406 plugins: Vec::new(),
407 prompt_suggestions: false,
408 external_tool_handler: None,
409 custom_tool_definitions: Vec::new(),
410 followup_rx: None,
411 api_key: None,
412 attachments: Vec::new(),
413 provider: None,
414 pre_compact_handler: None,
415 max_tokens: None,
416 summary_max_tokens: None,
417 min_keep_messages: None,
418 max_tool_result_bytes: None,
419 prune_threshold_pct: None,
420 prune_tool_result_max_chars: None,
421 }
422 }
423}
424
425impl Options {
426 pub fn builder() -> OptionsBuilder {
428 OptionsBuilder::default()
429 }
430}
431
432#[derive(Default)]
434pub struct OptionsBuilder {
435 options: Options,
436}
437
438impl OptionsBuilder {
439 pub fn allowed_tools(mut self, tools: Vec<String>) -> Self {
440 self.options.allowed_tools = tools;
441 self
442 }
443
444 pub fn disallowed_tools(mut self, tools: Vec<String>) -> Self {
445 self.options.disallowed_tools = tools;
446 self
447 }
448
449 pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
450 self.options.permission_mode = mode;
451 self
452 }
453
454 pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
455 self.options.cwd = Some(cwd.into());
456 self
457 }
458
459 pub fn additional_directories(mut self, dirs: Vec<String>) -> Self {
460 self.options.additional_directories = dirs;
461 self
462 }
463
464 pub fn env_blocklist(mut self, keys: Vec<String>) -> Self {
465 self.options.env_blocklist = keys;
466 self
467 }
468
469 pub fn model(mut self, model: impl Into<String>) -> Self {
470 self.options.model = Some(model.into());
471 self
472 }
473
474 pub fn fallback_model(mut self, model: impl Into<String>) -> Self {
475 self.options.fallback_model = Some(model.into());
476 self
477 }
478
479 pub fn effort(mut self, effort: Effort) -> Self {
480 self.options.effort = Some(effort);
481 self
482 }
483
484 pub fn max_turns(mut self, max_turns: u32) -> Self {
485 self.options.max_turns = Some(max_turns);
486 self
487 }
488
489 pub fn max_budget_usd(mut self, budget: f64) -> Self {
490 self.options.max_budget_usd = Some(budget);
491 self
492 }
493
494 pub fn context_budget(mut self, budget: u64) -> Self {
495 self.options.context_budget = Some(budget);
496 self
497 }
498
499 pub fn compaction_model(mut self, model: impl Into<String>) -> Self {
500 self.options.compaction_model = Some(model.into());
501 self
502 }
503
504 pub fn compaction_provider(mut self, provider: Box<dyn LlmProvider>) -> Self {
505 self.options.compaction_provider = Some(provider);
506 self
507 }
508
509 pub fn system_prompt(mut self, prompt: SystemPrompt) -> Self {
510 self.options.system_prompt = Some(prompt);
511 self
512 }
513
514 pub fn thinking(mut self, config: ThinkingConfig) -> Self {
515 self.options.thinking = Some(config);
516 self
517 }
518
519 pub fn hook(mut self, event: HookEvent, matchers: Vec<HookCallbackMatcher>) -> Self {
520 self.options.hooks.insert(event, matchers);
521 self
522 }
523
524 pub fn hook_dirs(mut self, dirs: Vec<PathBuf>) -> Self {
525 self.options.hook_dirs = dirs;
526 self
527 }
528
529 pub fn mcp_server(mut self, name: impl Into<String>, config: McpServerConfig) -> Self {
530 self.options.mcp_servers.insert(name.into(), config);
531 self
532 }
533
534 pub fn agent(mut self, name: impl Into<String>, definition: AgentDefinition) -> Self {
535 self.options.agents.insert(name.into(), definition);
536 self
537 }
538
539 pub fn continue_session(mut self, value: bool) -> Self {
540 self.options.continue_session = value;
541 self
542 }
543
544 pub fn resume(mut self, session_id: impl Into<String>) -> Self {
545 self.options.resume = Some(session_id.into());
546 self
547 }
548
549 pub fn session_id(mut self, id: impl Into<String>) -> Self {
550 self.options.session_id = Some(id.into());
551 self
552 }
553
554 pub fn fork_session(mut self, value: bool) -> Self {
555 self.options.fork_session = value;
556 self
557 }
558
559 pub fn setting_sources(mut self, sources: Vec<SettingSource>) -> Self {
560 self.options.setting_sources = sources;
561 self
562 }
563
564 pub fn debug(mut self, value: bool) -> Self {
565 self.options.debug = value;
566 self
567 }
568
569 pub fn include_partial_messages(mut self, value: bool) -> Self {
570 self.options.include_partial_messages = value;
571 self
572 }
573
574 pub fn persist_session(mut self, value: bool) -> Self {
575 self.options.persist_session = value;
576 self
577 }
578
579 pub fn enable_file_checkpointing(mut self, value: bool) -> Self {
580 self.options.enable_file_checkpointing = value;
581 self
582 }
583
584 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
585 self.options.env.insert(key.into(), value.into());
586 self
587 }
588
589 pub fn output_format(mut self, schema: serde_json::Value) -> Self {
590 self.options.output_format = Some(schema);
591 self
592 }
593
594 pub fn sandbox(mut self, settings: SandboxSettings) -> Self {
595 self.options.sandbox = Some(settings);
596 self
597 }
598
599 pub fn external_tool_handler(mut self, handler: ExternalToolHandlerFn) -> Self {
600 self.options.external_tool_handler = Some(handler);
601 self
602 }
603
604 pub fn custom_tool(mut self, def: CustomToolDefinition) -> Self {
605 self.options.custom_tool_definitions.push(def);
606 self
607 }
608
609 pub fn custom_tools(mut self, defs: Vec<CustomToolDefinition>) -> Self {
610 self.options.custom_tool_definitions.extend(defs);
611 self
612 }
613
614 pub fn followup_rx(mut self, rx: mpsc::UnboundedReceiver<String>) -> Self {
615 self.options.followup_rx = Some(rx);
616 self
617 }
618
619 pub fn api_key(mut self, key: impl Into<String>) -> Self {
620 self.options.api_key = Some(key.into());
621 self
622 }
623
624 pub fn attachments(mut self, attachments: Vec<QueryAttachment>) -> Self {
625 self.options.attachments = attachments;
626 self
627 }
628
629 pub fn provider(mut self, provider: Box<dyn LlmProvider>) -> Self {
630 self.options.provider = Some(provider);
631 self
632 }
633
634 pub fn pre_compact_handler(mut self, handler: PreCompactHandlerFn) -> Self {
635 self.options.pre_compact_handler = Some(handler);
636 self
637 }
638
639 pub fn max_tokens(mut self, max_tokens: u32) -> Self {
640 self.options.max_tokens = Some(max_tokens);
641 self
642 }
643
644 pub fn summary_max_tokens(mut self, tokens: u32) -> Self {
645 self.options.summary_max_tokens = Some(tokens);
646 self
647 }
648
649 pub fn min_keep_messages(mut self, count: usize) -> Self {
650 self.options.min_keep_messages = Some(count);
651 self
652 }
653
654 pub fn max_tool_result_bytes(mut self, bytes: usize) -> Self {
655 self.options.max_tool_result_bytes = Some(bytes);
656 self
657 }
658
659 pub fn prune_threshold_pct(mut self, pct: u8) -> Self {
660 self.options.prune_threshold_pct = Some(pct);
661 self
662 }
663
664 pub fn prune_tool_result_max_chars(mut self, chars: usize) -> Self {
665 self.options.prune_tool_result_max_chars = Some(chars);
666 self
667 }
668
669 pub fn build(self) -> Options {
670 self.options
671 }
672}
673
674impl std::fmt::Debug for Options {
675 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
676 f.debug_struct("Options")
677 .field("allowed_tools", &self.allowed_tools)
678 .field("disallowed_tools", &self.disallowed_tools)
679 .field("permission_mode", &self.permission_mode)
680 .field("cwd", &self.cwd)
681 .field("model", &self.model)
682 .field("effort", &self.effort)
683 .field("max_turns", &self.max_turns)
684 .field("max_budget_usd", &self.max_budget_usd)
685 .field("context_budget", &self.context_budget)
686 .field("compaction_model", &self.compaction_model)
687 .field("hooks_count", &self.hooks.len())
688 .field("mcp_servers_count", &self.mcp_servers.len())
689 .field("agents_count", &self.agents.len())
690 .field("continue_session", &self.continue_session)
691 .field("resume", &self.resume)
692 .field("persist_session", &self.persist_session)
693 .finish()
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700
701 #[test]
702 fn builder_api_key_sets_field() {
703 let opts = Options::builder()
704 .api_key("sk-ant-test-key")
705 .build();
706 assert_eq!(opts.api_key.as_deref(), Some("sk-ant-test-key"));
707 }
708
709 #[test]
710 fn builder_api_key_default_is_none() {
711 let opts = Options::builder().build();
712 assert!(opts.api_key.is_none());
713 }
714
715 #[test]
716 fn builder_api_key_with_other_options() {
717 let opts = Options::builder()
718 .model("claude-haiku-4-5")
719 .api_key("sk-ant-combined")
720 .max_turns(10)
721 .build();
722 assert_eq!(opts.api_key.as_deref(), Some("sk-ant-combined"));
723 assert_eq!(opts.model.as_deref(), Some("claude-haiku-4-5"));
724 assert_eq!(opts.max_turns, Some(10));
725 }
726
727 #[test]
728 fn builder_max_tokens_sets_field() {
729 let opts = Options::builder().max_tokens(8192).build();
730 assert_eq!(opts.max_tokens, Some(8192));
731 }
732
733 #[test]
734 fn builder_summary_max_tokens_sets_field() {
735 let opts = Options::builder().summary_max_tokens(2048).build();
736 assert_eq!(opts.summary_max_tokens, Some(2048));
737 }
738
739 #[test]
740 fn builder_min_keep_messages_sets_field() {
741 let opts = Options::builder().min_keep_messages(6).build();
742 assert_eq!(opts.min_keep_messages, Some(6));
743 }
744
745 #[test]
746 fn builder_output_format_default_is_none() {
747 let opts = Options::builder().build();
748 assert!(opts.output_format.is_none());
749 }
750
751 #[test]
752 fn builder_output_format_sets_field() {
753 let schema = serde_json::json!({
754 "type": "object",
755 "properties": {
756 "name": { "type": "string" }
757 }
758 });
759 let opts = Options::builder().output_format(schema.clone()).build();
760 assert_eq!(opts.output_format, Some(schema));
761 }
762
763 #[test]
764 fn builder_output_format_with_other_options() {
765 let schema = serde_json::json!({"type": "object"});
766 let opts = Options::builder()
767 .output_format(schema.clone())
768 .max_turns(1)
769 .model("claude-haiku-4-5")
770 .build();
771 assert_eq!(opts.output_format, Some(schema));
772 assert_eq!(opts.max_turns, Some(1));
773 assert_eq!(opts.model.as_deref(), Some("claude-haiku-4-5"));
774 }
775
776 #[test]
777 fn builder_pre_compact_handler_default_is_none() {
778 let opts = Options::builder().build();
779 assert!(opts.pre_compact_handler.is_none());
780 }
781
782 #[test]
783 fn builder_pre_compact_handler_sets_field() {
784 let handler: PreCompactHandlerFn = Box::new(|_msgs| {
785 Box::pin(async {})
786 });
787 let opts = Options::builder()
788 .pre_compact_handler(handler)
789 .build();
790 assert!(opts.pre_compact_handler.is_some());
791 }
792
793 #[test]
794 fn builder_max_tool_result_bytes_sets_field() {
795 let opts = Options::builder().max_tool_result_bytes(100_000).build();
796 assert_eq!(opts.max_tool_result_bytes, Some(100_000));
797 }
798
799 #[test]
800 fn builder_max_tool_result_bytes_default_is_none() {
801 let opts = Options::builder().build();
802 assert!(opts.max_tool_result_bytes.is_none());
803 }
804
805 #[test]
806 fn builder_prune_threshold_pct_sets_field() {
807 let opts = Options::builder().prune_threshold_pct(80).build();
808 assert_eq!(opts.prune_threshold_pct, Some(80));
809 }
810
811 #[test]
812 fn builder_prune_threshold_pct_default_is_none() {
813 let opts = Options::builder().build();
814 assert!(opts.prune_threshold_pct.is_none());
815 }
816
817 #[test]
818 fn builder_prune_tool_result_max_chars_sets_field() {
819 let opts = Options::builder().prune_tool_result_max_chars(5000).build();
820 assert_eq!(opts.prune_tool_result_max_chars, Some(5000));
821 }
822
823 #[test]
824 fn builder_prune_tool_result_max_chars_default_is_none() {
825 let opts = Options::builder().build();
826 assert!(opts.prune_tool_result_max_chars.is_none());
827 }
828
829 #[test]
830 fn builder_sanitization_and_pruning_combined() {
831 let opts = Options::builder()
832 .max_tool_result_bytes(75_000)
833 .prune_threshold_pct(60)
834 .prune_tool_result_max_chars(3000)
835 .context_budget(200_000)
836 .build();
837 assert_eq!(opts.max_tool_result_bytes, Some(75_000));
838 assert_eq!(opts.prune_threshold_pct, Some(60));
839 assert_eq!(opts.prune_tool_result_max_chars, Some(3000));
840 assert_eq!(opts.context_budget, Some(200_000));
841 }
842}