use std::collections::HashMap;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentKind {
Claude,
OpenCode,
Codex,
Cursor,
}
impl AgentKind {
pub fn default_binary(&self) -> &'static str {
self.binary_candidates()[0]
}
pub fn binary_candidates(&self) -> &'static [&'static str] {
match self {
AgentKind::Claude => &["claude"],
AgentKind::OpenCode => &["opencode"],
AgentKind::Codex => &["codex"],
AgentKind::Cursor => &["cursor-agent", "agent"],
}
}
pub fn api_key_env_vars(&self) -> &'static [&'static str] {
match self {
AgentKind::Claude => &["ANTHROPIC_API_KEY"],
AgentKind::Codex => &["OPENAI_API_KEY"],
AgentKind::OpenCode => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
AgentKind::Cursor => &["CURSOR_API_KEY"],
}
}
pub fn display_name(&self) -> &'static str {
match self {
AgentKind::Claude => "Claude Code",
AgentKind::OpenCode => "OpenCode",
AgentKind::Codex => "Codex",
AgentKind::Cursor => "Cursor",
}
}
}
impl std::fmt::Display for AgentKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.display_name())
}
}
impl std::str::FromStr for AgentKind {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"claude" | "claude-code" | "claude_code" => Ok(AgentKind::Claude),
"opencode" | "open-code" | "open_code" => Ok(AgentKind::OpenCode),
"codex" | "openai-codex" | "openai_codex" => Ok(AgentKind::Codex),
"cursor" | "cursor-agent" | "cursor_agent" => Ok(AgentKind::Cursor),
_ => Err(format!(
"unknown agent: `{s}` (expected: claude, opencode, codex, cursor)"
)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum PermissionMode {
#[default]
FullAccess,
ReadOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum OutputFormat {
Text,
Json,
#[default]
StreamJson,
Markdown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskConfig {
pub prompt: String,
pub agent: AgentKind,
#[serde(default)]
pub cwd: Option<PathBuf>,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub permission_mode: PermissionMode,
#[serde(default)]
pub output_format: OutputFormat,
#[serde(default)]
pub max_turns: Option<u32>,
#[serde(default)]
pub max_budget_usd: Option<f64>,
#[serde(default)]
pub timeout_secs: Option<u64>,
#[serde(default)]
pub system_prompt: Option<String>,
#[serde(default)]
pub append_system_prompt: Option<String>,
#[serde(default)]
pub binary_path: Option<PathBuf>,
#[serde(default)]
pub env: HashMap<String, String>,
#[serde(default)]
pub extra_args: Vec<String>,
}
impl TaskConfig {
pub fn new(prompt: impl Into<String>, agent: AgentKind) -> Self {
Self {
prompt: prompt.into(),
agent,
cwd: None,
model: None,
permission_mode: PermissionMode::FullAccess,
output_format: OutputFormat::StreamJson,
max_turns: None,
max_budget_usd: None,
timeout_secs: None,
system_prompt: None,
append_system_prompt: None,
binary_path: None,
env: HashMap::new(),
extra_args: Vec::new(),
}
}
pub fn builder(prompt: impl Into<String>, agent: AgentKind) -> TaskConfigBuilder {
TaskConfigBuilder::new(prompt, agent)
}
}
pub struct TaskConfigBuilder {
config: TaskConfig,
}
impl TaskConfigBuilder {
pub fn new(prompt: impl Into<String>, agent: AgentKind) -> Self {
Self {
config: TaskConfig::new(prompt, agent),
}
}
pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
self.config.cwd = Some(cwd.into());
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.config.model = Some(model.into());
self
}
pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
self.config.permission_mode = mode;
self
}
pub fn read_only(mut self) -> Self {
self.config.permission_mode = PermissionMode::ReadOnly;
self
}
pub fn output_format(mut self, format: OutputFormat) -> Self {
self.config.output_format = format;
self
}
pub fn max_turns(mut self, turns: u32) -> Self {
self.config.max_turns = Some(turns);
self
}
pub fn max_budget_usd(mut self, budget: f64) -> Self {
self.config.max_budget_usd = Some(budget);
self
}
pub fn timeout_secs(mut self, secs: u64) -> Self {
self.config.timeout_secs = Some(secs);
self
}
pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.config.system_prompt = Some(prompt.into());
self
}
pub fn append_system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.config.append_system_prompt = Some(prompt.into());
self
}
pub fn binary_path(mut self, path: impl Into<PathBuf>) -> Self {
self.config.binary_path = Some(path.into());
self
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.config.env.insert(key.into(), value.into());
self
}
pub fn extra_arg(mut self, arg: impl Into<String>) -> Self {
self.config.extra_args.push(arg.into());
self
}
pub fn extra_args(mut self, args: Vec<String>) -> Self {
self.config.extra_args.extend(args);
self
}
pub fn build(self) -> TaskConfig {
self.config
}
}