Skip to main content

agent_sdk/
options.rs

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/// Permission mode controls how Claude uses tools.
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "camelCase")]
18pub enum PermissionMode {
19    /// Standard permission behavior - unmatched tools trigger `can_use_tool`.
20    Default,
21    /// Auto-accept file edits.
22    AcceptEdits,
23    /// Bypass all permission checks (use with caution).
24    BypassPermissions,
25    /// Planning mode - no tool execution.
26    Plan,
27    /// Don't prompt for permissions, deny if not pre-approved.
28    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/// Effort level controlling how much reasoning Claude applies.
44#[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/// Setting sources to load from filesystem.
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
55#[serde(rename_all = "lowercase")]
56pub enum SettingSource {
57    /// Global user settings (~/.claude/settings.json).
58    User,
59    /// Shared project settings (.claude/settings.json).
60    Project,
61    /// Local project settings (.claude/settings.local.json).
62    Local,
63}
64
65/// Thinking configuration for Claude's reasoning behavior.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(tag = "type")]
68pub enum ThinkingConfig {
69    /// Adaptive thinking - Claude decides when to think.
70    #[serde(rename = "adaptive")]
71    Adaptive,
72    /// Disabled thinking.
73    #[serde(rename = "disabled")]
74    Disabled,
75    /// Enabled with a specific budget.
76    #[serde(rename = "enabled")]
77    Enabled {
78        budget_tokens: u64,
79    },
80}
81
82/// System prompt configuration.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(untagged)]
85pub enum SystemPrompt {
86    /// Custom system prompt string.
87    Custom(String),
88    /// Use Claude Code's built-in system prompt.
89    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/// Sandbox settings for tool execution.
99#[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/// Configuration for the AskUserQuestion tool.
108#[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/// Plugin configuration.
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct PluginConfig {
130    #[serde(rename = "type")]
131    pub plugin_type: String,
132    pub path: String,
133}
134
135/// Configuration options for a query.
136pub struct Options {
137    /// Tools to auto-approve without prompting.
138    pub allowed_tools: Vec<String>,
139
140    /// Tools to always deny.
141    pub disallowed_tools: Vec<String>,
142
143    /// Permission mode for the session.
144    pub permission_mode: PermissionMode,
145
146    /// Custom permission function for tool usage.
147    pub can_use_tool: Option<CanUseToolFn>,
148
149    /// Current working directory.
150    pub cwd: Option<String>,
151
152    /// Claude model to use.
153    pub model: Option<String>,
154
155    /// Fallback model if primary fails.
156    pub fallback_model: Option<String>,
157
158    /// Controls reasoning depth.
159    pub effort: Option<Effort>,
160
161    /// Maximum agentic turns (tool-use round trips).
162    pub max_turns: Option<u32>,
163
164    /// Maximum budget in USD.
165    pub max_budget_usd: Option<f64>,
166
167    /// Context budget in tokens. When input_tokens from the API response
168    /// exceeds this threshold, older messages are compacted into a summary.
169    /// Recommended: ~80% of the model's context window (e.g. 160_000 for 200k).
170    pub context_budget: Option<u64>,
171
172    /// Model to use for generating compaction summaries.
173    /// Falls back to the primary model if the compaction call fails.
174    pub compaction_model: Option<String>,
175
176    /// Optional separate LLM provider for compaction (e.g. use Anthropic for compaction
177    /// while the primary provider is OpenAI). If `None`, the primary provider is used.
178    pub compaction_provider: Option<Box<dyn LlmProvider>>,
179
180    /// System prompt configuration.
181    pub system_prompt: Option<SystemPrompt>,
182
183    /// Thinking configuration.
184    pub thinking: Option<ThinkingConfig>,
185
186    /// Hook callbacks for events.
187    pub hooks: HashMap<HookEvent, Vec<HookCallbackMatcher>>,
188
189    /// Directories to scan for HOOK.md-based hooks. Discovered hooks are
190    /// merged into the programmatic `hooks` map at query time.
191    pub hook_dirs: Vec<PathBuf>,
192
193    /// MCP server configurations.
194    pub mcp_servers: HashMap<String, McpServerConfig>,
195
196    /// Programmatically defined subagents.
197    pub agents: HashMap<String, AgentDefinition>,
198
199    /// Continue the most recent conversation.
200    pub continue_session: bool,
201
202    /// Session ID to resume.
203    pub resume: Option<String>,
204
205    /// Fork session when resuming.
206    pub fork_session: bool,
207
208    /// Use a specific UUID for the session.
209    pub session_id: Option<String>,
210
211    /// Control which filesystem settings to load.
212    pub setting_sources: Vec<SettingSource>,
213
214    /// Enable debug mode.
215    pub debug: bool,
216
217    /// Write debug logs to a specific file.
218    pub debug_file: Option<String>,
219
220    /// Include partial message events.
221    pub include_partial_messages: bool,
222
223    /// When false, disables session persistence to disk.
224    pub persist_session: bool,
225
226    /// Enable file change tracking for rewinding.
227    pub enable_file_checkpointing: bool,
228
229    /// Environment variables.
230    pub env: HashMap<String, String>,
231
232    /// Additional directories Claude can access.
233    pub additional_directories: Vec<String>,
234
235    /// Environment variable names to strip from child processes (e.g. Bash).
236    /// These keys will be removed via `env_remove` when spawning subprocesses,
237    /// preventing the agent from reading them through shell commands.
238    pub env_blocklist: Vec<String>,
239
240    /// Structured output schema.
241    pub output_format: Option<serde_json::Value>,
242
243    /// Sandbox settings.
244    pub sandbox: Option<SandboxSettings>,
245
246    /// Tool configuration.
247    pub tool_config: Option<ToolConfig>,
248
249    /// Plugin configurations.
250    pub plugins: Vec<PluginConfig>,
251
252    /// Enable prompt suggestions.
253    pub prompt_suggestions: bool,
254
255    /// External tool handler for custom tools.
256    ///
257    /// Called before the built-in executor. If it returns `Some(ToolResult)`,
258    /// the built-in executor is skipped for that tool call.
259    pub external_tool_handler: Option<ExternalToolHandlerFn>,
260
261    /// Custom tool definitions (JSON schemas) sent to the API alongside built-in tools.
262    ///
263    /// These are typically used with `external_tool_handler` to register and handle
264    /// tools that aren't part of the built-in set (e.g. MemorySearch, VaultGet).
265    pub custom_tool_definitions: Vec<CustomToolDefinition>,
266
267    /// Receiver for followup messages injected during an active agent loop.
268    ///
269    /// At each iteration boundary (before calling the API), the loop drains
270    /// all pending messages from this channel and appends them as user messages.
271    pub followup_rx: Option<mpsc::UnboundedReceiver<String>>,
272
273    /// Explicit API key. When set, bypasses the `ANTHROPIC_API_KEY` env var lookup.
274    pub api_key: Option<String>,
275
276    /// File attachments to include in the first user message (images, PDFs, etc.).
277    pub attachments: Vec<QueryAttachment>,
278
279    /// LLM provider to use. If `None`, defaults to `AnthropicProvider::from_env()`.
280    pub provider: Option<Box<dyn LlmProvider>>,
281
282    /// Pre-compaction handler: called with messages about to be discarded
283    /// during conversation compaction, allowing the host to persist key facts.
284    pub pre_compact_handler: Option<PreCompactHandlerFn>,
285
286    /// Maximum tokens for LLM API responses. Overrides `DEFAULT_MAX_TOKENS`.
287    pub max_tokens: Option<u32>,
288
289    /// Max tokens for the compaction summary response.
290    pub summary_max_tokens: Option<u32>,
291
292    /// Minimum number of messages to keep at the end during compaction.
293    pub min_keep_messages: Option<usize>,
294
295    /// Maximum size in bytes for any single tool result. Results exceeding this
296    /// limit are truncated. Also strips base64 data URIs and hex blobs.
297    /// Defaults to [`sanitize::DEFAULT_MAX_TOOL_RESULT_BYTES`] (50 000) when `None`.
298    pub max_tool_result_bytes: Option<usize>,
299
300    /// Percentage of context_budget at which lightweight tool-result pruning triggers.
301    /// Defaults to [`compact::DEFAULT_PRUNE_THRESHOLD_PCT`] (70) when `None`.
302    pub prune_threshold_pct: Option<u8>,
303
304    /// Tool results longer than this (in chars) are candidates for pruning.
305    /// Defaults to [`compact::DEFAULT_PRUNE_TOOL_RESULT_MAX_CHARS`] (2 000) when `None`.
306    pub prune_tool_result_max_chars: Option<usize>,
307}
308
309/// A custom tool definition to send to the Claude API.
310#[derive(Debug, Clone)]
311pub struct CustomToolDefinition {
312    pub name: String,
313    pub description: String,
314    pub input_schema: serde_json::Value,
315}
316
317/// An attachment to include in the first user message sent to the API.
318#[derive(Debug, Clone)]
319pub struct QueryAttachment {
320    /// Original filename.
321    pub file_name: String,
322    /// MIME type (e.g. "image/png").
323    pub mime_type: String,
324    /// Base64-encoded data.
325    pub base64_data: String,
326}
327
328/// Type alias for the pre-compaction handler.
329///
330/// Called just before conversation messages are compacted (summarized).
331/// Receives the messages about to be discarded, allowing the host to
332/// extract and persist important information before it's lost.
333pub type PreCompactHandlerFn = Box<
334    dyn Fn(
335            Vec<crate::client::ApiMessage>,
336        ) -> Pin<Box<dyn Future<Output = ()> + Send>>
337        + Send
338        + Sync,
339>;
340
341/// Type alias for external tool handler callback.
342///
343/// When set, this handler is called before the built-in tool executor. If it returns
344/// `Some(ToolResult)`, the built-in executor is skipped for that tool call.
345/// This allows embedding custom tools (e.g. MemorySearch, VaultGet) alongside
346/// the built-in tools (Read, Write, Bash, etc.).
347pub 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
356/// Type alias for the can_use_tool callback.
357pub 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    /// Create a new Options builder.
427    pub fn builder() -> OptionsBuilder {
428        OptionsBuilder::default()
429    }
430}
431
432/// Builder for constructing Options.
433#[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}