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
@@ -29,7 +29,8 @@ _PROSE_NUDGE = (
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RunOutcome:
|
||||
kind: Literal["wait", "capped", "stopped", "failed"]
|
||||
# stuck: failed commands in a row hit the failure limit (L9's trigger)
|
||||
kind: Literal["wait", "capped", "stuck", "stopped", "failed"]
|
||||
tool: str = "" # the waiting tool called
|
||||
arguments: dict[str, Any] | None = None
|
||||
note: str = ""
|
||||
@@ -43,10 +44,12 @@ class Driver:
|
||||
*,
|
||||
max_turns: int = DEFAULT_MAX_TURNS,
|
||||
cancel: Cancel | None = None,
|
||||
failure_limit: int = 0, # 0: no limit
|
||||
) -> None:
|
||||
self._client = client
|
||||
self._model = model
|
||||
self._max_turns = max_turns
|
||||
self._failure_limit = failure_limit
|
||||
self._cancel = cancel or Cancel()
|
||||
|
||||
def run(
|
||||
@@ -59,6 +62,7 @@ class Driver:
|
||||
handlers: Mapping[str, Callable[[dict[str, Any]], tuple[str, bool]]]
|
||||
| None = None,
|
||||
checks: Mapping[str, Callable[[dict[str, Any]], str | None]] | None = None,
|
||||
model: str | None = None,
|
||||
) -> RunOutcome:
|
||||
"""``handlers`` answer control tools that don't end the run (e.g.
|
||||
start_milestone): each returns a result and whether it succeeded.
|
||||
@@ -66,6 +70,8 @@ class Driver:
|
||||
failed result and the run goes on, so the model puts it right."""
|
||||
handlers = handlers or {}
|
||||
checks = checks or {}
|
||||
model = model or self._model # the work item's pinned driver (L9)
|
||||
failures: list[str] = [] # the failed commands in a row, latest last
|
||||
offered = names(tools)
|
||||
nudged = False
|
||||
for turn in range(self._max_turns):
|
||||
@@ -74,7 +80,7 @@ class Driver:
|
||||
return RunOutcome("stopped", note=f"before turn {turn + 1}")
|
||||
try:
|
||||
produced = self._client.complete(
|
||||
transcript.view(), tools=tools, model=self._model, system=system
|
||||
transcript.view(), tools=tools, model=model, system=system
|
||||
)
|
||||
except LLMError as exc:
|
||||
return RunOutcome("failed", note=f"the driver's call failed: {exc}")
|
||||
@@ -132,4 +138,21 @@ class Driver:
|
||||
transcript.append(
|
||||
ToolResult(call_id=call.call_id, result=result, success=ok)
|
||||
)
|
||||
if call.tool == "run_command":
|
||||
failures = [] if ok else [*failures, _brief(call, result)]
|
||||
if self._failure_limit and len(failures) >= self._failure_limit:
|
||||
skip_calls(
|
||||
transcript, calls[index + 1 :], "not run: you're stuck"
|
||||
)
|
||||
return RunOutcome(
|
||||
"stuck",
|
||||
note=f"{len(failures)} commands failed in a row: "
|
||||
+ "; ".join(failures),
|
||||
)
|
||||
return RunOutcome("capped", note=f"hit the {self._max_turns}-turn cap")
|
||||
|
||||
|
||||
def _brief(call: ToolCall, result: str) -> str:
|
||||
"""A failed command and the end of what it said, for a helper's brief."""
|
||||
command = str(call.arguments.get("command", ""))[:120]
|
||||
return f"`{command}`: {result.strip()[-300:]}"
|
||||
Reference in new issue
Block a user