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    /// Optional pre-exec hook for subprocesses (Unix only).
239    /// Called in the child process after `fork()` but before `execve()`.
240    /// Used for network namespace isolation (secret proxy Phase 4).
241    #[cfg(unix)]
242    pub pre_exec_fn: Option<Box<dyn Fn() -> std::io::Result<()> + Send + Sync>>,
243
244    /// Structured output schema.
245    pub output_format: Option<serde_json::Value>,
246
247    /// Sandbox settings.
248    pub sandbox: Option<SandboxSettings>,
249
250    /// Tool configuration.
251    pub tool_config: Option<ToolConfig>,
252
253    /// Plugin configurations.
254    pub plugins: Vec<PluginConfig>,
255
256    /// Enable prompt suggestions.
257    pub prompt_suggestions: bool,
258
259    /// External tool handler for custom tools.
260    ///
261    /// Called before the built-in executor. If it returns `Some(ToolResult)`,
262    /// the built-in executor is skipped for that tool call.
263    pub external_tool_handler: Option<ExternalToolHandlerFn>,
264
265    /// Custom tool definitions (JSON schemas) sent to the API alongside built-in tools.
266    ///
267    /// These are typically used with `external_tool_handler` to register and handle
268    /// tools that aren't part of the built-in set (e.g. MemorySearch, VaultGet).
269    pub custom_tool_definitions: Vec<CustomToolDefinition>,
270
271    /// Receiver for followup messages injected during an active agent loop.
272    ///
273    /// At each iteration boundary (before calling the API), the loop drains
274    /// all pending messages from this channel and appends them as user messages.
275    pub followup_rx: Option<mpsc::UnboundedReceiver<String>>,
276
277    /// Explicit API key. When set, bypasses the `ANTHROPIC_API_KEY` env var lookup.
278    pub api_key: Option<String>,
279
280    /// File attachments to include in the first user message (images, PDFs, etc.).
281    pub attachments: Vec<QueryAttachment>,
282
283    /// LLM provider to use. If `None`, defaults to `AnthropicProvider::from_env()`.
284    pub provider: Option<Box<dyn LlmProvider>>,
285
286    /// Pre-compaction handler: called with messages about to be discarded
287    /// during conversation compaction, allowing the host to persist key facts.
288    pub pre_compact_handler: Option<PreCompactHandlerFn>,
289
290    /// Maximum tokens for LLM API responses. Overrides `DEFAULT_MAX_TOKENS`.
291    pub max_tokens: Option<u32>,
292
293    /// Max tokens for the compaction summary response.
294    pub summary_max_tokens: Option<u32>,
295
296    /// Minimum number of messages to keep at the end during compaction.
297    pub min_keep_messages: Option<usize>,
298
299    /// Maximum size in bytes for any single tool result. Results exceeding this
300    /// limit are truncated. Also strips base64 data URIs and hex blobs.
301    /// Defaults to [`sanitize::DEFAULT_MAX_TOOL_RESULT_BYTES`] (50 000) when `None`.
302    pub max_tool_result_bytes: Option<usize>,
303
304    /// Percentage of context_budget at which lightweight tool-result pruning triggers.
305    /// Defaults to [`compact::DEFAULT_PRUNE_THRESHOLD_PCT`] (70) when `None`.
306    pub prune_threshold_pct: Option<u8>,
307
308    /// Tool results longer than this (in chars) are candidates for pruning.
309    /// Defaults to [`compact::DEFAULT_PRUNE_TOOL_RESULT_MAX_CHARS`] (2 000) when `None`.
310    pub prune_tool_result_max_chars: Option<usize>,
311}
312
313/// A custom tool definition to send to the Claude API.
314#[derive(Debug, Clone)]
315pub struct CustomToolDefinition {
316    pub name: String,
317    pub description: String,
318    pub input_schema: serde_json::Value,
319}
320
321/// An attachment to include in the first user message sent to the API.
322#[derive(Debug, Clone)]
323pub struct QueryAttachment {
324    /// Original filename.
325    pub file_name: String,
326    /// MIME type (e.g. "image/png").
327    pub mime_type: String,
328    /// Base64-encoded data.
329    pub base64_data: String,
330}
331
332/// Type alias for the pre-compaction handler.
333///
334/// Called just before conversation messages are compacted (summarized).
335/// Receives the messages about to be discarded, allowing the host to
336/// extract and persist important information before it's lost.
337pub type PreCompactHandlerFn = Box<
338    dyn Fn(Vec<crate::client::ApiMessage>) -> Pin<Box<dyn Future<Output = ()> + Send>>
339        + Send
340        + Sync,
341>;
342
343/// Type alias for external tool handler callback.
344///
345/// When set, this handler is called before the built-in tool executor. If it returns
346/// `Some(ToolResult)`, the built-in executor is skipped for that tool call.
347/// This allows embedding custom tools (e.g. MemorySearch, VaultGet) alongside
348/// the built-in tools (Read, Write, Bash, etc.).
349pub type ExternalToolHandlerFn = Box<
350    dyn Fn(String, serde_json::Value) -> Pin<Box<dyn Future<Output = Option<ToolResult>> + Send>>
351        + Send
352        + Sync,
353>;
354
355/// Type alias for the can_use_tool callback.
356pub type CanUseToolFn = Box<
357    dyn Fn(
358            String,
359            serde_json::Value,
360            CanUseToolOptions,
361        ) -> std::pin::Pin<
362            Box<dyn std::future::Future<Output = crate::error::Result<PermissionResult>> + Send>,
363        > + Send
364        + Sync,
365>;
366
367impl Default for Options {
368    fn default() -> Self {
369        Self {
370            allowed_tools: Vec::new(),
371            disallowed_tools: Vec::new(),
372            permission_mode: PermissionMode::Default,
373            can_use_tool: None,
374            cwd: None,
375            model: None,
376            fallback_model: None,
377            effort: None,
378            max_turns: None,
379            max_budget_usd: None,
380            context_budget: None,
381            compaction_model: None,
382            compaction_provider: None,
383            system_prompt: None,
384            thinking: None,
385            hooks: HashMap::new(),
386            hook_dirs: Vec::new(),
387            mcp_servers: HashMap::new(),
388            agents: HashMap::new(),
389            continue_session: false,
390            resume: None,
391            fork_session: false,
392            session_id: None,
393            setting_sources: Vec::new(),
394            debug: false,
395            debug_file: None,
396            include_partial_messages: false,
397            persist_session: true,
398            enable_file_checkpointing: false,
399            env: HashMap::new(),
400            additional_directories: Vec::new(),
401            env_blocklist: Vec::new(),
402            #[cfg(unix)]
403            pre_exec_fn: None,
404            output_format: None,
405            sandbox: None,
406            tool_config: None,
407            plugins: Vec::new(),
408            prompt_suggestions: false,
409            external_tool_handler: None,
410            custom_tool_definitions: Vec::new(),
411            followup_rx: None,
412            api_key: None,
413            attachments: Vec::new(),
414            provider: None,
415            pre_compact_handler: None,
416            max_tokens: None,
417            summary_max_tokens: None,
418            min_keep_messages: None,
419            max_tool_result_bytes: None,
420            prune_threshold_pct: None,
421            prune_tool_result_max_chars: None,
422        }
423    }
424}
425
426impl Options {
427    /// Create a new Options builder.
428    pub fn builder() -> OptionsBuilder {
429        OptionsBuilder::default()
430    }
431}
432
433/// Builder for constructing Options.
434#[derive(Default)]
435pub struct OptionsBuilder {
436    options: Options,
437}
438
439impl OptionsBuilder {
440    pub fn allowed_tools(mut self, tools: Vec<String>) -> Self {
441        self.options.allowed_tools = tools;
442        self
443    }
444
445    pub fn disallowed_tools(mut self, tools: Vec<String>) -> Self {
446        self.options.disallowed_tools = tools;
447        self
448    }
449
450    pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
451        self.options.permission_mode = mode;
452        self
453    }
454
455    pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
456        self.options.cwd = Some(cwd.into());
457        self
458    }
459
460    pub fn additional_directories(mut self, dirs: Vec<String>) -> Self {
461        self.options.additional_directories = dirs;
462        self
463    }
464
465    pub fn env_blocklist(mut self, keys: Vec<String>) -> Self {
466        self.options.env_blocklist = keys;
467        self
468    }
469
470    /// Set a pre-exec hook for subprocesses (Unix only).
471    /// Called in the child after fork, before exec. Used for netns isolation.
472    #[cfg(unix)]
473    pub fn pre_exec_fn(mut self, f: Box<dyn Fn() -> std::io::Result<()> + Send + Sync>) -> Self {
474        self.options.pre_exec_fn = Some(f);
475        self
476    }
477
478    pub fn model(mut self, model: impl Into<String>) -> Self {
479        self.options.model = Some(model.into());
480        self
481    }
482
483    pub fn fallback_model(mut self, model: impl Into<String>) -> Self {
484        self.options.fallback_model = Some(model.into());
485        self
486    }
487
488    pub fn effort(mut self, effort: Effort) -> Self {
489        self.options.effort = Some(effort);
490        self
491    }
492
493    pub fn max_turns(mut self, max_turns: u32) -> Self {
494        self.options.max_turns = Some(max_turns);
495        self
496    }
497
498    pub fn max_budget_usd(mut self, budget: f64) -> Self {
499        self.options.max_budget_usd = Some(budget);
500        self
501    }
502
503    pub fn context_budget(mut self, budget: u64) -> Self {
504        self.options.context_budget = Some(budget);
505        self
506    }
507
508    pub fn compaction_model(mut self, model: impl Into<String>) -> Self {
509        self.options.compaction_model = Some(model.into());
510        self
511    }
512
513    pub fn compaction_provider(mut self, provider: Box<dyn LlmProvider>) -> Self {
514        self.options.compaction_provider = Some(provider);
515        self
516    }
517
518    pub fn system_prompt(mut self, prompt: SystemPrompt) -> Self {
519        self.options.system_prompt = Some(prompt);
520        self
521    }
522
523    pub fn thinking(mut self, config: ThinkingConfig) -> Self {
524        self.options.thinking = Some(config);
525        self
526    }
527
528    pub fn hook(mut self, event: HookEvent, matchers: Vec<HookCallbackMatcher>) -> Self {
529        self.options.hooks.insert(event, matchers);
530        self
531    }
532
533    pub fn hook_dirs(mut self, dirs: Vec<PathBuf>) -> Self {
534        self.options.hook_dirs = dirs;
535        self
536    }
537
538    pub fn mcp_server(mut self, name: impl Into<String>, config: McpServerConfig) -> Self {
539        self.options.mcp_servers.insert(name.into(), config);
540        self
541    }
542
543    pub fn agent(mut self, name: impl Into<String>, definition: AgentDefinition) -> Self {
544        self.options.agents.insert(name.into(), definition);
545        self
546    }
547
548    pub fn continue_session(mut self, value: bool) -> Self {
549        self.options.continue_session = value;
550        self
551    }
552
553    pub fn resume(mut self, session_id: impl Into<String>) -> Self {
554        self.options.resume = Some(session_id.into());
555        self
556    }
557
558    pub fn session_id(mut self, id: impl Into<String>) -> Self {
559        self.options.session_id = Some(id.into());
560        self
561    }
562
563    pub fn fork_session(mut self, value: bool) -> Self {
564        self.options.fork_session = value;
565        self
566    }
567
568    pub fn setting_sources(mut self, sources: Vec<SettingSource>) -> Self {
569        self.options.setting_sources = sources;
570        self
571    }
572
573    pub fn debug(mut self, value: bool) -> Self {
574        self.options.debug = value;
575        self
576    }
577
578    pub fn include_partial_messages(mut self, value: bool) -> Self {
579        self.options.include_partial_messages = value;
580        self
581    }
582
583    pub fn persist_session(mut self, value: bool) -> Self {
584        self.options.persist_session = value;
585        self
586    }
587
588    pub fn enable_file_checkpointing(mut self, value: bool) -> Self {
589        self.options.enable_file_checkpointing = value;
590        self
591    }
592
593    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
594        self.options.env.insert(key.into(), value.into());
595        self
596    }
597
598    pub fn output_format(mut self, schema: serde_json::Value) -> Self {
599        self.options.output_format = Some(schema);
600        self
601    }
602
603    pub fn sandbox(mut self, settings: SandboxSettings) -> Self {
604        self.options.sandbox = Some(settings);
605        self
606    }
607
608    pub fn external_tool_handler(mut self, handler: ExternalToolHandlerFn) -> Self {
609        self.options.external_tool_handler = Some(handler);
610        self
611    }
612
613    pub fn custom_tool(mut self, def: CustomToolDefinition) -> Self {
614        self.options.custom_tool_definitions.push(def);
615        self
616    }
617
618    pub fn custom_tools(mut self, defs: Vec<CustomToolDefinition>) -> Self {
619        self.options.custom_tool_definitions.extend(defs);
620        self
621    }
622
623    pub fn followup_rx(mut self, rx: mpsc::UnboundedReceiver<String>) -> Self {
624        self.options.followup_rx = Some(rx);
625        self
626    }
627
628    pub fn api_key(mut self, key: impl Into<String>) -> Self {
629        self.options.api_key = Some(key.into());
630        self
631    }
632
633    pub fn attachments(mut self, attachments: Vec<QueryAttachment>) -> Self {
634        self.options.attachments = attachments;
635        self
636    }
637
638    pub fn provider(mut self, provider: Box<dyn LlmProvider>) -> Self {
639        self.options.provider = Some(provider);
640        self
641    }
642
643    pub fn pre_compact_handler(mut self, handler: PreCompactHandlerFn) -> Self {
644        self.options.pre_compact_handler = Some(handler);
645        self
646    }
647
648    pub fn max_tokens(mut self, max_tokens: u32) -> Self {
649        self.options.max_tokens = Some(max_tokens);
650        self
651    }
652
653    pub fn summary_max_tokens(mut self, tokens: u32) -> Self {
654        self.options.summary_max_tokens = Some(tokens);
655        self
656    }
657
658    pub fn min_keep_messages(mut self, count: usize) -> Self {
659        self.options.min_keep_messages = Some(count);
660        self
661    }
662
663    pub fn max_tool_result_bytes(mut self, bytes: usize) -> Self {
664        self.options.max_tool_result_bytes = Some(bytes);
665        self
666    }
667
668    pub fn prune_threshold_pct(mut self, pct: u8) -> Self {
669        self.options.prune_threshold_pct = Some(pct);
670        self
671    }
672
673    pub fn prune_tool_result_max_chars(mut self, chars: usize) -> Self {
674        self.options.prune_tool_result_max_chars = Some(chars);
675        self
676    }
677
678    pub fn build(self) -> Options {
679        self.options
680    }
681}
682
683impl std::fmt::Debug for Options {
684    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685        f.debug_struct("Options")
686            .field("allowed_tools", &self.allowed_tools)
687            .field("disallowed_tools", &self.disallowed_tools)
688            .field("permission_mode", &self.permission_mode)
689            .field("cwd", &self.cwd)
690            .field("model", &self.model)
691            .field("effort", &self.effort)
692            .field("max_turns", &self.max_turns)
693            .field("max_budget_usd", &self.max_budget_usd)
694            .field("context_budget", &self.context_budget)
695            .field("compaction_model", &self.compaction_model)
696            .field("hooks_count", &self.hooks.len())
697            .field("mcp_servers_count", &self.mcp_servers.len())
698            .field("agents_count", &self.agents.len())
699            .field("continue_session", &self.continue_session)
700            .field("resume", &self.resume)
701            .field("persist_session", &self.persist_session)
702            .finish()
703    }
704}
705
706#[cfg(test)]
707mod tests {
708    use super::*;
709
710    #[test]
711    fn builder_api_key_sets_field() {
712        let opts = Options::builder().api_key("sk-ant-test-key").build();
713        assert_eq!(opts.api_key.as_deref(), Some("sk-ant-test-key"));
714    }
715
716    #[test]
717    fn builder_api_key_default_is_none() {
718        let opts = Options::builder().build();
719        assert!(opts.api_key.is_none());
720    }
721
722    #[test]
723    fn builder_api_key_with_other_options() {
724        let opts = Options::builder()
725            .model("claude-haiku-4-5")
726            .api_key("sk-ant-combined")
727            .max_turns(10)
728            .build();
729        assert_eq!(opts.api_key.as_deref(), Some("sk-ant-combined"));
730        assert_eq!(opts.model.as_deref(), Some("claude-haiku-4-5"));
731        assert_eq!(opts.max_turns, Some(10));
732    }
733
734    #[test]
735    fn builder_max_tokens_sets_field() {
736        let opts = Options::builder().max_tokens(8192).build();
737        assert_eq!(opts.max_tokens, Some(8192));
738    }
739
740    #[test]
741    fn builder_summary_max_tokens_sets_field() {
742        let opts = Options::builder().summary_max_tokens(2048).build();
743        assert_eq!(opts.summary_max_tokens, Some(2048));
744    }
745
746    #[test]
747    fn builder_min_keep_messages_sets_field() {
748        let opts = Options::builder().min_keep_messages(6).build();
749        assert_eq!(opts.min_keep_messages, Some(6));
750    }
751
752    #[test]
753    fn builder_output_format_default_is_none() {
754        let opts = Options::builder().build();
755        assert!(opts.output_format.is_none());
756    }
757
758    #[test]
759    fn builder_output_format_sets_field() {
760        let schema = serde_json::json!({
761            "type": "object",
762            "properties": {
763                "name": { "type": "string" }
764            }
765        });
766        let opts = Options::builder().output_format(schema.clone()).build();
767        assert_eq!(opts.output_format, Some(schema));
768    }
769
770    #[test]
771    fn builder_output_format_with_other_options() {
772        let schema = serde_json::json!({"type": "object"});
773        let opts = Options::builder()
774            .output_format(schema.clone())
775            .max_turns(1)
776            .model("claude-haiku-4-5")
777            .build();
778        assert_eq!(opts.output_format, Some(schema));
779        assert_eq!(opts.max_turns, Some(1));
780        assert_eq!(opts.model.as_deref(), Some("claude-haiku-4-5"));
781    }
782
783    #[test]
784    fn builder_pre_compact_handler_default_is_none() {
785        let opts = Options::builder().build();
786        assert!(opts.pre_compact_handler.is_none());
787    }
788
789    #[test]
790    fn builder_pre_compact_handler_sets_field() {
791        let handler: PreCompactHandlerFn = Box::new(|_msgs| Box::pin(async {}));
792        let opts = Options::builder().pre_compact_handler(handler).build();
793        assert!(opts.pre_compact_handler.is_some());
794    }
795
796    #[test]
797    fn builder_max_tool_result_bytes_sets_field() {
798        let opts = Options::builder().max_tool_result_bytes(100_000).build();
799        assert_eq!(opts.max_tool_result_bytes, Some(100_000));
800    }
801
802    #[test]
803    fn builder_max_tool_result_bytes_default_is_none() {
804        let opts = Options::builder().build();
805        assert!(opts.max_tool_result_bytes.is_none());
806    }
807
808    #[test]
809    fn builder_prune_threshold_pct_sets_field() {
810        let opts = Options::builder().prune_threshold_pct(80).build();
811        assert_eq!(opts.prune_threshold_pct, Some(80));
812    }
813
814    #[test]
815    fn builder_prune_threshold_pct_default_is_none() {
816        let opts = Options::builder().build();
817        assert!(opts.prune_threshold_pct.is_none());
818    }
819
820    #[test]
821    fn builder_prune_tool_result_max_chars_sets_field() {
822        let opts = Options::builder().prune_tool_result_max_chars(5000).build();
823        assert_eq!(opts.prune_tool_result_max_chars, Some(5000));
824    }
825
826    #[test]
827    fn builder_prune_tool_result_max_chars_default_is_none() {
828        let opts = Options::builder().build();
829        assert!(opts.prune_tool_result_max_chars.is_none());
830    }
831
832    #[test]
833    fn builder_sanitization_and_pruning_combined() {
834        let opts = Options::builder()
835            .max_tool_result_bytes(75_000)
836            .prune_threshold_pct(60)
837            .prune_tool_result_max_chars(3000)
838            .context_budget(200_000)
839            .build();
840        assert_eq!(opts.max_tool_result_bytes, Some(75_000));
841        assert_eq!(opts.prune_threshold_pct, Some(60));
842        assert_eq!(opts.prune_tool_result_max_chars, Some(3000));
843        assert_eq!(opts.context_budget, Some(200_000));
844    }
845}