1use std::collections::BTreeMap;
5use std::ffi::OsString;
6use std::future::Future;
7use std::path::PathBuf;
8use std::pin::Pin;
9use std::sync::Arc;
10use std::time::Duration;
11
12use serde_json::{Map, Value, json};
13
14use crate::tools::Tool;
15use crate::wire::{ApprovalRequest, Decision};
16
17pub(crate) type ApprovalCallback = Arc<
19 dyn Fn(ApprovalRequest) -> Pin<Box<dyn Future<Output = Decision> + Send + 'static>>
20 + Send
21 + Sync,
22>;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Mode {
27 Manual,
29 Auto,
31 Plan,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Sandbox {
38 On,
40 Off,
42 External,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum SystemPrompt {
49 Replace(String),
52 Append(String),
54}
55
56#[derive(Clone, Default)]
59pub struct Options {
60 model: Option<String>,
61 thinking: Option<String>,
62 mode: Option<Mode>,
63 sandbox: Option<Sandbox>,
64 builtin_tools: Option<Vec<String>>,
65 system_prompt: Option<SystemPrompt>,
66 project_instructions: Option<bool>,
67 user_config: Option<bool>,
68 skills: Option<bool>,
69 agents: Option<bool>,
70 hooks: Option<bool>,
71 mcp_servers: BTreeMap<String, Value>,
72 approval_timeout: Option<Option<Duration>>,
74 allow_escapes: Option<bool>,
75 max_iterations: Option<u32>,
76 pub(crate) cwd: Option<PathBuf>,
77 pub(crate) env: Vec<(OsString, OsString)>,
78 pub(crate) binary: Option<PathBuf>,
79 pub(crate) mock: Option<PathBuf>,
80 pub(crate) tools: Vec<Tool>,
81 pub(crate) on_approval: Option<ApprovalCallback>,
82}
83
84impl std::fmt::Debug for Options {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 let env: Vec<_> = self.env.iter().map(|(k, _)| k).collect();
88 f.debug_struct("Options")
89 .field("model", &self.model)
90 .field("thinking", &self.thinking)
91 .field("mode", &self.mode)
92 .field("sandbox", &self.sandbox)
93 .field("builtin_tools", &self.builtin_tools)
94 .field("system_prompt", &self.system_prompt)
95 .field("project_instructions", &self.project_instructions)
96 .field("user_config", &self.user_config)
97 .field("skills", &self.skills)
98 .field("agents", &self.agents)
99 .field("hooks", &self.hooks)
100 .field("mcp_servers", &self.mcp_servers)
101 .field("approval_timeout", &self.approval_timeout)
102 .field("allow_escapes", &self.allow_escapes)
103 .field("max_iterations", &self.max_iterations)
104 .field("cwd", &self.cwd)
105 .field("env (names only)", &env)
106 .field("binary", &self.binary)
107 .field("mock", &self.mock)
108 .field("tools", &self.tools)
109 .field("on_approval", &self.on_approval.is_some())
110 .finish()
111 }
112}
113
114impl Options {
115 pub fn new() -> Self {
117 Self::default()
118 }
119
120 pub fn plain(system_prompt: impl Into<String>) -> Self {
123 Self::new()
124 .builtin_tools(Vec::<String>::new())
125 .system_prompt(SystemPrompt::Replace(system_prompt.into()))
126 }
127
128 pub fn model(mut self, model: impl Into<String>) -> Self {
130 self.model = Some(model.into());
131 self
132 }
133
134 pub fn thinking(mut self, thinking: impl Into<String>) -> Self {
136 self.thinking = Some(thinking.into());
137 self
138 }
139
140 pub fn mode(mut self, mode: Mode) -> Self {
142 self.mode = Some(mode);
143 self
144 }
145
146 pub fn sandbox(mut self, sandbox: Sandbox) -> Self {
148 self.sandbox = Some(sandbox);
149 self
150 }
151
152 pub fn builtin_tools<I, S>(mut self, names: I) -> Self
155 where
156 I: IntoIterator<Item = S>,
157 S: Into<String>,
158 {
159 self.builtin_tools = Some(names.into_iter().map(Into::into).collect());
160 self
161 }
162
163 pub fn system_prompt(mut self, prompt: SystemPrompt) -> Self {
165 self.system_prompt = Some(prompt);
166 self
167 }
168
169 pub fn project_instructions(mut self, on: bool) -> Self {
171 self.project_instructions = Some(on);
172 self
173 }
174
175 pub fn user_config(mut self, on: bool) -> Self {
177 self.user_config = Some(on);
178 self
179 }
180
181 pub fn skills(mut self, on: bool) -> Self {
183 self.skills = Some(on);
184 self
185 }
186
187 pub fn agents(mut self, on: bool) -> Self {
189 self.agents = Some(on);
190 self
191 }
192
193 pub fn hooks(mut self, on: bool) -> Self {
195 self.hooks = Some(on);
196 self
197 }
198
199 pub fn mcp_server(mut self, name: impl Into<String>, server: Value) -> Self {
202 self.mcp_servers.insert(name.into(), server);
203 self
204 }
205
206 pub fn approval_timeout(mut self, timeout: Option<Duration>) -> Self {
209 self.approval_timeout = Some(timeout);
210 self
211 }
212
213 pub fn allow_escapes(mut self, on: bool) -> Self {
215 self.allow_escapes = Some(on);
216 self
217 }
218
219 pub fn max_iterations(mut self, n: u32) -> Self {
221 self.max_iterations = Some(n);
222 self
223 }
224
225 pub fn cwd(mut self, dir: impl Into<PathBuf>) -> Self {
227 self.cwd = Some(dir.into());
228 self
229 }
230
231 pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
233 self.env.push((key.into(), value.into()));
234 self
235 }
236
237 pub fn binary(mut self, path: impl Into<PathBuf>) -> Self {
239 self.binary = Some(path.into());
240 self
241 }
242
243 pub fn mock(mut self, path: impl Into<PathBuf>) -> Self {
245 self.mock = Some(path.into());
246 self
247 }
248
249 pub fn tool(mut self, tool: Tool) -> Self {
252 self.tools.push(tool);
253 self
254 }
255
256 pub fn on_approval<F, Fut>(mut self, callback: F) -> Self
261 where
262 F: Fn(ApprovalRequest) -> Fut + Send + Sync + 'static,
263 Fut: Future<Output = Decision> + Send + 'static,
264 {
265 self.on_approval = Some(Arc::new(move |request| Box::pin(callback(request))));
266 self
267 }
268
269 pub(crate) fn file_json(&self, resume: bool) -> Value {
275 let mut m = Map::new();
276 if let Some(v) = &self.model {
277 m.insert("model".into(), json!(v));
278 }
279 if let Some(v) = &self.thinking {
280 m.insert("thinking".into(), json!(v));
281 }
282 if let Some(v) = self.max_iterations {
283 m.insert("max_iterations".into(), json!(v));
284 }
285 if let Some(v) = self.approval_timeout {
286 let text = match v {
287 None => "off".to_string(),
288 Some(d) => format!("{}s", crate::tools::whole_seconds(d)),
289 };
290 m.insert("approval_timeout".into(), json!(text));
291 }
292 if let Some(v) = self.allow_escapes {
293 m.insert("allow_escapes".into(), json!(v));
294 }
295 if !self.tools.is_empty() {
296 let tools: Vec<Value> = self.tools.iter().map(Tool::definition).collect();
297 m.insert("client_tools".into(), Value::Array(tools));
298 }
299 if resume {
300 return Value::Object(m);
301 }
302 if !self.mcp_servers.is_empty() {
303 m.insert("mcp_servers".into(), json!(self.mcp_servers));
304 }
305 if let Some(v) = self.mode {
306 let name = match v {
307 Mode::Manual => "manual",
308 Mode::Auto => "auto",
309 Mode::Plan => "plan",
310 };
311 m.insert("mode".into(), json!(name));
312 }
313 if let Some(v) = self.sandbox {
314 let name = match v {
315 Sandbox::On => "on",
316 Sandbox::Off => "off",
317 Sandbox::External => "external",
318 };
319 m.insert("sandbox".into(), json!(name));
320 }
321 if let Some(v) = &self.builtin_tools {
322 m.insert("builtin_tools".into(), json!(v));
323 }
324 if let Some(v) = &self.system_prompt {
325 let v = match v {
326 SystemPrompt::Replace(t) => json!({ "replace": t }),
327 SystemPrompt::Append(t) => json!({ "append": t }),
328 };
329 m.insert("system_prompt".into(), v);
330 }
331 for (name, v) in [
332 ("project_instructions", self.project_instructions),
333 ("user_config", self.user_config),
334 ("skills", self.skills),
335 ("agents", self.agents),
336 ("hooks", self.hooks),
337 ] {
338 if let Some(v) = v {
339 m.insert(name.into(), json!(v));
340 }
341 }
342 Value::Object(m)
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 #[test]
351 fn a_resume_sends_only_what_a_resume_may_give() {
352 let o = Options::plain("be brief")
353 .mode(Mode::Plan)
354 .sandbox(Sandbox::Off)
355 .skills(true)
356 .model("m")
357 .approval_timeout(None)
358 .max_iterations(3)
359 .mcp_server("files", json!({ "command": ["true"] }));
360 assert_eq!(
361 o.file_json(true),
362 json!({"model": "m", "approval_timeout": "off", "max_iterations": 3})
363 );
364 assert_eq!(
365 o.file_json(false),
366 json!({
367 "model": "m", "approval_timeout": "off", "max_iterations": 3,
368 "mode": "plan", "sandbox": "off", "builtin_tools": [],
369 "system_prompt": {"replace": "be brief"}, "skills": true,
370 "mcp_servers": {"files": {"command": ["true"]}},
371 })
372 );
373 }
374
375 #[test]
376 fn debug_names_the_environment_but_never_its_values() {
377 let o = Options::new().env("ANTHROPIC_API_KEY", "sk-secret-value");
378 let shown = format!("{o:?}");
379 assert!(shown.contains("ANTHROPIC_API_KEY"), "{shown}");
380 assert!(!shown.contains("sk-secret-value"), "{shown}");
381 }
382
383 #[test]
384 fn an_approval_timeout_is_whole_seconds_rounded_up() {
385 let o = Options::new().approval_timeout(Some(Duration::from_millis(1500)));
386 assert_eq!(o.file_json(false), json!({"approval_timeout": "2s"}));
387 let o = Options::new().approval_timeout(Some(Duration::from_millis(10)));
388 assert_eq!(o.file_json(false), json!({"approval_timeout": "1s"}));
389 }
390}