Skip to main content

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}