Add executor tool loop (A12/A15) — step 5b
- tools.py: four flat tools (read_file, write_file, list_dir, run_command) per A15's reduced manifest. Deliberately unsandboxed: the VM is the trust boundary (B3) and tool use is auto-approved inside an approved unit of work (A4), so path confinement would add failure modes without adding safety. What is enforced is operational — timeout with process-group kill so a backgrounded server cannot outlive its command, output truncation so a build log cannot eat the context window, closed stdin so prompts fail fast, and a scrubbed environment so project code never sees the agent's credentials - executor.py: renders the log, calls the executor role with the reduced manifest, dispatches tool calls, and checkpoints every result (A13). Escalation is observable, never self-assessed (A12): consecutive command failures and a hard turn cap. Failed reads do not count — looking around is not failing - Until delegation exists (step 7), escalation gates to the human with the failures attached - control.py: start milestone now runs the executor; a stopped milestone gates to AWAIT_INPUT Two bugs found by running it live against real models: - A turn-ending gate tool (submit_plan) left a tool_call with no tool_result, which providers reject outright, so every planning turn after the first failed with a 400. The planner now records the gate as the call's outcome, which is also the truthful transcript - The renderer now emits a placeholder for any tool_call lacking a result, so a crash between call and result (A3) cannot poison the transcript permanently Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
47b2bdfdbd
commit
0fd0b2e4a5
10 files changed
+784
-5
No files matched your search
@@ -59,6 +59,14 @@ class AskHuman:
|
||||
question: str
|
||||
|
||||
|
||||
class MilestoneExecutor(Protocol):
|
||||
"""What the orchestrator needs from an executor loop; the concrete one
|
||||
lives in nisse_dev.executor (imported there, not here, to keep the
|
||||
dependency pointing one way)."""
|
||||
|
||||
def run(self, milestone: int) -> Any: ...
|
||||
|
||||
|
||||
class PlanningAgent(Protocol):
|
||||
def plan(
|
||||
self,
|
||||
@@ -106,11 +114,13 @@ class Orchestrator:
|
||||
agent: PlanningAgent,
|
||||
operators: Iterable[str],
|
||||
roles: RoleMap | None = None,
|
||||
executor: "MilestoneExecutor | None" = None,
|
||||
) -> None:
|
||||
self._store = store
|
||||
self._transport = transport
|
||||
self._agent = agent
|
||||
self._roles = roles
|
||||
self._executor = executor
|
||||
self._operators = set(operators)
|
||||
self._data = _fresh_data()
|
||||
self._project: str | None = None
|
||||
@@ -259,7 +269,7 @@ class Orchestrator:
|
||||
self._transition(
|
||||
EXECUTING, cause=f"operator command: start milestone {milestone}"
|
||||
)
|
||||
self._transport.send_message(f"[stub] executing milestone {milestone}")
|
||||
self._execute_milestone(milestone)
|
||||
else:
|
||||
self._transport.send_message(
|
||||
f"plan approved; say 'start milestone {milestone}' to begin"
|
||||
@@ -313,6 +323,22 @@ class Orchestrator:
|
||||
f"milestone plan approved; say 'start milestone {milestone}' to begin"
|
||||
)
|
||||
|
||||
def _execute_milestone(self, milestone: int) -> None:
|
||||
if self._executor is None:
|
||||
self._transport.send_message(f"[stub] executing milestone {milestone}")
|
||||
return
|
||||
self._transport.send_message(f"executing milestone {milestone}")
|
||||
outcome = self._executor.run(milestone)
|
||||
if outcome.status == "completed":
|
||||
# PR_OPEN and the review loop arrive with the Gitea layer (B2);
|
||||
# until then a finished milestone simply reports and waits.
|
||||
self._transport.send_message(f"milestone {milestone} done: {outcome.note}")
|
||||
return
|
||||
self._transition(AWAIT_INPUT, cause=f"milestone {milestone} {outcome.status}")
|
||||
self._transport.ask_question(
|
||||
f"milestone {milestone} stopped ({outcome.status}): {outcome.note}"
|
||||
)
|
||||
|
||||
def _handle_answer(self, text: str) -> None:
|
||||
question = self._data.get("question")
|
||||
if question is None:
|
||||
|
||||
Reference in new issue
Block a user