fix(agent): retry empty final turns instead of silently abandoning the task - #1896
Open
snimu wants to merge 4 commits into
Open
fix(agent): retry empty final turns instead of silently abandoning the task#1896snimu wants to merge 4 commits into
snimu wants to merge 4 commits into
Conversation
…pleting (ENG-5795) Providers occasionally end a stream with a normal stop reason but no usable output (no text, no tool calls; sometimes thinking-only). The agent loop treated any no-tool-call turn as completion, silently abandoning the task (-p printed nothing and exited 0). - agent: an empty final turn (no tool calls, no non-thinking content) is silently resent up to 3 attempts; empty attempts are dropped from the request context and never emitted as message_end, so they do not pollute the transcript. After the third empty response the turn ends as a normal turn error (print mode exits non-zero via the existing error path). - coding-agent: an RLM child whose final turn ended in a graceful error and that never replied now surfaces to the parent as rlm_child_failure with the error text instead of a bare completed-without-reply notice.
…G-5795) Review follow-up: an empty length-stop turn (Xiaomi MiMo overflow shape) and a silent stop-overflow turn (z.ai shape, usage.input past the context window) must pass through untouched so agent_end compaction recovery can see them. The retry now skips stopReason length entirely (truncation is a signal; an identical resend cannot change the outcome) and guards with the existing isContextOverflow detector instead of re-deriving overflow logic.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Providers occasionally end a turn with a normal stop reason and zero usable content: no text, no tool calls, sometimes a thinking-only message (observed and raw-SSE-confirmed for
moonshotai/kimi-k3via Prime Inference: single-chunk stream, no content,usage.output=0; ~1/10 raw probes). The agent loop treated any no-tool-call turn as completion, so the agent silently abandoned its task: print mode printed nothing and exited 0; interactive mode thought for minutes and then did nothing.The fix
streamAssistantResponsenow retries an empty final turn silently, up to 3 attempts. The retry happens beforemessage_endis emitted, which is the durability edge: discarded attempts are popped from the request context and never reach session persistence, usage accounting, or transcript consumers.error,aborted, andlengthstops are excluded - they are signals of their own, and an identical resend cannot help.!isContextOverflow(message, contextWindow)guards the retry, reusing the existing detector): a silent-overflow turn is empty by definition, and retrying it would burn full-context requests and then mask the stop reason that auto-compaction recovery keys on.Model returned an empty response ... 3 times in a row) and flows the existing error path: print mode exits non-zero with the message on stderr; session-level retry/backoff applies as for any other turn error.RLM child <name> failed: <error>to its parent instead of the misleading "completed without sending a reply" notice. (Found while wiring exhaustion propagation: graceful error turns resolvepromptAndWait, so the parent never saw the error text.)Tests
Eight new tests in
packages/agent(silent retry with clean transcript and exactly onemessage_end; whitespace-only retry; exhaustion after 3 attempts; no-retry guards for real content, tool calls, aborts, emptylengthstops, and silent-overflow turns - the last two pin the compaction-recovery contract) plus one inagent-session-recursion(parent receives the child failure message, no terminal notice). All retry-path tests were proven red against the pre-fix loop.Linear: ENG-5795
Note
Medium Risk
Changes core assistant-turn completion and
message_endtiming for every agent run; behavior is guarded and tested but affects persistence, events, and RLM parent notices.Overview
Fixes a case where providers return a normal stop with no usable output (thinking-only or whitespace text, no tool calls) and the agent loop treated that as a finished turn, so work stopped with no error.
Agent loop (
streamAssistantResponse): detects empty assistant turns (no tool calls and no non-empty text;error/aborted/lengthstops are not retried). It silently re-requests the model up to 3 times. Failed attempts arepopped from context and never getmessage_end, so they do not hit the transcript or retry context. After three empties in a row, the turn is finalized withstopReason: errorand a clearerrorMessage. Turns that matchisContextOverfloware left alone so compaction recovery still sees them.RLM: when a child finishes without a parent reply, if the last assistant message has
stopReason === 'error'(e.g. empty-response exhaustion), the parent getsrlm_child_failureinstead of a completed without reply notice.Tests cover retry hygiene, exhaustion, no-retry guards, overflow passthrough, and parent failure propagation.
Reviewed by Cursor Bugbot for commit 537e79e. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Retry empty final turns up to 3 times in
streamAssistantResponseinstead of abandoningMAX_EMPTY_TURN_ATTEMPTS=3andisEmptyAssistantTurnhelper; empty final turns (no tool calls, no non-whitespace text, stopReason not error/aborted/length) are silently dropped from context and retried up to 3 times, after which the message is finalized withstopReason='error'and an explanatoryerrorMessage.isContextOverflow.streamAssistantResponseAttempt, which no longer emitsmessage_end; the caller emits it exactly once after retry handling.AgentSession.runRlmChildso that when a child run ends with no parent reply but the last assistant message hasstopReason='error', the parent receivesrlm_child_failurewith the child's error message instead of the previouscompleted_without_replyterminal notice.message_endbeing emitted by the inner attempt function must now depend on the outerstreamAssistantResponse; in agent-loop.ts the only caller is updated.Macroscope summarized 537e79e.