1use std::collections::HashMap;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum AgentKind {
10 Claude,
11 OpenCode,
12 Codex,
13 Cursor,
14}
15
16impl AgentKind {
17 pub fn default_binary(&self) -> &'static str {
19 self.binary_candidates()[0]
20 }
21
22 pub fn binary_candidates(&self) -> &'static [&'static str] {
25 match self {
26 AgentKind::Claude => &["claude"],
27 AgentKind::OpenCode => &["opencode"],
28 AgentKind::Codex => &["codex"],
29 AgentKind::Cursor => &["cursor-agent", "agent"],
31 }
32 }
33
34 pub fn api_key_env_vars(&self) -> &'static [&'static str] {
36 match self {
37 AgentKind::Claude => &["ANTHROPIC_API_KEY"],
38 AgentKind::Codex => &["OPENAI_API_KEY"],
39 AgentKind::OpenCode => &["ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
40 AgentKind::Cursor => &["CURSOR_API_KEY"],
41 }
42 }
43
44 pub fn display_name(&self) -> &'static str {
45 match self {
46 AgentKind::Claude => "Claude Code",
47 AgentKind::OpenCode => "OpenCode",
48 AgentKind::Codex => "Codex",
49 AgentKind::Cursor => "Cursor",
50 }
51 }
52}
53
54impl std::fmt::Display for AgentKind {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 f.write_str(self.display_name())
57 }
58}
59
60impl std::str::FromStr for AgentKind {
61 type Err = String;
62
63 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
64 match s.to_lowercase().as_str() {
65 "claude" | "claude-code" | "claude_code" => Ok(AgentKind::Claude),
66 "opencode" | "open-code" | "open_code" => Ok(AgentKind::OpenCode),
67 "codex" | "openai-codex" | "openai_codex" => Ok(AgentKind::Codex),
68 "cursor" | "cursor-agent" | "cursor_agent" => Ok(AgentKind::Cursor),
69 _ => Err(format!(
70 "unknown agent: `{s}` (expected: claude, opencode, codex, cursor)"
71 )),
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
80#[serde(rename_all = "snake_case")]
81pub enum PermissionMode {
82 #[default]
84 FullAccess,
85 ReadOnly,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
91#[serde(rename_all = "snake_case")]
92pub enum OutputFormat {
93 Text,
95 Json,
97 #[default]
99 StreamJson,
100 Markdown,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct TaskConfig {
107 pub prompt: String,
109
110 pub agent: AgentKind,
112
113 #[serde(default)]
115 pub cwd: Option<PathBuf>,
116
117 #[serde(default)]
119 pub model: Option<String>,
120
121 #[serde(default)]
123 pub permission_mode: PermissionMode,
124
125 #[serde(default)]
127 pub output_format: OutputFormat,
128
129 #[serde(default)]
131 pub max_turns: Option<u32>,
132
133 #[serde(default)]
135 pub max_budget_usd: Option<f64>,
136
137 #[serde(default)]
139 pub timeout_secs: Option<u64>,
140
141 #[serde(default)]
143 pub system_prompt: Option<String>,
144
145 #[serde(default)]
147 pub append_system_prompt: Option<String>,
148
149 #[serde(default)]
151 pub binary_path: Option<PathBuf>,
152
153 #[serde(default)]
155 pub env: HashMap<String, String>,
156
157 #[serde(default)]
159 pub extra_args: Vec<String>,
160}
161
162impl TaskConfig {
163 pub fn new(prompt: impl Into<String>, agent: AgentKind) -> Self {
164 Self {
165 prompt: prompt.into(),
166 agent,
167 cwd: None,
168 model: None,
169 permission_mode: PermissionMode::FullAccess,
170 output_format: OutputFormat::StreamJson,
171 max_turns: None,
172 max_budget_usd: None,
173 timeout_secs: None,
174 system_prompt: None,
175 append_system_prompt: None,
176 binary_path: None,
177 env: HashMap::new(),
178 extra_args: Vec::new(),
179 }
180 }
181
182 pub fn builder(prompt: impl Into<String>, agent: AgentKind) -> TaskConfigBuilder {
184 TaskConfigBuilder::new(prompt, agent)
185 }
186}
187
188pub struct TaskConfigBuilder {
198 config: TaskConfig,
199}
200
201impl TaskConfigBuilder {
202 pub fn new(prompt: impl Into<String>, agent: AgentKind) -> Self {
203 Self {
204 config: TaskConfig::new(prompt, agent),
205 }
206 }
207
208 pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
209 self.config.cwd = Some(cwd.into());
210 self
211 }
212
213 pub fn model(mut self, model: impl Into<String>) -> Self {
214 self.config.model = Some(model.into());
215 self
216 }
217
218 pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
219 self.config.permission_mode = mode;
220 self
221 }
222
223 pub fn read_only(mut self) -> Self {
224 self.config.permission_mode = PermissionMode::ReadOnly;
225 self
226 }
227
228 pub fn output_format(mut self, format: OutputFormat) -> Self {
229 self.config.output_format = format;
230 self
231 }
232
233 pub fn max_turns(mut self, turns: u32) -> Self {
234 self.config.max_turns = Some(turns);
235 self
236 }
237
238 pub fn max_budget_usd(mut self, budget: f64) -> Self {
239 self.config.max_budget_usd = Some(budget);
240 self
241 }
242
243 pub fn timeout_secs(mut self, secs: u64) -> Self {
244 self.config.timeout_secs = Some(secs);
245 self
246 }
247
248 pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
249 self.config.system_prompt = Some(prompt.into());
250 self
251 }
252
253 pub fn append_system_prompt(mut self, prompt: impl Into<String>) -> Self {
254 self.config.append_system_prompt = Some(prompt.into());
255 self
256 }
257
258 pub fn binary_path(mut self, path: impl Into<PathBuf>) -> Self {
259 self.config.binary_path = Some(path.into());
260 self
261 }
262
263 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
264 self.config.env.insert(key.into(), value.into());
265 self
266 }
267
268 pub fn extra_arg(mut self, arg: impl Into<String>) -> Self {
269 self.config.extra_args.push(arg.into());
270 self
271 }
272
273 pub fn extra_args(mut self, args: Vec<String>) -> Self {
274 self.config.extra_args.extend(args);
275 self
276 }
277
278 pub fn build(self) -> TaskConfig {
279 self.config
280 }
281}