use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
pub fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
pub use claude_wrapper::types::{Effort, PermissionMode};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TaskId(pub String);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SlotId(pub String);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SlotMode {
#[default]
Persistent,
Ephemeral,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScalingConfig {
pub min_slots: usize,
pub max_slots: usize,
}
impl Default for ScalingConfig {
fn default() -> Self {
Self {
min_slots: 1,
max_slots: 16,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoolConfig {
pub model: Option<String>,
pub permission_mode: Option<PermissionMode>,
pub max_turns: Option<u32>,
pub system_prompt: Option<String>,
pub allowed_tools: Vec<String>,
pub mcp_servers: HashMap<String, serde_json::Value>,
pub effort: Option<Effort>,
pub fallback_model: Option<String>,
pub budget_microdollars: Option<u64>,
pub slot_mode: SlotMode,
pub max_restarts: u32,
pub worktree_isolation: bool,
pub slot_assignment_timeout_secs: u64,
pub scaling: ScalingConfig,
pub unattended_mode: bool,
pub detect_permission_prompts: bool,
pub supervisor_enabled: bool,
pub supervisor_interval_secs: u64,
pub strict_mcp_config: bool,
pub worktree_base_dir: Option<PathBuf>,
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
model: None,
permission_mode: Some(PermissionMode::Plan),
max_turns: None,
system_prompt: None,
allowed_tools: Vec::new(),
mcp_servers: HashMap::new(),
effort: None,
fallback_model: None,
budget_microdollars: None,
slot_mode: SlotMode::default(),
max_restarts: 3,
worktree_isolation: false,
slot_assignment_timeout_secs: 300,
scaling: ScalingConfig::default(),
unattended_mode: false,
detect_permission_prompts: true,
supervisor_enabled: false,
supervisor_interval_secs: 30,
strict_mcp_config: true,
worktree_base_dir: None,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SlotConfig {
pub model: Option<String>,
pub permission_mode: Option<PermissionMode>,
pub max_turns: Option<u32>,
pub system_prompt: Option<String>,
pub allowed_tools: Option<Vec<String>>,
pub mcp_servers: Option<HashMap<String, serde_json::Value>>,
pub effort: Option<Effort>,
pub fallback_model: Option<String>,
pub role: Option<String>,
pub name: Option<String>,
pub description: Option<String>,
pub slot_assignment_timeout_secs: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TaskOverrides {
pub model: Option<String>,
pub permission_mode: Option<PermissionMode>,
pub max_turns: Option<u32>,
pub system_prompt: Option<String>,
pub allowed_tools: Option<Vec<String>>,
pub disallowed_tools: Option<Vec<String>>,
pub tools: Option<Vec<String>>,
pub mcp_servers: Option<HashMap<String, serde_json::Value>>,
pub effort: Option<Effort>,
pub fallback_model: Option<String>,
pub json_schema: Option<serde_json::Value>,
pub max_budget_usd: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SlotState {
Idle,
Busy,
Stopped,
Errored,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlotRecord {
pub id: SlotId,
pub state: SlotState,
pub config: SlotConfig,
pub current_task: Option<TaskId>,
pub session_id: Option<String>,
pub tasks_completed: u64,
pub cost_microdollars: u64,
pub restart_count: u32,
pub worktree_path: Option<String>,
#[serde(skip)]
pub mcp_config_path: Option<std::path::PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TaskState {
Pending,
Running,
Completed,
Failed,
Cancelled,
PendingReview,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskRecord {
pub id: TaskId,
pub prompt: String,
pub state: TaskState,
pub slot_id: Option<SlotId>,
pub result: Option<TaskResult>,
pub tags: Vec<String>,
pub config: Option<TaskOverrides>,
#[serde(default)]
pub review_required: bool,
#[serde(default = "default_max_rejections")]
pub max_rejections: u32,
#[serde(default)]
pub rejection_count: u32,
#[serde(default)]
pub original_prompt: Option<String>,
#[serde(default)]
pub created_at_ms: Option<u64>,
#[serde(default)]
pub started_at_ms: Option<u64>,
#[serde(default)]
pub completed_at_ms: Option<u64>,
}
fn default_max_rejections() -> u32 {
3
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskResult {
pub output: String,
pub success: bool,
pub cost_microdollars: u64,
pub turns_used: u32,
#[serde(default)]
pub elapsed_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
pub session_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub failed_command: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stderr: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub budget_exceeded: bool,
}
impl TaskResult {
pub fn success(output: impl Into<String>, cost_microdollars: u64, turns_used: u32) -> Self {
Self {
output: output.into(),
success: true,
cost_microdollars,
turns_used,
elapsed_ms: 0,
model: None,
session_id: None,
failed_command: None,
exit_code: None,
stderr: None,
budget_exceeded: false,
}
}
pub fn failure(output: impl Into<String>) -> Self {
Self {
output: output.into(),
success: false,
cost_microdollars: 0,
turns_used: 0,
elapsed_ms: 0,
model: None,
session_id: None,
failed_command: None,
exit_code: None,
stderr: None,
budget_exceeded: false,
}
}
pub fn with_model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn with_elapsed_ms(mut self, elapsed_ms: u64) -> Self {
self.elapsed_ms = elapsed_ms;
self
}
pub fn with_session_id(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
pub fn with_failure_details(
mut self,
command: Option<String>,
exit_code: Option<i32>,
stderr: Option<String>,
) -> Self {
self.failed_command = command;
self.exit_code = exit_code;
self.stderr = stderr;
self
}
}
impl TaskRecord {
pub fn new_pending(id: TaskId, prompt: impl Into<String>) -> Self {
Self {
id,
prompt: prompt.into(),
state: TaskState::Pending,
slot_id: None,
result: None,
tags: vec![],
config: None,
review_required: false,
max_rejections: 3,
rejection_count: 0,
original_prompt: None,
created_at_ms: Some(now_ms()),
started_at_ms: None,
completed_at_ms: None,
}
}
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.tags = tags;
self
}
pub fn with_config(mut self, config: Option<TaskOverrides>) -> Self {
self.config = config;
self
}
pub fn with_review(mut self, max_rejections: u32) -> Self {
self.review_required = true;
self.max_rejections = max_rejections;
self.original_prompt = Some(self.prompt.clone());
self
}
pub fn transition_to(&mut self, state: TaskState) {
self.state = state;
let now = now_ms();
match state {
TaskState::Running => {
self.started_at_ms = Some(now);
}
TaskState::Completed
| TaskState::Failed
| TaskState::Cancelled
| TaskState::PendingReview => {
self.completed_at_ms = Some(now);
}
TaskState::Pending => {}
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TaskFilter {
pub state: Option<TaskState>,
pub slot_id: Option<SlotId>,
pub tags: Option<Vec<String>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SessionMetrics {
pub total_tasks: u64,
pub completed_tasks: u64,
pub failed_tasks: u64,
pub cancelled_tasks: u64,
pub running_tasks: u64,
pub pending_tasks: u64,
pub total_spend_microdollars: u64,
pub avg_cost_microdollars: u64,
pub max_cost_microdollars: u64,
pub avg_elapsed_ms: u64,
pub median_elapsed_ms: u64,
pub max_elapsed_ms: u64,
pub min_elapsed_ms: u64,
pub avg_turns: f64,
pub tasks_by_model: HashMap<String, u64>,
pub model_breakdown: Vec<ModelMetrics>,
pub session_start_ms: u64,
pub session_duration_ms: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ModelMetrics {
pub model: String,
pub task_count: u64,
pub total_cost_microdollars: u64,
pub avg_cost_microdollars: u64,
pub avg_elapsed_ms: u64,
pub total_turns: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MetricsFilter {
pub since_ms: Option<u64>,
pub until_ms: Option<u64>,
pub tags: Option<Vec<String>>,
pub model: Option<String>,
}