fix(agent): fully silent assent — no system notes, no chat_assent events
The len(pending)>0 path still injected a brief note saying 'execution(s) are now running' — the model saw this, thought work was being done for it, and no-op'd (finish_reason=stop, content_len=0). Same confusion as the len(pending)==0 case, just from the other branch. Fix: both assent paths are now fully silent. No system note at all. The model sees 'go ahead' in the replayed history and responds naturally. Also removed chat_assent tool_use/tool_result emit events. These were persisted in the transcript and confused the model on replay — it saw its own 'tool calls' (chat_assent) and thought it had already acted. VERSION 0.7.4 → 0.7.5
This commit is contained in:
@@ -296,17 +296,6 @@ func (a *agent) chatWith(ctx context.Context, sessionID, message, systemInject s
|
||||
if ok {
|
||||
granted = append(granted, p.execID)
|
||||
slog.Info("nomos: chat-assent granted", "execution", p.execID, "status", status, "session", sessionID)
|
||||
// Mark as continued so the continuation worker doesn't
|
||||
// pick up this execution and call resumeSession while the
|
||||
// chat handler is still processing "go ahead." Without this,
|
||||
// two LLM calls run concurrently for the same session —
|
||||
// the chat handler's chat() and the worker's resumeSession()
|
||||
// — causing empty responses and race conditions.
|
||||
if execUUID, perr := uuid.Parse(p.execID); perr == nil {
|
||||
a.store.markContinued(ctx, execUUID)
|
||||
}
|
||||
emit(agentEvent{Type: "tool_use", Data: map[string]any{"name": "chat_assent", "args": map[string]any{"execution_id": p.execID}, "id": "assent-" + p.execID}, SessionID: sessionID})
|
||||
emit(agentEvent{Type: "tool_result", Data: map[string]any{"name": "chat_assent", "result": fmt.Sprintf("Approved via chat assent (%q). Status: %s.", message, status), "id": "assent-" + p.execID}, SessionID: sessionID})
|
||||
|
||||
// An explicit typed confirmation for a destructive action
|
||||
// opens a short, target-scoped window so the rest of a
|
||||
@@ -324,15 +313,22 @@ func (a *agent) chatWith(ctx context.Context, sessionID, message, systemInject s
|
||||
}
|
||||
if len(granted) > 0 {
|
||||
a.openAssentWindow(ctx, sessionID)
|
||||
// Brief note: only tells the model WHICH specific executions
|
||||
// were approved (so it doesn't re-request them). Does NOT say
|
||||
// "continue the plan" — the model already sees "go ahead" in
|
||||
// the replayed history and knows to continue. The old verbose
|
||||
// note ("they are now running... you MUST continue...") made
|
||||
// the model think work was being done for it, causing empty
|
||||
// responses (finish_reason=stop, content_len=0).
|
||||
note := fmt.Sprintf("[System: execution(s) %s were approved by the operator and are now running. Do not re-request those — check get_execution_status if you need the outcome.]", strings.Join(granted, ", "))
|
||||
messages = append(messages, openai.SystemMessage(note))
|
||||
// Mark approved executions as continued so the continuation
|
||||
// worker doesn't call resumeSession while the chat handler is
|
||||
// still processing "go ahead" — two concurrent LLM calls for the
|
||||
// same session cause empty responses and race conditions.
|
||||
for _, execID := range granted {
|
||||
if execUUID, perr := uuid.Parse(execID); perr == nil {
|
||||
a.store.markContinued(ctx, execUUID)
|
||||
}
|
||||
}
|
||||
// No system note. The model already sees "go ahead" in the
|
||||
// replayed history (the user message was saved to the DB before
|
||||
// chat() was called). The old note said "they are now running"
|
||||
// which made the model think work was being done for it —
|
||||
// causing empty responses (finish_reason=stop, content_len=0).
|
||||
// The approved executions are dispatched; the model will
|
||||
// continue with the remaining plan steps naturally.
|
||||
}
|
||||
if len(blocked) > 0 {
|
||||
note := fmt.Sprintf("[System: execution(s) %s are classified DESTRUCTIVE and were NOT approved by loose assent — you must ask the operator for an explicit typed confirmation before they can run. Once they do confirm, further destructive steps on that SAME target (e.g. finishing a stop-then-destroy sequence) will auto-run for 15 minutes without asking again — but a different target always needs its own confirmation.]", strings.Join(blocked, ", "))
|
||||
@@ -341,15 +337,9 @@ func (a *agent) chatWith(ctx context.Context, sessionID, message, systemInject s
|
||||
} else if assent && len(pending) == 0 {
|
||||
// The operator said "proceed"/"go ahead"/"yes" but there are no
|
||||
// pending approvals — the agent proposed a plan (via propose_plan)
|
||||
// and asked "shall I?" Open the assent window silently. Do NOT
|
||||
// inject a system note: the model already sees "go ahead" in the
|
||||
// replayed history (the user message was saved to the DB before
|
||||
// chat() was called, and getRecentMessages replays it). The old
|
||||
// verbose system note ("The operator approved your proposed plan.
|
||||
// Execute it now — call update_plan_step then run...") was redundant
|
||||
// with the user's "go ahead" and caused the model to no-op
|
||||
// (finish_reason=stop, content_len=0) — the model saw "approved" +
|
||||
// "running" and concluded there was nothing to do.
|
||||
// and asked "shall I?" Open the assent window silently. No system
|
||||
// note: the model sees "go ahead" in the replayed history and
|
||||
// responds naturally.
|
||||
a.openAssentWindow(ctx, sessionID)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user