Roles, capability profiles and help on the new loop (M2.6)
- roles.driver (unset: the planner's model) and roles.classifier (unset: the expert's); the mapping is pinned when an item opens, and the driver and the go-ahead reader run on the item's own (L9). - profiles: in configuration (tools, strengths, weaknesses), used in briefings: the driver's instructions say what its helper and expert are good at, the helper's brief who reads its handback; never a model name. - delegate(task) and ask_expert(question) for the driver. A helper works on a branch off the milestone's; on a handback its work comes back as uncommitted changes, otherwise it is discarded, so harness branches never hold commits only the clone has. ask_expert keeps its budget. - Triggers: failure_limit failed commands in a row (Driver's new "stuck" outcome) or the turn cap, mid-milestone, call a helper in once per milestone; if it fails too, the item waits on the operator. A delegation cut off by a crash is discarded at boot. - Evaluation capture: delegations and ask_expert as before, and the reader's verdicts as classifier pairs. Tests: help tests (merge-back, discard, needs a milestone, the stuck trigger and its once-per-milestone limit, the expert and its budget, pinning, briefings by profile, boot discarding a cut-off delegation), config tests for roles and profiles, and the walk delegating with a scripted helper that sometimes fails, with a delegations_settle property. The walk found helper commits stranded on a merged PR's branch, fixed by returning a helper's work uncommitted. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
1 parent
04c352b30c
commit
59fd00ce8c
19 files changed
+890
-38
No files matched your search
@@ -12,7 +12,7 @@ config change cannot split a task across two planners.
|
||||
|
||||
import json
|
||||
import time
|
||||
from dataclasses import asdict, dataclass
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, ClassVar, Protocol
|
||||
|
||||
import httpx
|
||||
@@ -107,21 +107,40 @@ class LLMClient(Protocol):
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RoleMap:
|
||||
executor: str
|
||||
planner: str
|
||||
helper: str
|
||||
expert: str
|
||||
executor: str # the old loop's (control.py), until M2.8
|
||||
planner: str # the old loop's, until M2.8
|
||||
helper: str # delegations (L9)
|
||||
expert: str # ask_expert (L9)
|
||||
driver: str = "" # drives the new loop; unset, the planner's model
|
||||
classifier: str = "" # reads go-aheads now, actions later (L4); unset, expert's
|
||||
|
||||
ROLES: ClassVar[tuple[str, ...]] = ("executor", "planner", "helper", "expert")
|
||||
ROLES: ClassVar[tuple[str, ...]] = (
|
||||
"executor",
|
||||
"planner",
|
||||
"helper",
|
||||
"expert",
|
||||
"driver",
|
||||
"classifier",
|
||||
)
|
||||
_FALLBACK: ClassVar[dict[str, str]] = {"driver": "planner", "classifier": "expert"}
|
||||
|
||||
def resolve(self, role: str) -> str:
|
||||
if role not in self.ROLES:
|
||||
raise LLMError(f"unknown role: {role!r}")
|
||||
return str(getattr(self, role))
|
||||
model = str(getattr(self, role))
|
||||
if not model and role in self._FALLBACK:
|
||||
return self.resolve(self._FALLBACK[role])
|
||||
return model
|
||||
|
||||
def pinned(self) -> dict[str, str]:
|
||||
"""The full resolved mapping, to record at task start (L9)."""
|
||||
return asdict(self)
|
||||
return {role: self.resolve(role) for role in self.ROLES}
|
||||
|
||||
@classmethod
|
||||
def from_pinned(cls, pinned: dict[str, str], default: "RoleMap") -> "RoleMap":
|
||||
"""A work item's pinned mapping; a role it predates comes from
|
||||
``default``."""
|
||||
return cls(**{r: pinned.get(r) or default.resolve(r) for r in cls.ROLES})
|
||||
|
||||
|
||||
class LiteLLMClient:
|
||||
|
||||
Reference in new issue
Block a user