claude_pool/types.rs
1//! Core types for claude-pool.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6// Re-export shared types from claude-wrapper so consumers don't need
7// to depend on both crates for basic config.
8pub use claude_wrapper::types::{Effort, PermissionMode};
9
10// ── Identifiers ──────────────────────────────────────────────────────
11
12/// Unique identifier for a task.
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct TaskId(pub String);
15
16/// Unique identifier for a slot.
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub struct SlotId(pub String);
19
20// ── Slot types ─────────────────────────────────────────────────────
21
22/// Slot persistence mode.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
24#[serde(rename_all = "snake_case")]
25pub enum SlotMode {
26 /// Persistent slots stay alive across tasks, resuming sessions.
27 #[default]
28 Persistent,
29 /// Ephemeral slots are created per task and destroyed after.
30 Ephemeral,
31}
32
33/// Configuration for dynamic slot pool scaling.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct ScalingConfig {
36 /// Minimum number of slots (default: 1).
37 pub min_slots: usize,
38 /// Maximum number of slots (default: 16).
39 pub max_slots: usize,
40}
41
42impl Default for ScalingConfig {
43 fn default() -> Self {
44 Self {
45 min_slots: 1,
46 max_slots: 16,
47 }
48 }
49}
50
51/// Configuration that applies to all slots by default.
52///
53/// Individual slots can override any of these fields via [`SlotConfig`].
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct PoolConfig {
56 /// Claude model to use (e.g. "claude-haiku-4-5-20251001").
57 pub model: Option<String>,
58
59 /// Permission mode for slots.
60 pub permission_mode: Option<PermissionMode>,
61
62 /// Maximum turns per task.
63 pub max_turns: Option<u32>,
64
65 /// System prompt prepended to all slot tasks.
66 pub system_prompt: Option<String>,
67
68 /// Allowed tools for slots.
69 pub allowed_tools: Vec<String>,
70
71 /// MCP servers available to slots.
72 pub mcp_servers: HashMap<String, serde_json::Value>,
73
74 /// Default effort level for slots (maps to `--effort`).
75 pub effort: Option<Effort>,
76
77 /// Total budget cap for the pool in microdollars.
78 /// When cumulative spend across all slots reaches this limit,
79 /// new tasks are rejected with [`crate::Error::BudgetExhausted`].
80 pub budget_microdollars: Option<u64>,
81
82 /// Default slot mode.
83 pub slot_mode: SlotMode,
84
85 /// Maximum number of restarts per slot before marking as errored.
86 pub max_restarts: u32,
87
88 /// Enable git worktree isolation for slots.
89 pub worktree_isolation: bool,
90
91 /// Maximum time to wait for an idle slot before failing a task (in seconds).
92 pub slot_assignment_timeout_secs: u64,
93
94 /// Dynamic scaling configuration (min/max bounds).
95 pub scaling: ScalingConfig,
96
97 /// Enable unattended mode: use stricter permission defaults to prevent prompts.
98 /// When true, defaults to `DontAsk` permission mode if not explicitly set.
99 pub unattended_mode: bool,
100
101 /// If true, detect permission prompt patterns in stderr and provide actionable errors.
102 pub detect_permission_prompts: bool,
103
104 /// Enable the background supervisor loop for slot health monitoring.
105 ///
106 /// When enabled, the supervisor periodically checks for errored slots and
107 /// restarts them automatically (up to [`max_restarts`](Self::max_restarts)).
108 pub supervisor_enabled: bool,
109
110 /// Interval in seconds between supervisor health checks (default: 30).
111 ///
112 /// Only used when [`supervisor_enabled`](Self::supervisor_enabled) is true.
113 pub supervisor_interval_secs: u64,
114
115 /// Use `--strict-mcp-config` when passing MCP config to slots.
116 ///
117 /// Prevents slots from inheriting the coordinator's `.mcp.json`, which
118 /// avoids accidental recursive pool calls (a slot invoking `pool_run` on itself).
119 /// Default: `true`.
120 pub strict_mcp_config: bool,
121}
122
123impl Default for PoolConfig {
124 fn default() -> Self {
125 Self {
126 model: None,
127 permission_mode: Some(PermissionMode::Plan),
128 max_turns: None,
129 system_prompt: None,
130 allowed_tools: Vec::new(),
131 mcp_servers: HashMap::new(),
132 effort: None,
133 budget_microdollars: None,
134 slot_mode: SlotMode::default(),
135 max_restarts: 3,
136 worktree_isolation: false,
137 slot_assignment_timeout_secs: 300,
138 scaling: ScalingConfig::default(),
139 unattended_mode: false,
140 detect_permission_prompts: true,
141 supervisor_enabled: false,
142 supervisor_interval_secs: 30,
143 strict_mcp_config: true,
144 }
145 }
146}
147
148/// Per-slot configuration overrides.
149///
150/// Any `Some` field here takes precedence over the corresponding field
151/// in [`PoolConfig`].
152#[derive(Debug, Clone, Default, Serialize, Deserialize)]
153pub struct SlotConfig {
154 /// Override model for this slot.
155 pub model: Option<String>,
156
157 /// Override permission mode for this slot.
158 pub permission_mode: Option<PermissionMode>,
159
160 /// Override max turns for this slot.
161 pub max_turns: Option<u32>,
162
163 /// Override system prompt for this slot.
164 pub system_prompt: Option<String>,
165
166 /// Additional allowed tools (merged with global).
167 pub allowed_tools: Option<Vec<String>>,
168
169 /// Additional MCP servers (merged with global).
170 pub mcp_servers: Option<HashMap<String, serde_json::Value>>,
171
172 /// Override effort level for this slot.
173 pub effort: Option<Effort>,
174
175 /// Optional name/role for this slot (e.g. "reviewer", "coder").
176 pub role: Option<String>,
177
178 /// Optional human-readable name for the slot (e.g. "reviewer", "writer").
179 pub name: Option<String>,
180
181 /// Optional description of the slot's purpose or responsibilities.
182 pub description: Option<String>,
183
184 /// Override slot assignment timeout (in seconds).
185 pub slot_assignment_timeout_secs: Option<u64>,
186}
187
188/// Current state of a slot.
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
190#[serde(rename_all = "snake_case")]
191pub enum SlotState {
192 /// Slot is ready to accept a task.
193 Idle,
194 /// Slot is currently executing a task.
195 Busy,
196 /// Slot process has exited or been stopped.
197 Stopped,
198 /// Slot encountered an error and needs attention.
199 Errored,
200}
201
202/// Record of a slot in the pool.
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct SlotRecord {
205 /// Unique slot identifier.
206 pub id: SlotId,
207
208 /// Current state.
209 pub state: SlotState,
210
211 /// Per-slot config overrides.
212 pub config: SlotConfig,
213
214 /// The task currently being executed, if any.
215 pub current_task: Option<TaskId>,
216
217 /// Claude session ID for session resumption.
218 pub session_id: Option<String>,
219
220 /// Number of tasks completed by this slot.
221 pub tasks_completed: u64,
222
223 /// Cumulative cost in microdollars.
224 pub cost_microdollars: u64,
225
226 /// Number of times this slot has been restarted.
227 pub restart_count: u32,
228
229 /// Git worktree path, if worktree isolation is enabled.
230 pub worktree_path: Option<String>,
231
232 /// Path to the slot's temp `.mcp.json` file, if MCP servers are configured.
233 ///
234 /// Written once per slot (on first task that needs it) and reused across
235 /// subsequent tasks. Cleaned up on pool drain/shutdown.
236 #[serde(skip)]
237 pub mcp_config_path: Option<std::path::PathBuf>,
238}
239
240// ── Task types ───────────────────────────────────────────────────────
241
242/// Current state of a task.
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(rename_all = "snake_case")]
245pub enum TaskState {
246 /// Task is waiting for a slot.
247 Pending,
248 /// Task is being executed by a slot.
249 Running,
250 /// Task completed successfully.
251 Completed,
252 /// Task failed.
253 Failed,
254 /// Task was cancelled.
255 Cancelled,
256 /// Task completed but awaits coordinator approval before being considered done.
257 PendingReview,
258}
259
260/// A task submitted to the pool.
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct TaskRecord {
263 /// Unique task identifier.
264 pub id: TaskId,
265
266 /// The prompt/instruction for the task.
267 pub prompt: String,
268
269 /// Current state.
270 pub state: TaskState,
271
272 /// Slot assigned to this task.
273 pub slot_id: Option<SlotId>,
274
275 /// Task result, available when state is `Completed` or `Failed`.
276 pub result: Option<TaskResult>,
277
278 /// Optional tags for filtering and grouping.
279 pub tags: Vec<String>,
280
281 /// Per-task config overrides (takes precedence over slot and global config).
282 pub config: Option<SlotConfig>,
283
284 /// When true, completed tasks transition to `PendingReview` instead of `Completed`.
285 #[serde(default)]
286 pub review_required: bool,
287
288 /// Maximum number of rejections before the task is marked as failed (default: 3).
289 #[serde(default = "default_max_rejections")]
290 pub max_rejections: u32,
291
292 /// Number of times this task has been rejected and re-queued.
293 #[serde(default)]
294 pub rejection_count: u32,
295
296 /// The original prompt before any rejection feedback was appended.
297 #[serde(default)]
298 pub original_prompt: Option<String>,
299}
300
301fn default_max_rejections() -> u32 {
302 3
303}
304
305/// The result of a completed task.
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct TaskResult {
308 /// The text output from Claude.
309 pub output: String,
310
311 /// Whether the task succeeded.
312 pub success: bool,
313
314 /// Cost in microdollars.
315 pub cost_microdollars: u64,
316
317 /// Number of turns used.
318 pub turns_used: u32,
319
320 /// Session ID from the execution.
321 pub session_id: Option<String>,
322
323 /// On failure: the CLI command that was run.
324 #[serde(skip_serializing_if = "Option::is_none")]
325 pub failed_command: Option<String>,
326
327 /// On failure: the exit code from the CLI.
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub exit_code: Option<i32>,
330
331 /// On failure: stderr output from the CLI.
332 #[serde(skip_serializing_if = "Option::is_none")]
333 pub stderr: Option<String>,
334}
335
336/// Filter criteria for listing tasks.
337#[derive(Debug, Clone, Default, Serialize, Deserialize)]
338pub struct TaskFilter {
339 /// Filter by state.
340 pub state: Option<TaskState>,
341
342 /// Filter by slot.
343 pub slot_id: Option<SlotId>,
344
345 /// Filter by tags (any match).
346 pub tags: Option<Vec<String>>,
347}