Skip to main content

claude_pool/
types.rs

1//! Core types for claude-pool.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::PathBuf;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8/// Current time in milliseconds since epoch.
9pub fn now_ms() -> u64 {
10    SystemTime::now()
11        .duration_since(UNIX_EPOCH)
12        .unwrap_or_default()
13        .as_millis() as u64
14}
15
16// Re-export shared types from claude-wrapper so consumers don't need
17// to depend on both crates for basic config.
18pub use claude_wrapper::types::{Effort, PermissionMode};
19
20// ── Identifiers ──────────────────────────────────────────────────────
21
22/// Unique identifier for a task.
23#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub struct TaskId(pub String);
25
26/// Unique identifier for a slot.
27#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
28pub struct SlotId(pub String);
29
30// ── Slot types ─────────────────────────────────────────────────────
31
32/// Slot persistence mode.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
34#[serde(rename_all = "snake_case")]
35pub enum SlotMode {
36    /// Persistent slots stay alive across tasks, resuming sessions.
37    #[default]
38    Persistent,
39    /// Ephemeral slots are created per task and destroyed after.
40    Ephemeral,
41}
42
43/// Configuration for dynamic slot pool scaling.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ScalingConfig {
46    /// Minimum number of slots (default: 1).
47    pub min_slots: usize,
48    /// Maximum number of slots (default: 16).
49    pub max_slots: usize,
50}
51
52impl Default for ScalingConfig {
53    fn default() -> Self {
54        Self {
55            min_slots: 1,
56            max_slots: 16,
57        }
58    }
59}
60
61/// Configuration that applies to all slots by default.
62///
63/// Individual slots can override any of these fields via [`SlotConfig`].
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct PoolConfig {
66    /// Claude model to use (e.g. "claude-haiku-4-5-20251001").
67    pub model: Option<String>,
68
69    /// Permission mode for slots.
70    pub permission_mode: Option<PermissionMode>,
71
72    /// Maximum turns per task.
73    pub max_turns: Option<u32>,
74
75    /// System prompt prepended to all slot tasks.
76    pub system_prompt: Option<String>,
77
78    /// Allowed tools for slots.
79    pub allowed_tools: Vec<String>,
80
81    /// MCP servers available to slots.
82    pub mcp_servers: HashMap<String, serde_json::Value>,
83
84    /// Default effort level for slots (maps to `--effort`).
85    pub effort: Option<Effort>,
86
87    /// Fallback model to use if the primary model fails.
88    pub fallback_model: Option<String>,
89
90    /// Total budget cap for the pool in microdollars.
91    /// When cumulative spend across all slots reaches this limit,
92    /// new tasks are rejected with [`crate::Error::BudgetExhausted`].
93    pub budget_microdollars: Option<u64>,
94
95    /// Default slot mode.
96    pub slot_mode: SlotMode,
97
98    /// Maximum number of restarts per slot before marking as errored.
99    pub max_restarts: u32,
100
101    /// Enable git worktree isolation for slots.
102    pub worktree_isolation: bool,
103
104    /// Maximum time to wait for an idle slot before failing a task (in seconds).
105    pub slot_assignment_timeout_secs: u64,
106
107    /// Dynamic scaling configuration (min/max bounds).
108    pub scaling: ScalingConfig,
109
110    /// Enable unattended mode: use stricter permission defaults to prevent prompts.
111    /// When true, defaults to `DontAsk` permission mode if not explicitly set.
112    pub unattended_mode: bool,
113
114    /// If true, detect permission prompt patterns in stderr and provide actionable errors.
115    pub detect_permission_prompts: bool,
116
117    /// Enable the background supervisor loop for slot health monitoring.
118    ///
119    /// When enabled, the supervisor periodically checks for errored slots and
120    /// restarts them automatically (up to [`max_restarts`](Self::max_restarts)).
121    pub supervisor_enabled: bool,
122
123    /// Interval in seconds between supervisor health checks (default: 30).
124    ///
125    /// Only used when [`supervisor_enabled`](Self::supervisor_enabled) is true.
126    pub supervisor_interval_secs: u64,
127
128    /// Use `--strict-mcp-config` when passing MCP config to slots.
129    ///
130    /// Prevents slots from inheriting the coordinator's `.mcp.json`, which
131    /// avoids accidental recursive pool calls (a slot invoking `pool_run` on itself).
132    /// Default: `true`.
133    pub strict_mcp_config: bool,
134
135    /// Base directory for git worktrees (chains and slot isolation).
136    ///
137    /// Defaults to `.claude/pool-worktrees/` under the repo root, which keeps
138    /// worktrees within the project directory so Claude's `auto` permission
139    /// mode can write to them. Override if you need worktrees elsewhere.
140    pub worktree_base_dir: Option<PathBuf>,
141}
142
143impl Default for PoolConfig {
144    fn default() -> Self {
145        Self {
146            model: None,
147            permission_mode: Some(PermissionMode::Plan),
148            max_turns: None,
149            system_prompt: None,
150            allowed_tools: Vec::new(),
151            mcp_servers: HashMap::new(),
152            effort: None,
153            fallback_model: None,
154            budget_microdollars: None,
155            slot_mode: SlotMode::default(),
156            max_restarts: 3,
157            worktree_isolation: false,
158            slot_assignment_timeout_secs: 300,
159            scaling: ScalingConfig::default(),
160            unattended_mode: false,
161            detect_permission_prompts: true,
162            supervisor_enabled: false,
163            supervisor_interval_secs: 30,
164            strict_mcp_config: true,
165            worktree_base_dir: None,
166        }
167    }
168}
169
170/// Per-slot configuration overrides.
171///
172/// Any `Some` field here takes precedence over the corresponding field
173/// in [`PoolConfig`].
174#[derive(Debug, Clone, Default, Serialize, Deserialize)]
175pub struct SlotConfig {
176    /// Override model for this slot.
177    pub model: Option<String>,
178
179    /// Override permission mode for this slot.
180    pub permission_mode: Option<PermissionMode>,
181
182    /// Override max turns for this slot.
183    pub max_turns: Option<u32>,
184
185    /// Override system prompt for this slot.
186    pub system_prompt: Option<String>,
187
188    /// Additional allowed tools (merged with global).
189    pub allowed_tools: Option<Vec<String>>,
190
191    /// Additional MCP servers (merged with global).
192    pub mcp_servers: Option<HashMap<String, serde_json::Value>>,
193
194    /// Override effort level for this slot.
195    pub effort: Option<Effort>,
196
197    /// Override fallback model for this slot.
198    pub fallback_model: Option<String>,
199
200    /// Optional name/role for this slot (e.g. "reviewer", "coder").
201    pub role: Option<String>,
202
203    /// Optional human-readable name for the slot (e.g. "reviewer", "writer").
204    pub name: Option<String>,
205
206    /// Optional description of the slot's purpose or responsibilities.
207    pub description: Option<String>,
208
209    /// Override slot assignment timeout (in seconds).
210    pub slot_assignment_timeout_secs: Option<u64>,
211}
212
213/// Per-task configuration overrides.
214///
215/// Applied on top of the slot config for a single task execution. Unlike
216/// [`SlotConfig`], this struct contains only execution parameters — it has
217/// no identity fields (name, role, description) or slot lifecycle settings.
218#[derive(Debug, Clone, Default, Serialize, Deserialize)]
219pub struct TaskOverrides {
220    /// Override model for this task.
221    pub model: Option<String>,
222
223    /// Override permission mode for this task.
224    pub permission_mode: Option<PermissionMode>,
225
226    /// Override max turns for this task.
227    pub max_turns: Option<u32>,
228
229    /// Override system prompt for this task.
230    pub system_prompt: Option<String>,
231
232    /// Additional allowed tools for this task (merged with global and slot).
233    pub allowed_tools: Option<Vec<String>>,
234
235    /// Tools to explicitly disallow for this task.
236    pub disallowed_tools: Option<Vec<String>>,
237
238    /// Built-in tool selection for this task (e.g. "Bash", "Edit", "Read").
239    pub tools: Option<Vec<String>>,
240
241    /// Additional MCP servers for this task (merged with global and slot).
242    pub mcp_servers: Option<HashMap<String, serde_json::Value>>,
243
244    /// Override effort level for this task.
245    pub effort: Option<Effort>,
246
247    /// Override fallback model for this task.
248    pub fallback_model: Option<String>,
249
250    /// JSON schema for structured output validation.
251    pub json_schema: Option<serde_json::Value>,
252
253    /// Maximum budget cap for this task in USD.
254    pub max_budget_usd: Option<f64>,
255}
256
257/// Current state of a slot.
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
259#[serde(rename_all = "snake_case")]
260pub enum SlotState {
261    /// Slot is ready to accept a task.
262    Idle,
263    /// Slot is currently executing a task.
264    Busy,
265    /// Slot process has exited or been stopped.
266    Stopped,
267    /// Slot encountered an error and needs attention.
268    Errored,
269}
270
271/// Record of a slot in the pool.
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct SlotRecord {
274    /// Unique slot identifier.
275    pub id: SlotId,
276
277    /// Current state.
278    pub state: SlotState,
279
280    /// Per-slot config overrides.
281    pub config: SlotConfig,
282
283    /// The task currently being executed, if any.
284    pub current_task: Option<TaskId>,
285
286    /// Claude session ID for session resumption.
287    pub session_id: Option<String>,
288
289    /// Number of tasks completed by this slot.
290    pub tasks_completed: u64,
291
292    /// Cumulative cost in microdollars.
293    pub cost_microdollars: u64,
294
295    /// Number of times this slot has been restarted.
296    pub restart_count: u32,
297
298    /// Git worktree path, if worktree isolation is enabled.
299    pub worktree_path: Option<String>,
300
301    /// Path to the slot's temp `.mcp.json` file, if MCP servers are configured.
302    ///
303    /// Written once per slot (on first task that needs it) and reused across
304    /// subsequent tasks. Cleaned up on pool drain/shutdown.
305    #[serde(skip)]
306    pub mcp_config_path: Option<std::path::PathBuf>,
307}
308
309// ── Task types ───────────────────────────────────────────────────────
310
311/// Current state of a task.
312#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
313#[serde(rename_all = "snake_case")]
314pub enum TaskState {
315    /// Task is waiting for a slot.
316    Pending,
317    /// Task is being executed by a slot.
318    Running,
319    /// Task completed successfully.
320    Completed,
321    /// Task failed.
322    Failed,
323    /// Task was cancelled.
324    Cancelled,
325    /// Task completed but awaits coordinator approval before being considered done.
326    PendingReview,
327}
328
329/// A task submitted to the pool.
330#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct TaskRecord {
332    /// Unique task identifier.
333    pub id: TaskId,
334
335    /// The prompt/instruction for the task.
336    pub prompt: String,
337
338    /// Current state.
339    pub state: TaskState,
340
341    /// Slot assigned to this task.
342    pub slot_id: Option<SlotId>,
343
344    /// Task result, available when state is `Completed` or `Failed`.
345    pub result: Option<TaskResult>,
346
347    /// Optional tags for filtering and grouping.
348    pub tags: Vec<String>,
349
350    /// Per-task config overrides (takes precedence over slot and global config).
351    pub config: Option<TaskOverrides>,
352
353    /// When true, completed tasks transition to `PendingReview` instead of `Completed`.
354    #[serde(default)]
355    pub review_required: bool,
356
357    /// Maximum number of rejections before the task is marked as failed (default: 3).
358    #[serde(default = "default_max_rejections")]
359    pub max_rejections: u32,
360
361    /// Number of times this task has been rejected and re-queued.
362    #[serde(default)]
363    pub rejection_count: u32,
364
365    /// The original prompt before any rejection feedback was appended.
366    #[serde(default)]
367    pub original_prompt: Option<String>,
368
369    /// When this task was submitted (millis since epoch).
370    #[serde(default)]
371    pub created_at_ms: Option<u64>,
372
373    /// When this task started executing (millis since epoch).
374    #[serde(default)]
375    pub started_at_ms: Option<u64>,
376
377    /// When this task completed/failed/was cancelled (millis since epoch).
378    #[serde(default)]
379    pub completed_at_ms: Option<u64>,
380}
381
382fn default_max_rejections() -> u32 {
383    3
384}
385
386/// The result of a completed task.
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct TaskResult {
389    /// The text output from Claude.
390    pub output: String,
391
392    /// Whether the task succeeded.
393    pub success: bool,
394
395    /// Cost in microdollars.
396    pub cost_microdollars: u64,
397
398    /// Number of turns used.
399    pub turns_used: u32,
400
401    /// Wall-clock execution time in milliseconds.
402    #[serde(default)]
403    pub elapsed_ms: u64,
404
405    /// Model that executed this task.
406    #[serde(default, skip_serializing_if = "Option::is_none")]
407    pub model: Option<String>,
408
409    /// Session ID from the execution.
410    pub session_id: Option<String>,
411
412    /// On failure: the CLI command that was run.
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub failed_command: Option<String>,
415
416    /// On failure: the exit code from the CLI.
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub exit_code: Option<i32>,
419
420    /// On failure: stderr output from the CLI.
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub stderr: Option<String>,
423
424    /// Whether this task exceeded its per-task budget cap.
425    ///
426    /// Set by the pool after execution if the task's actual cost exceeded
427    /// the `max_budget_usd` from its [`TaskOverrides`]. The CLI enforces
428    /// the cap during execution, so this primarily flags tasks that ran
429    /// up against their limit.
430    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
431    pub budget_exceeded: bool,
432}
433
434impl TaskResult {
435    /// Create a successful task result.
436    pub fn success(output: impl Into<String>, cost_microdollars: u64, turns_used: u32) -> Self {
437        Self {
438            output: output.into(),
439            success: true,
440            cost_microdollars,
441            turns_used,
442            elapsed_ms: 0,
443            model: None,
444            session_id: None,
445            failed_command: None,
446            exit_code: None,
447            stderr: None,
448            budget_exceeded: false,
449        }
450    }
451
452    /// Create a failed task result.
453    pub fn failure(output: impl Into<String>) -> Self {
454        Self {
455            output: output.into(),
456            success: false,
457            cost_microdollars: 0,
458            turns_used: 0,
459            elapsed_ms: 0,
460            model: None,
461            session_id: None,
462            failed_command: None,
463            exit_code: None,
464            stderr: None,
465            budget_exceeded: false,
466        }
467    }
468
469    /// Set the model that executed this task.
470    pub fn with_model(mut self, model: impl Into<String>) -> Self {
471        self.model = Some(model.into());
472        self
473    }
474
475    /// Set the elapsed execution time in milliseconds.
476    pub fn with_elapsed_ms(mut self, elapsed_ms: u64) -> Self {
477        self.elapsed_ms = elapsed_ms;
478        self
479    }
480
481    /// Set the session ID.
482    pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
483        self.session_id = Some(session_id.into());
484        self
485    }
486
487    /// Set failure details (command, exit code, stderr).
488    pub fn with_failure_details(
489        mut self,
490        command: Option<String>,
491        exit_code: Option<i32>,
492        stderr: Option<String>,
493    ) -> Self {
494        self.failed_command = command;
495        self.exit_code = exit_code;
496        self.stderr = stderr;
497        self
498    }
499}
500
501impl TaskRecord {
502    /// Create a new pending task record with timestamps.
503    pub fn new_pending(id: TaskId, prompt: impl Into<String>) -> Self {
504        Self {
505            id,
506            prompt: prompt.into(),
507            state: TaskState::Pending,
508            slot_id: None,
509            result: None,
510            tags: vec![],
511            config: None,
512            review_required: false,
513            max_rejections: 3,
514            rejection_count: 0,
515            original_prompt: None,
516            created_at_ms: Some(now_ms()),
517            started_at_ms: None,
518            completed_at_ms: None,
519        }
520    }
521
522    /// Set tags.
523    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
524        self.tags = tags;
525        self
526    }
527
528    /// Set per-task config overrides.
529    pub fn with_config(mut self, config: Option<TaskOverrides>) -> Self {
530        self.config = config;
531        self
532    }
533
534    /// Enable review-required mode.
535    pub fn with_review(mut self, max_rejections: u32) -> Self {
536        self.review_required = true;
537        self.max_rejections = max_rejections;
538        self.original_prompt = Some(self.prompt.clone());
539        self
540    }
541
542    /// Transition the task to a new state, setting timestamps automatically.
543    ///
544    /// - `Running`: sets `started_at_ms`
545    /// - `Completed`, `Failed`, `Cancelled`, `PendingReview`: sets `completed_at_ms`
546    pub fn transition_to(&mut self, state: TaskState) {
547        self.state = state;
548        let now = now_ms();
549        match state {
550            TaskState::Running => {
551                self.started_at_ms = Some(now);
552            }
553            TaskState::Completed
554            | TaskState::Failed
555            | TaskState::Cancelled
556            | TaskState::PendingReview => {
557                self.completed_at_ms = Some(now);
558            }
559            TaskState::Pending => {}
560        }
561    }
562}
563
564/// Filter criteria for listing tasks.
565#[derive(Debug, Clone, Default, Serialize, Deserialize)]
566pub struct TaskFilter {
567    /// Filter by state.
568    pub state: Option<TaskState>,
569
570    /// Filter by slot.
571    pub slot_id: Option<SlotId>,
572
573    /// Filter by tags (any match).
574    pub tags: Option<Vec<String>>,
575}
576
577/// Aggregated metrics for the current pool session.
578///
579/// Provides developer-focused insights: spend tracking, task timing,
580/// and sizing data useful for optimizing pool usage patterns.
581#[derive(Debug, Clone, Default, Serialize, Deserialize)]
582pub struct SessionMetrics {
583    /// Total number of tasks submitted this session.
584    pub total_tasks: u64,
585    /// Number of completed tasks.
586    pub completed_tasks: u64,
587    /// Number of failed tasks.
588    pub failed_tasks: u64,
589    /// Number of cancelled tasks.
590    pub cancelled_tasks: u64,
591    /// Number of currently running tasks.
592    pub running_tasks: u64,
593    /// Number of pending tasks.
594    pub pending_tasks: u64,
595
596    /// Total spend across all tasks in microdollars.
597    pub total_spend_microdollars: u64,
598    /// Average cost per completed task in microdollars.
599    pub avg_cost_microdollars: u64,
600    /// Highest single-task cost in microdollars.
601    pub max_cost_microdollars: u64,
602
603    /// Average execution time for completed tasks in milliseconds.
604    pub avg_elapsed_ms: u64,
605    /// Median execution time for completed tasks in milliseconds.
606    pub median_elapsed_ms: u64,
607    /// Maximum execution time for completed tasks in milliseconds.
608    pub max_elapsed_ms: u64,
609    /// Minimum execution time for completed tasks in milliseconds.
610    pub min_elapsed_ms: u64,
611
612    /// Average number of turns per completed task.
613    pub avg_turns: f64,
614
615    /// Breakdown of tasks by model (count only).
616    pub tasks_by_model: HashMap<String, u64>,
617
618    /// Detailed per-model metrics.
619    pub model_breakdown: Vec<ModelMetrics>,
620
621    /// Session start time (millis since epoch).
622    pub session_start_ms: u64,
623    /// Session duration so far in milliseconds.
624    pub session_duration_ms: u64,
625}
626
627/// Per-model aggregated metrics.
628#[derive(Debug, Clone, Default, Serialize, Deserialize)]
629pub struct ModelMetrics {
630    /// Model identifier.
631    pub model: String,
632    /// Number of tasks run on this model.
633    pub task_count: u64,
634    /// Total spend for this model in microdollars.
635    pub total_cost_microdollars: u64,
636    /// Average cost per task in microdollars.
637    pub avg_cost_microdollars: u64,
638    /// Average execution time in milliseconds.
639    pub avg_elapsed_ms: u64,
640    /// Total turns used by this model.
641    pub total_turns: u64,
642}
643
644/// Filter criteria for session metrics queries.
645#[derive(Debug, Clone, Default, Serialize, Deserialize)]
646pub struct MetricsFilter {
647    /// Only include tasks created after this time (millis since epoch).
648    pub since_ms: Option<u64>,
649    /// Only include tasks created before this time (millis since epoch).
650    pub until_ms: Option<u64>,
651    /// Only include tasks with these tags (any match).
652    pub tags: Option<Vec<String>>,
653    /// Only include tasks that ran on this model.
654    pub model: Option<String>,
655}