Files
Aperant/auto-claude/phase_config.py
T
AndyMik90 26725286d5 feat: introduce phase configuration module and enhance agent profiles
This commit adds a new phase configuration module that manages model and thinking level settings for different execution phases. It reads configurations from `task_metadata.json` and provides resolved model IDs for various phases, including spec creation, planning, coding, and QA.

Key changes include:
- New `phase_config.py` file to handle model ID mappings and thinking budgets.
- Updates to agent files (`coder.py`, `planner.py`, `loop.py`) to utilize phase-specific models and thinking levels.
- Modifications to the CLI and UI components to support per-phase configuration, enhancing the user experience for task creation and editing.

The new structure allows for optimized model selection and thinking depth based on the phase, improving overall task execution efficiency.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-12-19 10:24:44 +01:00

291 lines
8.2 KiB
Python

"""
Phase Configuration Module
===========================
Handles model and thinking level configuration for different execution phases.
Reads configuration from task_metadata.json and provides resolved model IDs.
"""
import json
from pathlib import Path
from typing import Literal, TypedDict
# Model shorthand to full model ID mapping
MODEL_ID_MAP: dict[str, str] = {
"opus": "claude-opus-4-5-20251101",
"sonnet": "claude-sonnet-4-5-20250929",
"haiku": "claude-haiku-4-5-20251001",
}
# Thinking level to budget tokens mapping (None = no extended thinking)
# Values calibrated for Claude Opus 4.5 extended thinking
THINKING_BUDGET_MAP: dict[str, int | None] = {
"none": None,
"low": 1024,
"medium": 5000, # Balanced thinking for light phases
"high": 10000, # Deep thinking for QA review
"ultrathink": 16000, # Maximum thinking for spec creation
}
# Spec runner phase-specific thinking levels
# Heavy phases use ultrathink for deep analysis
# Light phases use medium after compaction
SPEC_PHASE_THINKING_LEVELS: dict[str, str] = {
# Heavy phases - ultrathink (discovery, spec creation, self-critique)
"discovery": "ultrathink",
"spec_writing": "ultrathink",
"self_critique": "ultrathink",
# Light phases - medium (after first invocation with compaction)
"requirements": "medium",
"research": "medium",
"context": "medium",
"planning": "medium",
"validation": "medium",
"quick_spec": "medium",
"historical_context": "medium",
"complexity_assessment": "medium",
}
# Default phase configuration (matches UI defaults)
DEFAULT_PHASE_MODELS: dict[str, str] = {
"spec": "sonnet",
"planning": "opus",
"coding": "sonnet",
"qa": "sonnet",
}
DEFAULT_PHASE_THINKING: dict[str, str] = {
"spec": "medium",
"planning": "high",
"coding": "medium",
"qa": "high",
}
class PhaseModelConfig(TypedDict, total=False):
spec: str
planning: str
coding: str
qa: str
class PhaseThinkingConfig(TypedDict, total=False):
spec: str
planning: str
coding: str
qa: str
class TaskMetadataConfig(TypedDict, total=False):
"""Structure of model-related fields in task_metadata.json"""
isAutoProfile: bool
phaseModels: PhaseModelConfig
phaseThinking: PhaseThinkingConfig
model: str
thinkingLevel: str
Phase = Literal["spec", "planning", "coding", "qa"]
def resolve_model_id(model: str) -> str:
"""
Resolve a model shorthand (haiku, sonnet, opus) to a full model ID.
If the model is already a full ID, return it unchanged.
Args:
model: Model shorthand or full ID
Returns:
Full Claude model ID
"""
# Check if it's a shorthand
if model in MODEL_ID_MAP:
return MODEL_ID_MAP[model]
# Already a full model ID
return model
def get_thinking_budget(thinking_level: str) -> int | None:
"""
Get the thinking budget for a thinking level.
Args:
thinking_level: Thinking level (none, low, medium, high, ultrathink)
Returns:
Token budget or None for no extended thinking
"""
return THINKING_BUDGET_MAP.get(thinking_level, THINKING_BUDGET_MAP["medium"])
def load_task_metadata(spec_dir: Path) -> TaskMetadataConfig | None:
"""
Load task_metadata.json from the spec directory.
Args:
spec_dir: Path to the spec directory
Returns:
Parsed task metadata or None if not found
"""
metadata_path = spec_dir / "task_metadata.json"
if not metadata_path.exists():
return None
try:
with open(metadata_path) as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
return None
def get_phase_model(
spec_dir: Path,
phase: Phase,
cli_model: str | None = None,
) -> str:
"""
Get the resolved model ID for a specific execution phase.
Priority:
1. CLI argument (if provided)
2. Phase-specific config from task_metadata.json (if auto profile)
3. Single model from task_metadata.json (if not auto profile)
4. Default phase configuration
Args:
spec_dir: Path to the spec directory
phase: Execution phase (spec, planning, coding, qa)
cli_model: Model from CLI argument (optional)
Returns:
Resolved full model ID
"""
# CLI argument takes precedence
if cli_model:
return resolve_model_id(cli_model)
# Load task metadata
metadata = load_task_metadata(spec_dir)
if metadata:
# Check for auto profile with phase-specific config
if metadata.get("isAutoProfile") and metadata.get("phaseModels"):
phase_models = metadata["phaseModels"]
model = phase_models.get(phase, DEFAULT_PHASE_MODELS[phase])
return resolve_model_id(model)
# Non-auto profile: use single model
if metadata.get("model"):
return resolve_model_id(metadata["model"])
# Fall back to default phase configuration
return resolve_model_id(DEFAULT_PHASE_MODELS[phase])
def get_phase_thinking(
spec_dir: Path,
phase: Phase,
cli_thinking: str | None = None,
) -> str:
"""
Get the thinking level for a specific execution phase.
Priority:
1. CLI argument (if provided)
2. Phase-specific config from task_metadata.json (if auto profile)
3. Single thinking level from task_metadata.json (if not auto profile)
4. Default phase configuration
Args:
spec_dir: Path to the spec directory
phase: Execution phase (spec, planning, coding, qa)
cli_thinking: Thinking level from CLI argument (optional)
Returns:
Thinking level string
"""
# CLI argument takes precedence
if cli_thinking:
return cli_thinking
# Load task metadata
metadata = load_task_metadata(spec_dir)
if metadata:
# Check for auto profile with phase-specific config
if metadata.get("isAutoProfile") and metadata.get("phaseThinking"):
phase_thinking = metadata["phaseThinking"]
return phase_thinking.get(phase, DEFAULT_PHASE_THINKING[phase])
# Non-auto profile: use single thinking level
if metadata.get("thinkingLevel"):
return metadata["thinkingLevel"]
# Fall back to default phase configuration
return DEFAULT_PHASE_THINKING[phase]
def get_phase_thinking_budget(
spec_dir: Path,
phase: Phase,
cli_thinking: str | None = None,
) -> int | None:
"""
Get the thinking budget tokens for a specific execution phase.
Args:
spec_dir: Path to the spec directory
phase: Execution phase (spec, planning, coding, qa)
cli_thinking: Thinking level from CLI argument (optional)
Returns:
Token budget or None for no extended thinking
"""
thinking_level = get_phase_thinking(spec_dir, phase, cli_thinking)
return get_thinking_budget(thinking_level)
def get_phase_config(
spec_dir: Path,
phase: Phase,
cli_model: str | None = None,
cli_thinking: str | None = None,
) -> tuple[str, str, int | None]:
"""
Get the full configuration for a specific execution phase.
Args:
spec_dir: Path to the spec directory
phase: Execution phase (spec, planning, coding, qa)
cli_model: Model from CLI argument (optional)
cli_thinking: Thinking level from CLI argument (optional)
Returns:
Tuple of (model_id, thinking_level, thinking_budget)
"""
model_id = get_phase_model(spec_dir, phase, cli_model)
thinking_level = get_phase_thinking(spec_dir, phase, cli_thinking)
thinking_budget = get_thinking_budget(thinking_level)
return model_id, thinking_level, thinking_budget
def get_spec_phase_thinking_budget(phase_name: str) -> int | None:
"""
Get the thinking budget for a specific spec runner phase.
This maps granular spec phases (discovery, spec_writing, etc.) to their
appropriate thinking budgets based on SPEC_PHASE_THINKING_LEVELS.
Args:
phase_name: Name of the spec phase (e.g., 'discovery', 'spec_writing')
Returns:
Token budget for extended thinking, or None for no extended thinking
"""
thinking_level = SPEC_PHASE_THINKING_LEVELS.get(phase_name, "medium")
return get_thinking_budget(thinking_level)