feat(agent): auto-complete tasks when all plan steps are terminal
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

The #1 remaining model reliability gap: the agent does the work (proposes
plan, executes all steps, writes back) but forgets to call complete_task,
leaving the session stuck in 'executing'. The eval showed 3/8 failures
with this pattern.

Fix: autoCompleteIfPlanDone — a structural safety net that fires at both
chat exit paths (normal completion + maxIterations). If the session has a
goal, the agent didn't call complete_task, and ALL plan steps are in a
terminal state (done/failed/replaced/skipped/blocked), auto-complete with
the agent's final text as the summary. Mirrors autoCompleteTrivialTask
but for structured tasks where the work is provably done.

Also: bump maxLLMRetries from 2 to 3 (complex multi-turn flows benefit
from one more retry on empty responses).
This commit is contained in:
2026-07-15 13:06:47 +02:00
parent 3c3b12df5e
commit e3b5fdc358
3 changed files with 66 additions and 0 deletions

View File

@@ -357,3 +357,39 @@ func (a *agent) autoCompleteTrivialTask(ctx context.Context, sessionID, response
slog.Error("nomos: auto-complete trivial task failed", "session", sessionID, "error", err)
}
}
// autoCompleteIfPlanDone is the structural safety net for "the agent did the
// work but forgot to call complete_task" — the #1 remaining model reliability
// gap after D.1's writeback gate. After a turn ends, if the session has a goal,
// the agent never called complete_task this turn, and ALL plan steps are in a
// terminal state (done/failed/replaced), auto-complete the task. This mirrors
// autoCompleteTrivialTask but for structured tasks where the work is provably
// done — the model just didn't close the loop.
func (a *agent) autoCompleteIfPlanDone(ctx context.Context, sessionID, responseText string) {
if a.store == nil || sessionID == "" || sessionID == "ephemeral" {
return
}
// Only auto-complete if the session is still executing (not already
// terminal — complete_task or a prior auto-complete already ran).
sess, err := a.store.getSession(ctx, sessionID)
if err != nil || sess.Status != "executing" {
return
}
if !a.store.allPlanStepsTerminal(ctx, sessionID) {
return
}
summary := strings.TrimSpace(responseText)
summary = strings.SplitN(summary, "\n", 2)[0]
const maxLen = 120
if len(summary) > maxLen {
summary = summary[:maxLen] + "…"
}
if summary == "" {
summary = "All plan steps completed."
}
if err := a.store.completeTask(ctx, sessionID, "success", summary); err != nil {
slog.Error("nomos: auto-complete plan-done task failed", "session", sessionID, "error", err)
} else {
slog.Info("nomos: auto-completed task — all plan steps terminal but agent didn't call complete_task", "session", sessionID)
}
}