use std::collections::BTreeMap;
use std::ffi::OsString;
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use serde_json::{Map, Value, json};
use crate::tools::Tool;
use crate::wire::{ApprovalRequest, Decision};
pub(crate) type ApprovalCallback = Arc<
dyn Fn(ApprovalRequest) -> Pin<Box<dyn Future<Output = Decision> + Send + 'static>>
+ Send
+ Sync,
>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Manual,
Auto,
Plan,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Sandbox {
On,
Off,
External,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SystemPrompt {
Replace(String),
Append(String),
}
#[derive(Clone, Default)]
pub struct Options {
model: Option<String>,
thinking: Option<String>,
mode: Option<Mode>,
sandbox: Option<Sandbox>,
builtin_tools: Option<Vec<String>>,
system_prompt: Option<SystemPrompt>,
project_instructions: Option<bool>,
user_config: Option<bool>,
skills: Option<bool>,
agents: Option<bool>,
hooks: Option<bool>,
mcp_servers: BTreeMap<String, Value>,
approval_timeout: Option<Option<Duration>>,
allow_escapes: Option<bool>,
max_iterations: Option<u32>,
pub(crate) cwd: Option<PathBuf>,
pub(crate) env: Vec<(OsString, OsString)>,
pub(crate) binary: Option<PathBuf>,
pub(crate) mock: Option<PathBuf>,
pub(crate) tools: Vec<Tool>,
pub(crate) on_approval: Option<ApprovalCallback>,
}
impl std::fmt::Debug for Options {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let env: Vec<_> = self.env.iter().map(|(k, _)| k).collect();
f.debug_struct("Options")
.field("model", &self.model)
.field("thinking", &self.thinking)
.field("mode", &self.mode)
.field("sandbox", &self.sandbox)
.field("builtin_tools", &self.builtin_tools)
.field("system_prompt", &self.system_prompt)
.field("project_instructions", &self.project_instructions)
.field("user_config", &self.user_config)
.field("skills", &self.skills)
.field("agents", &self.agents)
.field("hooks", &self.hooks)
.field("mcp_servers", &self.mcp_servers)
.field("approval_timeout", &self.approval_timeout)
.field("allow_escapes", &self.allow_escapes)
.field("max_iterations", &self.max_iterations)
.field("cwd", &self.cwd)
.field("env (names only)", &env)
.field("binary", &self.binary)
.field("mock", &self.mock)
.field("tools", &self.tools)
.field("on_approval", &self.on_approval.is_some())
.finish()
}
}
impl Options {
pub fn new() -> Self {
Self::default()
}
pub fn plain(system_prompt: impl Into<String>) -> Self {
Self::new()
.builtin_tools(Vec::<String>::new())
.system_prompt(SystemPrompt::Replace(system_prompt.into()))
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn thinking(mut self, thinking: impl Into<String>) -> Self {
self.thinking = Some(thinking.into());
self
}
pub fn mode(mut self, mode: Mode) -> Self {
self.mode = Some(mode);
self
}
pub fn sandbox(mut self, sandbox: Sandbox) -> Self {
self.sandbox = Some(sandbox);
self
}
pub fn builtin_tools<I, S>(mut self, names: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.builtin_tools = Some(names.into_iter().map(Into::into).collect());
self
}
pub fn system_prompt(mut self, prompt: SystemPrompt) -> Self {
self.system_prompt = Some(prompt);
self
}
pub fn project_instructions(mut self, on: bool) -> Self {
self.project_instructions = Some(on);
self
}
pub fn user_config(mut self, on: bool) -> Self {
self.user_config = Some(on);
self
}
pub fn skills(mut self, on: bool) -> Self {
self.skills = Some(on);
self
}
pub fn agents(mut self, on: bool) -> Self {
self.agents = Some(on);
self
}
pub fn hooks(mut self, on: bool) -> Self {
self.hooks = Some(on);
self
}
pub fn mcp_server(mut self, name: impl Into<String>, server: Value) -> Self {
self.mcp_servers.insert(name.into(), server);
self
}
pub fn approval_timeout(mut self, timeout: Option<Duration>) -> Self {
self.approval_timeout = Some(timeout);
self
}
pub fn allow_escapes(mut self, on: bool) -> Self {
self.allow_escapes = Some(on);
self
}
pub fn max_iterations(mut self, n: u32) -> Self {
self.max_iterations = Some(n);
self
}
pub fn cwd(mut self, dir: impl Into<PathBuf>) -> Self {
self.cwd = Some(dir.into());
self
}
pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
self.env.push((key.into(), value.into()));
self
}
pub fn binary(mut self, path: impl Into<PathBuf>) -> Self {
self.binary = Some(path.into());
self
}
pub fn mock(mut self, path: impl Into<PathBuf>) -> Self {
self.mock = Some(path.into());
self
}
pub fn tool(mut self, tool: Tool) -> Self {
self.tools.push(tool);
self
}
pub fn on_approval<F, Fut>(mut self, callback: F) -> Self
where
F: Fn(ApprovalRequest) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Decision> + Send + 'static,
{
self.on_approval = Some(Arc::new(move |request| Box::pin(callback(request))));
self
}
pub(crate) fn file_json(&self, resume: bool) -> Value {
let mut m = Map::new();
if let Some(v) = &self.model {
m.insert("model".into(), json!(v));
}
if let Some(v) = &self.thinking {
m.insert("thinking".into(), json!(v));
}
if let Some(v) = self.max_iterations {
m.insert("max_iterations".into(), json!(v));
}
if let Some(v) = self.approval_timeout {
let text = match v {
None => "off".to_string(),
Some(d) => format!("{}s", crate::tools::whole_seconds(d)),
};
m.insert("approval_timeout".into(), json!(text));
}
if let Some(v) = self.allow_escapes {
m.insert("allow_escapes".into(), json!(v));
}
if !self.tools.is_empty() {
let tools: Vec<Value> = self.tools.iter().map(Tool::definition).collect();
m.insert("client_tools".into(), Value::Array(tools));
}
if resume {
return Value::Object(m);
}
if !self.mcp_servers.is_empty() {
m.insert("mcp_servers".into(), json!(self.mcp_servers));
}
if let Some(v) = self.mode {
let name = match v {
Mode::Manual => "manual",
Mode::Auto => "auto",
Mode::Plan => "plan",
};
m.insert("mode".into(), json!(name));
}
if let Some(v) = self.sandbox {
let name = match v {
Sandbox::On => "on",
Sandbox::Off => "off",
Sandbox::External => "external",
};
m.insert("sandbox".into(), json!(name));
}
if let Some(v) = &self.builtin_tools {
m.insert("builtin_tools".into(), json!(v));
}
if let Some(v) = &self.system_prompt {
let v = match v {
SystemPrompt::Replace(t) => json!({ "replace": t }),
SystemPrompt::Append(t) => json!({ "append": t }),
};
m.insert("system_prompt".into(), v);
}
for (name, v) in [
("project_instructions", self.project_instructions),
("user_config", self.user_config),
("skills", self.skills),
("agents", self.agents),
("hooks", self.hooks),
] {
if let Some(v) = v {
m.insert(name.into(), json!(v));
}
}
Value::Object(m)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_resume_sends_only_what_a_resume_may_give() {
let o = Options::plain("be brief")
.mode(Mode::Plan)
.sandbox(Sandbox::Off)
.skills(true)
.model("m")
.approval_timeout(None)
.max_iterations(3)
.mcp_server("files", json!({ "command": ["true"] }));
assert_eq!(
o.file_json(true),
json!({"model": "m", "approval_timeout": "off", "max_iterations": 3})
);
assert_eq!(
o.file_json(false),
json!({
"model": "m", "approval_timeout": "off", "max_iterations": 3,
"mode": "plan", "sandbox": "off", "builtin_tools": [],
"system_prompt": {"replace": "be brief"}, "skills": true,
"mcp_servers": {"files": {"command": ["true"]}},
})
);
}
#[test]
fn debug_names_the_environment_but_never_its_values() {
let o = Options::new().env("ANTHROPIC_API_KEY", "sk-secret-value");
let shown = format!("{o:?}");
assert!(shown.contains("ANTHROPIC_API_KEY"), "{shown}");
assert!(!shown.contains("sk-secret-value"), "{shown}");
}
#[test]
fn an_approval_timeout_is_whole_seconds_rounded_up() {
let o = Options::new().approval_timeout(Some(Duration::from_millis(1500)));
assert_eq!(o.file_json(false), json!({"approval_timeout": "2s"}));
let o = Options::new().approval_timeout(Some(Duration::from_millis(10)));
assert_eq!(o.file_json(false), json!({"approval_timeout": "1s"}));
}
}