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 pub output_format: Option<serde_json::Value>,
240
241 pub sandbox: Option<SandboxSettings>,
243
244 pub tool_config: Option<ToolConfig>,
246
247 pub plugins: Vec<PluginConfig>,
249
250 pub prompt_suggestions: bool,
252
253 pub external_tool_handler: Option<ExternalToolHandlerFn>,
258
259 pub custom_tool_definitions: Vec<CustomToolDefinition>,
264
265 pub followup_rx: Option<mpsc::UnboundedReceiver<String>>,
270
271 pub api_key: Option<String>,
273
274 pub attachments: Vec<QueryAttachment>,
276
277 pub provider: Option<Box<dyn LlmProvider>>,
279
280 pub pre_compact_handler: Option<PreCompactHandlerFn>,
283
284 pub max_tokens: Option<u32>,
286
287 pub summary_max_tokens: Option<u32>,
289
290 pub min_keep_messages: Option<usize>,
292
293 pub max_tool_result_bytes: Option<usize>,
297
298 pub prune_threshold_pct: Option<u8>,
301
302 pub prune_tool_result_max_chars: Option<usize>,
305}
306
307#[derive(Debug, Clone)]
309pub struct CustomToolDefinition {
310 pub name: String,
311 pub description: String,
312 pub input_schema: serde_json::Value,
313}
314
315#[derive(Debug, Clone)]
317pub struct QueryAttachment {
318 pub file_name: String,
320 pub mime_type: String,
322 pub base64_data: String,
324}
325
326pub type PreCompactHandlerFn = Box<
332 dyn Fn(Vec<crate::client::ApiMessage>) -> Pin<Box<dyn Future<Output = ()> + Send>>
333 + Send
334 + Sync,
335>;
336
337pub type ExternalToolHandlerFn = Box<
344 dyn Fn(String, serde_json::Value) -> Pin<Box<dyn Future<Output = Option<ToolResult>> + Send>>
345 + Send
346 + Sync,
347>;
348
349pub type CanUseToolFn = Box<
351 dyn Fn(
352 String,
353 serde_json::Value,
354 CanUseToolOptions,
355 ) -> std::pin::Pin<
356 Box<dyn std::future::Future<Output = crate::error::Result<PermissionResult>> + Send>,
357 > + Send
358 + Sync,
359>;
360
361impl Default for Options {
362 fn default() -> Self {
363 Self {
364 allowed_tools: Vec::new(),
365 disallowed_tools: Vec::new(),
366 permission_mode: PermissionMode::Default,
367 can_use_tool: None,
368 cwd: None,
369 model: None,
370 fallback_model: None,
371 effort: None,
372 max_turns: None,
373 max_budget_usd: None,
374 context_budget: None,
375 compaction_model: None,
376 compaction_provider: None,
377 system_prompt: None,
378 thinking: None,
379 hooks: HashMap::new(),
380 hook_dirs: Vec::new(),
381 mcp_servers: HashMap::new(),
382 agents: HashMap::new(),
383 continue_session: false,
384 resume: None,
385 fork_session: false,
386 session_id: None,
387 setting_sources: Vec::new(),
388 debug: false,
389 debug_file: None,
390 include_partial_messages: false,
391 persist_session: true,
392 enable_file_checkpointing: false,
393 env: HashMap::new(),
394 additional_directories: Vec::new(),
395 env_blocklist: Vec::new(),
396 output_format: None,
397 sandbox: None,
398 tool_config: None,
399 plugins: Vec::new(),
400 prompt_suggestions: false,
401 external_tool_handler: None,
402 custom_tool_definitions: Vec::new(),
403 followup_rx: None,
404 api_key: None,
405 attachments: Vec::new(),
406 provider: None,
407 pre_compact_handler: None,
408 max_tokens: None,
409 summary_max_tokens: None,
410 min_keep_messages: None,
411 max_tool_result_bytes: None,
412 prune_threshold_pct: None,
413 prune_tool_result_max_chars: None,
414 }
415 }
416}
417
418impl Options {
419 pub fn builder() -> OptionsBuilder {
421 OptionsBuilder::default()
422 }
423}
424
425#[derive(Default)]
427pub struct OptionsBuilder {
428 options: Options,
429}
430
431impl OptionsBuilder {
432 pub fn allowed_tools(mut self, tools: Vec<String>) -> Self {
433 self.options.allowed_tools = tools;
434 self
435 }
436
437 pub fn disallowed_tools(mut self, tools: Vec<String>) -> Self {
438 self.options.disallowed_tools = tools;
439 self
440 }
441
442 pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
443 self.options.permission_mode = mode;
444 self
445 }
446
447 pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
448 self.options.cwd = Some(cwd.into());
449 self
450 }
451
452 pub fn additional_directories(mut self, dirs: Vec<String>) -> Self {
453 self.options.additional_directories = dirs;
454 self
455 }
456
457 pub fn env_blocklist(mut self, keys: Vec<String>) -> Self {
458 self.options.env_blocklist = keys;
459 self
460 }
461
462 pub fn model(mut self, model: impl Into<String>) -> Self {
463 self.options.model = Some(model.into());
464 self
465 }
466
467 pub fn fallback_model(mut self, model: impl Into<String>) -> Self {
468 self.options.fallback_model = Some(model.into());
469 self
470 }
471
472 pub fn effort(mut self, effort: Effort) -> Self {
473 self.options.effort = Some(effort);
474 self
475 }
476
477 pub fn max_turns(mut self, max_turns: u32) -> Self {
478 self.options.max_turns = Some(max_turns);
479 self
480 }
481
482 pub fn max_budget_usd(mut self, budget: f64) -> Self {
483 self.options.max_budget_usd = Some(budget);
484 self
485 }
486
487 pub fn context_budget(mut self, budget: u64) -> Self {
488 self.options.context_budget = Some(budget);
489 self
490 }
491
492 pub fn compaction_model(mut self, model: impl Into<String>) -> Self {
493 self.options.compaction_model = Some(model.into());
494 self
495 }
496
497 pub fn compaction_provider(mut self, provider: Box<dyn LlmProvider>) -> Self {
498 self.options.compaction_provider = Some(provider);
499 self
500 }
501
502 pub fn system_prompt(mut self, prompt: SystemPrompt) -> Self {
503 self.options.system_prompt = Some(prompt);
504 self
505 }
506
507 pub fn thinking(mut self, config: ThinkingConfig) -> Self {
508 self.options.thinking = Some(config);
509 self
510 }
511
512 pub fn hook(mut self, event: HookEvent, matchers: Vec<HookCallbackMatcher>) -> Self {
513 self.options.hooks.insert(event, matchers);
514 self
515 }
516
517 pub fn hook_dirs(mut self, dirs: Vec<PathBuf>) -> Self {
518 self.options.hook_dirs = dirs;
519 self
520 }
521
522 pub fn mcp_server(mut self, name: impl Into<String>, config: McpServerConfig) -> Self {
523 self.options.mcp_servers.insert(name.into(), config);
524 self
525 }
526
527 pub fn agent(mut self, name: impl Into<String>, definition: AgentDefinition) -> Self {
528 self.options.agents.insert(name.into(), definition);
529 self
530 }
531
532 pub fn continue_session(mut self, value: bool) -> Self {
533 self.options.continue_session = value;
534 self
535 }
536
537 pub fn resume(mut self, session_id: impl Into<String>) -> Self {
538 self.options.resume = Some(session_id.into());
539 self
540 }
541
542 pub fn session_id(mut self, id: impl Into<String>) -> Self {
543 self.options.session_id = Some(id.into());
544 self
545 }
546
547 pub fn fork_session(mut self, value: bool) -> Self {
548 self.options.fork_session = value;
549 self
550 }
551
552 pub fn setting_sources(mut self, sources: Vec<SettingSource>) -> Self {
553 self.options.setting_sources = sources;
554 self
555 }
556
557 pub fn debug(mut self, value: bool) -> Self {
558 self.options.debug = value;
559 self
560 }
561
562 pub fn include_partial_messages(mut self, value: bool) -> Self {
563 self.options.include_partial_messages = value;
564 self
565 }
566
567 pub fn persist_session(mut self, value: bool) -> Self {
568 self.options.persist_session = value;
569 self
570 }
571
572 pub fn enable_file_checkpointing(mut self, value: bool) -> Self {
573 self.options.enable_file_checkpointing = value;
574 self
575 }
576
577 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
578 self.options.env.insert(key.into(), value.into());
579 self
580 }
581
582 pub fn output_format(mut self, schema: serde_json::Value) -> Self {
583 self.options.output_format = Some(schema);
584 self
585 }
586
587 pub fn sandbox(mut self, settings: SandboxSettings) -> Self {
588 self.options.sandbox = Some(settings);
589 self
590 }
591
592 pub fn external_tool_handler(mut self, handler: ExternalToolHandlerFn) -> Self {
593 self.options.external_tool_handler = Some(handler);
594 self
595 }
596
597 pub fn custom_tool(mut self, def: CustomToolDefinition) -> Self {
598 self.options.custom_tool_definitions.push(def);
599 self
600 }
601
602 pub fn custom_tools(mut self, defs: Vec<CustomToolDefinition>) -> Self {
603 self.options.custom_tool_definitions.extend(defs);
604 self
605 }
606
607 pub fn followup_rx(mut self, rx: mpsc::UnboundedReceiver<String>) -> Self {
608 self.options.followup_rx = Some(rx);
609 self
610 }
611
612 pub fn api_key(mut self, key: impl Into<String>) -> Self {
613 self.options.api_key = Some(key.into());
614 self
615 }
616
617 pub fn attachments(mut self, attachments: Vec<QueryAttachment>) -> Self {
618 self.options.attachments = attachments;
619 self
620 }
621
622 pub fn provider(mut self, provider: Box<dyn LlmProvider>) -> Self {
623 self.options.provider = Some(provider);
624 self
625 }
626
627 pub fn pre_compact_handler(mut self, handler: PreCompactHandlerFn) -> Self {
628 self.options.pre_compact_handler = Some(handler);
629 self
630 }
631
632 pub fn max_tokens(mut self, max_tokens: u32) -> Self {
633 self.options.max_tokens = Some(max_tokens);
634 self
635 }
636
637 pub fn summary_max_tokens(mut self, tokens: u32) -> Self {
638 self.options.summary_max_tokens = Some(tokens);
639 self
640 }
641
642 pub fn min_keep_messages(mut self, count: usize) -> Self {
643 self.options.min_keep_messages = Some(count);
644 self
645 }
646
647 pub fn max_tool_result_bytes(mut self, bytes: usize) -> Self {
648 self.options.max_tool_result_bytes = Some(bytes);
649 self
650 }
651
652 pub fn prune_threshold_pct(mut self, pct: u8) -> Self {
653 self.options.prune_threshold_pct = Some(pct);
654 self
655 }
656
657 pub fn prune_tool_result_max_chars(mut self, chars: usize) -> Self {
658 self.options.prune_tool_result_max_chars = Some(chars);
659 self
660 }
661
662 pub fn build(self) -> Options {
663 self.options
664 }
665}
666
667impl std::fmt::Debug for Options {
668 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
669 f.debug_struct("Options")
670 .field("allowed_tools", &self.allowed_tools)
671 .field("disallowed_tools", &self.disallowed_tools)
672 .field("permission_mode", &self.permission_mode)
673 .field("cwd", &self.cwd)
674 .field("model", &self.model)
675 .field("effort", &self.effort)
676 .field("max_turns", &self.max_turns)
677 .field("max_budget_usd", &self.max_budget_usd)
678 .field("context_budget", &self.context_budget)
679 .field("compaction_model", &self.compaction_model)
680 .field("hooks_count", &self.hooks.len())
681 .field("mcp_servers_count", &self.mcp_servers.len())
682 .field("agents_count", &self.agents.len())
683 .field("continue_session", &self.continue_session)
684 .field("resume", &self.resume)
685 .field("persist_session", &self.persist_session)
686 .finish()
687 }
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693
694 #[test]
695 fn builder_api_key_sets_field() {
696 let opts = Options::builder().api_key("sk-ant-test-key").build();
697 assert_eq!(opts.api_key.as_deref(), Some("sk-ant-test-key"));
698 }
699
700 #[test]
701 fn builder_api_key_default_is_none() {
702 let opts = Options::builder().build();
703 assert!(opts.api_key.is_none());
704 }
705
706 #[test]
707 fn builder_api_key_with_other_options() {
708 let opts = Options::builder()
709 .model("claude-haiku-4-5")
710 .api_key("sk-ant-combined")
711 .max_turns(10)
712 .build();
713 assert_eq!(opts.api_key.as_deref(), Some("sk-ant-combined"));
714 assert_eq!(opts.model.as_deref(), Some("claude-haiku-4-5"));
715 assert_eq!(opts.max_turns, Some(10));
716 }
717
718 #[test]
719 fn builder_max_tokens_sets_field() {
720 let opts = Options::builder().max_tokens(8192).build();
721 assert_eq!(opts.max_tokens, Some(8192));
722 }
723
724 #[test]
725 fn builder_summary_max_tokens_sets_field() {
726 let opts = Options::builder().summary_max_tokens(2048).build();
727 assert_eq!(opts.summary_max_tokens, Some(2048));
728 }
729
730 #[test]
731 fn builder_min_keep_messages_sets_field() {
732 let opts = Options::builder().min_keep_messages(6).build();
733 assert_eq!(opts.min_keep_messages, Some(6));
734 }
735
736 #[test]
737 fn builder_output_format_default_is_none() {
738 let opts = Options::builder().build();
739 assert!(opts.output_format.is_none());
740 }
741
742 #[test]
743 fn builder_output_format_sets_field() {
744 let schema = serde_json::json!({
745 "type": "object",
746 "properties": {
747 "name": { "type": "string" }
748 }
749 });
750 let opts = Options::builder().output_format(schema.clone()).build();
751 assert_eq!(opts.output_format, Some(schema));
752 }
753
754 #[test]
755 fn builder_output_format_with_other_options() {
756 let schema = serde_json::json!({"type": "object"});
757 let opts = Options::builder()
758 .output_format(schema.clone())
759 .max_turns(1)
760 .model("claude-haiku-4-5")
761 .build();
762 assert_eq!(opts.output_format, Some(schema));
763 assert_eq!(opts.max_turns, Some(1));
764 assert_eq!(opts.model.as_deref(), Some("claude-haiku-4-5"));
765 }
766
767 #[test]
768 fn builder_pre_compact_handler_default_is_none() {
769 let opts = Options::builder().build();
770 assert!(opts.pre_compact_handler.is_none());
771 }
772
773 #[test]
774 fn builder_pre_compact_handler_sets_field() {
775 let handler: PreCompactHandlerFn = Box::new(|_msgs| Box::pin(async {}));
776 let opts = Options::builder().pre_compact_handler(handler).build();
777 assert!(opts.pre_compact_handler.is_some());
778 }
779
780 #[test]
781 fn builder_max_tool_result_bytes_sets_field() {
782 let opts = Options::builder().max_tool_result_bytes(100_000).build();
783 assert_eq!(opts.max_tool_result_bytes, Some(100_000));
784 }
785
786 #[test]
787 fn builder_max_tool_result_bytes_default_is_none() {
788 let opts = Options::builder().build();
789 assert!(opts.max_tool_result_bytes.is_none());
790 }
791
792 #[test]
793 fn builder_prune_threshold_pct_sets_field() {
794 let opts = Options::builder().prune_threshold_pct(80).build();
795 assert_eq!(opts.prune_threshold_pct, Some(80));
796 }
797
798 #[test]
799 fn builder_prune_threshold_pct_default_is_none() {
800 let opts = Options::builder().build();
801 assert!(opts.prune_threshold_pct.is_none());
802 }
803
804 #[test]
805 fn builder_prune_tool_result_max_chars_sets_field() {
806 let opts = Options::builder().prune_tool_result_max_chars(5000).build();
807 assert_eq!(opts.prune_tool_result_max_chars, Some(5000));
808 }
809
810 #[test]
811 fn builder_prune_tool_result_max_chars_default_is_none() {
812 let opts = Options::builder().build();
813 assert!(opts.prune_tool_result_max_chars.is_none());
814 }
815
816 #[test]
817 fn builder_sanitization_and_pruning_combined() {
818 let opts = Options::builder()
819 .max_tool_result_bytes(75_000)
820 .prune_threshold_pct(60)
821 .prune_tool_result_max_chars(3000)
822 .context_budget(200_000)
823 .build();
824 assert_eq!(opts.max_tool_result_bytes, Some(75_000));
825 assert_eq!(opts.prune_threshold_pct, Some(60));
826 assert_eq!(opts.prune_tool_result_max_chars, Some(3000));
827 assert_eq!(opts.context_budget, Some(200_000));
828 }
829}