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
@@ -57,8 +57,24 @@ class OpenAIChatRenderer:
|
||||
messages: list[dict[str, Any]] = []
|
||||
if system is not None:
|
||||
messages.append({"role": "system", "content": system})
|
||||
for stored in effective_events(events):
|
||||
kept = effective_events(events)
|
||||
answered = {e.event.call_id for e in kept if isinstance(e.event, ToolResult)}
|
||||
for stored in kept:
|
||||
messages.append(self._message_for(stored.event))
|
||||
# Providers reject a tool call with no result following it. One
|
||||
# can legitimately be missing: a crash between the call and its
|
||||
# result (A3), or a turn-ending gate tool. Render a placeholder so
|
||||
# the transcript stays replayable rather than poisoned.
|
||||
if isinstance(stored.event, ToolCall) and (
|
||||
stored.event.call_id not in answered
|
||||
):
|
||||
messages.append(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": stored.event.call_id,
|
||||
"content": "[no result recorded]",
|
||||
}
|
||||
)
|
||||
body: dict[str, Any] = {"model": model, "messages": messages}
|
||||
if tools:
|
||||
body["tools"] = tools
|
||||
|
||||
Reference in new issue
Block a user