fix(agent): read-only plans execute without approval
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

SOUL.md step 4: all-read-only plans skip the approval wait and execute
immediately. Only config_mutation/destructive steps need operator approval.
set_goal + propose_plan return text updated to match.

Fixes 3/4 eval failures where the agent proposed a plan then waited
for approval on a read-only task.
This commit is contained in:
2026-07-15 10:12:51 +02:00
parent f1dda7290a
commit 844cfe5888
2 changed files with 17 additions and 11 deletions

View File

@@ -185,7 +185,7 @@ func (a *agent) handleTaskTool(ctx context.Context, sessionID, name string, args
// what the SOUL.md "approve the plan, not each step" model actually
// describes. set_goal records the goal + flips status to executing
// and nothing more.
return "Goal set: " + goal + ". NEXT: pre-plan with read-only tools (search_knowledge, get_entity, list_lxcs, get_relations), then propose_plan (mandatory — even read-only tasks need a one-step plan; the run handler refuses without one). Do not call run before propose_plan.", true
return "Goal set: " + goal + ". NEXT: pre-plan with read-only tools (search_knowledge, get_entity, list_lxcs, get_relations), then propose_plan (mandatory — even read-only tasks need a one-step plan; the run handler refuses without one). After propose_plan: if all steps are read-only, execute immediately (no approval needed). If any step is config_mutation/destructive, stop and wait for operator approval.", true
case "propose_plan":
raw, _ := args["steps"].([]any)
@@ -249,7 +249,7 @@ func (a *agent) handleTaskTool(ctx context.Context, sessionID, name string, args
// the agent forgot), so the old advisory nudge is replaced by the
// structural gate: D.1 refuses complete_task without the actual
// update_entity_attributes/create_relationship calls.
result := fmt.Sprintf("Plan set (%d steps)%s. STOP. Wait for operator approval — do not call run yet. Approval vocabulary: \"approved\", \"yes\", \"go\", \"proceed\", \"continue\", \"ok\", \"go ahead\". On approval, advance with update_plan_step + run. Do not call propose_plan again.", len(persisted), appendedNote)
result := fmt.Sprintf("Plan set (%d steps)%s. If all steps are read-only, execute now — call update_plan_step(running) + run for each step, no approval needed. If any step is config_mutation/destructive, STOP and wait for operator approval (\"approved\", \"yes\", \"go\", \"proceed\", \"continue\", \"ok\", \"go ahead\"). Do not call propose_plan again.", len(persisted), appendedNote)
return result, true
case "update_plan_step":

View File

@@ -30,16 +30,22 @@ Call ONCE with EVERY step end-to-end. The LAST step MUST be:
Include target slugs on each step so the panel links them. If you omit the
writeback step, one is auto-appended.
### 4. GET APPROVAL — stop and wait
After proposing the plan, END YOUR TURN. Do not call `run`. Do not execute.
Wait for the operator to approve. Approval vocabulary: "approved", "yes",
"go", "proceed", "continue", "ok", "go ahead". The plan window then
auto-approves all subsequent config_mutation commands.
### 4. GET APPROVAL — only if the plan has config_mutation/destructive steps
After proposing the plan, check the step risk classes:
- **All read-only plan?** No approval needed. Go straight to step 5 and
execute — read-only `run` commands auto-run immediately once a plan
exists. Do NOT stop and wait.
- **Any config_mutation or destructive step?** END YOUR TURN. Do not call
`run`. Wait for the operator to approve. Approval vocabulary: "approved",
"yes", "go", "proceed", "continue", "ok", "go ahead". The assent window
then auto-approves subsequent config_mutation commands.
### 5. EXECUTE — `run` calls auto-run under the plan window
Once approved, advance each step with `update_plan_step` (running → done) +
`run`. Do NOT call `propose_plan` again — it is refused once a step has
started. Config_mutation commands auto-execute without per-action approval.
### 5. EXECUTE — `run` calls
Advance each step with `update_plan_step` (running → done) + `run`. Do NOT
call `propose_plan` again — it is refused once a step has started.
Read-only commands auto-run (no approval). Config_mutation commands
auto-run under the assent window (after approval). Destructive commands
always need explicit typed confirmation.
### 6. WRITE BACK + COMPLETE — `complete_task`
Call `update_entity_attributes` for every entity you ran `run` against