Files
roboco/roboco/models/runtime.py
T
Renn F 803638e8e9 chore(models): upgrade the opus alias to claude-opus-4-8
MODEL_MAP["opus"] moves off claude-opus-4-6 to the newest Opus tier at the
same price; pricing already matched via the claude-opus-4 fragment, and a
new test_opus_is_priced guard keeps the alias priced. Fixtures, panel
mocks, and docs follow.
2026-07-25 00:13:15 +02:00

166 lines
6.5 KiB
Python

"""
Runtime Models
Domain types for the agent orchestrator system.
"""
from dataclasses import dataclass, field
from datetime import datetime
from enum import StrEnum
from pathlib import Path
from typing import Any
from uuid import UUID, uuid4
class OrchestratorAgentState(StrEnum):
"""Agent lifecycle states in the orchestrator."""
OFFLINE = "offline"
STARTING = "starting"
ACTIVE = "active"
WAITING_SHORT = "waiting_short" # Polling, agent still running
WAITING_LONG = "waiting_long" # Terminated, will respawn on event
IDLE = "idle"
STOPPING = "stopping"
@dataclass
class SpawnGitContext:
"""Git context passed when spawning an agent for a task."""
project_slug: str | None = None
branch_name: str | None = None
# Short id (task id[:8]) of the task whose per-task worktree the agent
# must edit in. Set only for tasks that carry a branch (a real worktree
# exists under {clone_root}/.worktrees/{task_short_id}/); branchless
# coordination roots leave it None so the spawn cwd falls back to the
# clone root.
task_short_id: str | None = None
@dataclass
class OrchestratorAgentConfig:
"""Configuration for an agent in the orchestrator."""
agent_id: str
blueprint_path: Path
model: str = "sonnet" # sonnet, opus, haiku, or any ollama-cloud tag
mcp_config_path: Path | None = None
working_directory: Path | None = None
# Orchestrator-assigned Claude Code session id (passed to the agent CLI as
# --session-id) so the agent's transcript can be located by id at finalize,
# regardless of which project/cwd dir Claude Code writes it to.
claude_session_id: str | None = None
# Git context for tasks requiring git workflow
git_context: SpawnGitContext | None = None
# Pre-rendered SessionStart briefing mounted as /app/briefing.md
briefing_path: Path | None = None
# Provider routing, resolved from `model_assignments` at spawn.
# provider_type drives `--model` CLI translation:
# `"anthropic"` → short-name lookup through MODEL_MAP,
# anything else (currently `"ollama_cloud"`) → pass `model` verbatim.
# provider_base_url + provider_auth_token are both NULL for the
# Anthropic default (container uses mounted ~/.claude credentials).
provider_type: str = "anthropic"
provider_base_url: str | None = None
provider_auth_token: str | None = None
# Services this spawn's project has opted into (sandbox_db_enabled + the
# project's sandbox_services) — an availability probe only. A non-empty
# list suppresses the legacy `_append_gate_env` prod-creds injection in
# favor of a marker env; actual provisioning happens on demand via the
# `request_sandbox` do-verb (`AgentOrchestrator.ensure_sandbox`), never here.
sandbox_available_services: list[str] = field(default_factory=list)
@dataclass
class AgentInstance:
"""A running Claude Code agent instance (Docker container)."""
id: UUID = field(default_factory=uuid4)
agent_id: str = ""
state: OrchestratorAgentState = OrchestratorAgentState.OFFLINE
container_id: str | None = None # Docker container ID
config: OrchestratorAgentConfig | None = None
started_at: datetime | None = None
last_activity: datetime | None = None
current_task_id: str | None = None
error_count: int = 0
waiting_for: str | None = None # For WAITING_LONG state
waiting_context: dict[str, Any] = field(default_factory=dict)
# UUID of the agent_spawn_sessions row created at spawn time.
# Used by _finalize_spawn_session for a direct-by-id lookup instead of a
# fragile (agent_slug, ended_at IS NULL) query.
usage_session_id: UUID | None = None
def __post_init__(self) -> None:
if not self.id:
self.id = uuid4()
@dataclass
class WaitingRecord:
"""Tracks what a WAITING_LONG agent is waiting for."""
agent_id: str
task_id: str | None
waiting_for: str # "blocker_resolution", "qa_result", "answer", "assignment"
waiting_since: datetime
context: dict[str, Any] = field(default_factory=dict)
# Model mapping for cost optimization
MODEL_MAP: dict[str, str] = {
"opus": "claude-opus-4-8",
"sonnet": "claude-sonnet-5",
"haiku": "claude-haiku-4-5-20251001",
}
# Default model by role
ROLE_MODEL_MAP: dict[str, str] = {
"developer": "sonnet",
# QA — emits structured review envelopes (pass_review's per-AC
# criteria_verified, the findings ledger) the haiku tier can't reliably
# produce: a haiku QA claims, gets validation-rejected, idles, respawns,
# and loops without passing (2026-07-24 live incident). Sonnet is the
# floor for every structured-verb lifecycle role.
"qa": "sonnet",
# PR reviewer — reviews untrusted external/fork PRs and gates root→master;
# highest-stakes review, so opus rather than the sonnet review tier.
"pr_reviewer": "opus",
"documenter": "sonnet",
"cell_pm": "sonnet",
# Main PM — cost is dominated by cache read/write of a large coordination
# context; Sonnet 5's cache-write is ~12x cheaper than Opus. Experiment: watch
# rework + coordination quality; revert here or via a per-slug DB override.
"main_pm": "sonnet",
"auditor": "opus",
"product_owner": "opus",
"head_marketing": "opus",
"ceo": "opus",
# Intake interviewer — reads real code and drafts the spec; needs to be sharp.
"prompter": "opus",
# Secretary — carries CEO authority; needs strong judgment.
"secretary": "opus",
}
# Per-role reasoning-effort override, passed as Claude Code's `--effort` CLI flag
# at spawn (a verified flag on Claude Code 2.x). Effort governs how much the model
# thinks and explores — thinking tokens and, more importantly, tool-call/turn
# count; lowering it on roles that don't need deep multi-step reasoning cuts turns,
# and turns drive the dominant cache-read cost. Delivery-critical reasoning roles
# (developer, pr_reviewer) keep the model default; main_pm is left at default to
# isolate its Opus→Sonnet-5 model experiment; Haiku roles (qa, documenter) ignore
# effort. Conservative and revertible — watch coordination/triage quality on the
# per-role panel and tune.
ROLE_EFFORT_MAP: dict[str, str] = {
# Cell PM — delegation + light triage, not deep reasoning; highest-volume
# role where lowering effort below the model default is defensible.
"cell_pm": "medium",
# Board + Auditor — triage / read-only observation, shallow reasoning depth.
"product_owner": "medium",
"head_marketing": "medium",
"auditor": "medium",
}