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
@@ -11,7 +11,7 @@ crash mid-planning leaves a truthful transcript.
|
||||
|
||||
from typing import Any
|
||||
|
||||
from nisse_dev.canonical import AssistantText, ToolCall
|
||||
from nisse_dev.canonical import AssistantText, ToolCall, ToolResult
|
||||
from nisse_dev.control import AskHuman, PlanningError, SubmitPlan
|
||||
from nisse_dev.llm import LLMClient, LLMError, RoleMap
|
||||
from nisse_dev.state import StateStore
|
||||
@@ -142,6 +142,21 @@ class LLMPlanningAgent:
|
||||
continue
|
||||
if isinstance(event, ToolCall):
|
||||
action = self._interpret(event)
|
||||
if action is not None:
|
||||
# The gate is this call's outcome. Recording it keeps the
|
||||
# transcript renderable: providers reject a tool call with
|
||||
# no result, and the next planning turn replays this log.
|
||||
self._store.append_event(
|
||||
ToolResult(
|
||||
call_id=event.call_id,
|
||||
result=(
|
||||
"plan posted for approval; turn ended"
|
||||
if isinstance(action, SubmitPlan)
|
||||
else "question posted; awaiting the operator"
|
||||
),
|
||||
success=True,
|
||||
)
|
||||
)
|
||||
return action
|
||||
|
||||
def _interpret(self, call: ToolCall) -> SubmitPlan | AskHuman | None:
|
||||
|
||||
Reference in new issue
Block a user