fix(agent): auto-complete with partial outcome when writeback missing
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 auto-complete safety net required hadEntityWriteback to be true,
which meant sessions where the agent did the work but forgot to call
update_entity_attributes stayed stuck in 'executing' forever.

Relax: auto-complete fires if the agent did discovery (ran run),
regardless of writeback. If writeback happened → success; if not →
partial (honest: work was done but knowledge graph not updated).

VERSION 0.7.3 → 0.7.4
This commit is contained in:
2026-07-15 23:49:50 +02:00
parent ca2ff56a25
commit e4e426de7d
2 changed files with 17 additions and 13 deletions

View File

@@ -1 +1 @@
0.7.3
0.7.4

View File

@@ -362,30 +362,34 @@ func (a *agent) autoCompleteTrivialTask(ctx context.Context, sessionID, response
// 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 either (a) all plan
// steps are terminal OR (b) the agent did discovery (ran `run`) AND wrote
// back (update_entity_attributes/create_relationship), auto-complete. Path (b)
// catches the common case where the agent skips update_plan_step bookkeeping
// but still does the actual work + writeback — the D.1 gate already enforces
// writeback before completion, so if writeback happened, the work is done.
// steps are terminal OR (b) the agent did discovery (ran `run`), auto-complete.
// Path (b) catches the common case where the agent skips update_plan_step
// bookkeeping but still does the actual work — the D.1 gate already enforces
// writeback before `complete_task`, so if the agent forgot to complete at all,
// we close it out mechanically. If writeback happened → success; if not →
// partial (honest: work was done but knowledge graph wasn't updated).
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
}
// Path (a): all plan steps are terminal (done/failed/replaced/...).
// Path (b): agent did discovery + writeback but didn't close the loop.
discovery := a.store.hadDiscovery(ctx, sessionID)
writeback := a.store.hadEntityWriteback(ctx, sessionID)
// (a) all plan steps terminal, OR (b) agent did discovery (ran `run`).
shouldComplete := a.store.allPlanStepsTerminal(ctx, sessionID)
if !shouldComplete && a.store.hadDiscovery(ctx, sessionID) && a.store.hadEntityWriteback(ctx, sessionID) {
if !shouldComplete && discovery {
shouldComplete = true
}
if !shouldComplete {
return
}
outcome := "success"
if discovery && !writeback {
outcome = "partial" // honest: work done, knowledge graph not updated
}
summary := strings.TrimSpace(responseText)
summary = strings.SplitN(summary, "\n", 2)[0]
const maxLen = 120
@@ -395,9 +399,9 @@ func (a *agent) autoCompleteIfPlanDone(ctx context.Context, sessionID, responseT
if summary == "" {
summary = "All plan steps completed."
}
if err := a.store.completeTask(ctx, sessionID, "success", summary); err != nil {
if err := a.store.completeTask(ctx, sessionID, outcome, summary); err != nil {
slog.Error("nomos: auto-complete plan-done task failed", "session", sessionID, "error", err)
} else {
slog.Info("nomos: auto-completed task — agent didn't call complete_task", "session", sessionID)
slog.Info("nomos: auto-completed task — agent didn't call complete_task", "session", sessionID, "outcome", outcome)
}
}