feat(agent): close knowledge loop — refuse complete_task without writeback (D.1+D.2)
D.1 — complete_task structural gate:
- hadDiscovery(ctx, session) reports whether the session ran `run` successfully
against a live target (NOT get_entity/list_lxcs — those are DB lookups, not
new facts). A trivial Q&A that only calls get_entity is a degenerate case
and must NOT be blocked.
- complete_task with outcome=success is REFUSED when hadDiscovery && !
hadEntityWriteback. The refusal fires BEFORE completeTask runs, so the
session stays in 'executing' state and the agent must call
update_entity_attributes/create_relationship then retry complete_task.
An explicit failure/partial is allowed through (the agent is acknowledging
it didn't finish — no reason to force writeback).
- Replaces the prior advisory warning (5.5) which the agent consistently
ignored. The agent saw the warning and ended the task anyway; this gate
makes the writeback a hard prerequisite for success.
D.2 — propose_plan auto-append writeback step:
- When the agent proposes a plan whose steps don't mention
update_entity_attributes or create_relationship, D.2 appends a final
'Write back: update_entity_attributes + create_relationship +
upsert_knowledge' step before persisting. The result string tells the
agent it was appended.
- With the seq-order enforcement (5.6) and D.1's complete_task gate, the
agent must complete the writeback step (and actually call the tools) to
finish. Neither relies on the agent reading SOUL.md.
- Removed the old advisory writeback nudge from propose_plan's result
string — D.2 makes it structural.
- Updated the propose_plan tool description to state both gates crisply.
Verification:
- TestHadDiscoveryAndWriteback: hadDiscovery true only after a successful
`run`; false after failed run, get_entity, or no calls. hadEntityWriteback
true only after update_entity_attributes/create_relationship.
- e2e against the live agent (oikos-nomos-1, v0.5.1):
- D.2: agent proposed 3 steps (no writeback); D.2 auto-appended step 4
'Write back: update_entity_attributes + ...'. Result string said
'(appended a writeback step — your plan didn't include one; step 4)'.
- D.1: agent ran `run` (uptime on lxc:gitea), called complete_task, was
REFUSED ('Refused: this session ran run against live targets (discovery)
but did not call update_entity_attributes...'). Agent self-corrected:
called update_entity_attributes, retried complete_task, succeeded.
Knowledge loop closed end-to-end.
Version 0.5.0 -> 0.5.1 (patch: structural enforcement of existing intent).
This commit is contained in:
@@ -726,6 +726,31 @@ func (s *store) hadEntityWriteback(ctx context.Context, sessionID string) bool {
|
||||
return count > 0
|
||||
}
|
||||
|
||||
// hadDiscovery checks whether this session ran `run` successfully against a
|
||||
// real target — i.e. discovered live state (versions, package counts, host
|
||||
// facts, service status) that the DB didn't have. Used by complete_task to
|
||||
// refuse success when discovery happened but no writeback followed (the
|
||||
// knowledge-loop drift the prior warnings failed to close — the agent
|
||||
// ignored advisory text, so D.1 makes it structural).
|
||||
//
|
||||
// Only `run` counts as discovery here, NOT get_entity/list_lxcs/etc. — those
|
||||
// are DB lookups, not new facts. A trivial Q&A ("status of lxc:dns?") that
|
||||
// only calls get_entity is a degenerate case (SOUL.md: "Don't invent
|
||||
// attributes that don't exist") and must NOT be blocked. Only sessions that
|
||||
// actually executed against a live target get the writeback gate.
|
||||
func (s *store) hadDiscovery(ctx context.Context, sessionID string) bool {
|
||||
if s == nil || sessionID == "" {
|
||||
return false // fail safe: don't block when we can't check
|
||||
}
|
||||
var count int
|
||||
s.pool.QueryRow(ctx, `
|
||||
SELECT COUNT(*) FROM agent_activity
|
||||
WHERE session_id = $1
|
||||
AND tool_name = 'run'
|
||||
AND success = true`, sessionID).Scan(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
// staleGoalSession is a goal-bearing task that's gone idle without reaching
|
||||
// a terminal state — the idle-sweep worker's work list (fix 2+3 of
|
||||
// plans/2026-07-11-task-completion-safety-net.md).
|
||||
|
||||
Reference in New Issue
Block a user