fix(agent): reserve the final step for a terminal response - #272
Merged
Conversation
The reserved-final-step guard in the agent loop's catch block preserved the explicit max-steps/stalled outcome for ANY error thrown during finalization — including an AbortError from the user pressing Esc. Issue #107's acceptance criteria require cancellation semantics to remain unchanged, so classify the failure BEFORE the guard and let a user abort keep its `cancelled` reason/status; only provider and parse failures fall back to the preserved max-steps outcome (now with a structured warn log instead of a silent break). Also pins the surrounding behavior with three regressions: - a user abort during the reserved final inference still ends the turn as `cancelled`, never `stalled`/`max_steps` (fails without this fix); - a model that insists on a non-terminal tool at the budget edge gets exactly one repair round-trip, executes nothing, and the turn keeps the explicit stalled outcome; - maxSteps=1 deliberately makes the only step terminal — the tool call is rejected before execution and the repair pass must produce the final reply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Summary
A run that used every configured step for non-terminal tools ended as
stalledwith the synthetic(stopped: max_steps reached without a reply)— the model never got a chance to deliver the requested evidence summary even when the work was done (issue #107).This PR adopts the fix from #269 by @mikemikimike (cherry-picked with authorship preserved — thank you!) and hardens its one gap: cancellation semantics on the reserved final step.
Changes
From #269 (unchanged):
i === maxSteps - 1) becomes a finalization step: the prompt and the native tool payload expose onlyreply/finish(newStepContext.terminalOnlyflag), and a transient notice tells the model to summarize.executeStepInnerbefore any execution, routing through the existing one-shot repair.Fixed on top:
agent-loop.tspreservedmax_stepsfor ANY error — including anAbortErrorfrom the user pressing Esc mid-finalization, which would have relabelled a user cancellation as a stall. The failure is now classified before the guard (sameCancelledError/LlmFailure/classifyFailureladder the loop already uses), so a user abort keeps itscancelledreason and session status. The preserved max-steps path also logs a structured warning instead of breaking silently.Tests (
src/agent/agent-loop.test.ts)cancelled, neverstalled/max_steps— fails without the catch fix3LLM calls,1tool run,1 parse_retry)maxSteps=1now deliberately means the single step is terminal: the tool call is rejected before execution and the repair pass must produce the final reply — asserting the behavior change is intentionalValidation
npm run lint(tsc --noEmit) — cleannpx vitest run src/agent/— 13 files, 204/204 passedcancelled), passes with itFixes #107