Skip to main content

agent_sdk/
options.rs

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