fix(agent): recover front-loaded answer when terminal turn is degenerate #1
Reference in New Issue
Block a user
Delete Branch "fix/agent-front-loaded-final-output"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The agent loop took the final answer only from the terminal (no-tool-call) turn (
agent.go:result.Output = resp.Text()). Models that "front-load" their full answer into an earlier turn that also calls a tool — then close with a trivial pointer like"(Already answered above.)"— had their real answer discarded and the pointer delivered.This is a recurring class of bug across open-weight models (observed with
glm-5.2:cloudin the downstream mort bot: the model produced its complete answer alongside acitecall, then emitted"(Already answered above.)"as the terminal turn, and that pointer is what the user saw). Well-behaved models (Claude/GPT) defer their answer to the terminal turn and are unaffected.Fix
New
agent/finalize.go:finalOutput(msgs, terminal)returns the terminal text when it's substantive, else falls back to the last substantive assistant content already in the transcript. Guardrails keep it safe:len ≤ 120and matches a back-reference regex (already answered,see above,as I said, …). Short-but-correct answers ("42","Yes.","It's down, restarting now.") are never treated as weak.≥ 200chars, or≥ 80and≥ 3×the terminal and not a preamble like"Let me check…").Tests
agent/finalize_test.go— predicate + recovery table tests (including false-positive guards) and two end-to-end loop tests via the fake provider: the front-loaded-answer shape is recovered with no extra model call, and a healthy deferred answer is delivered verbatim. Full suite green (go test ./...).🤖 Generated with Claude Code
The agent loop took the final answer only from the terminal (no-tool-call) turn. Models that "front-load" their answer into an earlier turn that also calls a tool — then close with a trivial pointer like "(Already answered above.)" — had their real answer discarded and the pointer delivered. This recurs across several open-weight models (glm-5.2, etc.); well-behaved models (Claude/GPT) defer their answer to the terminal turn and are unaffected. finalOutput() now falls back to the last substantive assistant content in the transcript when the terminal text is weak (empty, or a short back-reference). The predicate is narrow and back-reference-gated so short-but-correct answers ("42", "It's down, restarting now.") are never overridden; recovery only picks a prior turn that reads like a real answer, not a preamble. Zero extra model calls. Terminal-answer behavior for normal runs is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>