26725286d5
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)
291 lines
8.2 KiB
Python
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)
|