Skip to content

fix(agent): reserve the final step for a terminal response - #272

Merged
plombeer31 merged 2 commits into
mainfrom
fix/issue-107-agent-stalls-at-max
Aug 31, 2026
Merged

fix(agent): reserve the final step for a terminal response#272
plombeer31 merged 2 commits into
mainfrom
fix/issue-107-agent-stalls-at-max

Conversation

@plombeer31

@plombeer31 plombeer31 commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

A run that used every configured step for non-terminal tools ended as stalled with 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):

  • The last allowed inference (i === maxSteps - 1) becomes a finalization step: the prompt and the native tool payload expose only reply/finish (new StepContext.terminalOnly flag), and a transient notice tells the model to summarize.
  • Non-terminal calls on that step are rejected at both parse sites in executeStepInner before any execution, routing through the existing one-shot repair.
  • If finalization still fails, the established explicit stalled/max-steps outcome is preserved — the loop never claims success or retries unbounded.

Fixed on top:

  • The finalization catch in agent-loop.ts preserved max_steps for ANY error — including an AbortError from the user pressing Esc mid-finalization, which would have relabelled a user cancellation as a stall. The failure is now classified before the guard (same CancelledError/LlmFailure/classifyFailure ladder the loop already uses), so a user abort keeps its cancelled reason and session status. The preserved max-steps path also logs a structured warning instead of breaking silently.

Tests (src/agent/agent-loop.test.ts)

  • reserves the final step for a terminal reply (from fix(agent): reserve final step for response #269)
  • a user abort during the reserved final inference still ends the turn as cancelled, never stalled/max_steps — fails without the catch fix
  • a model that insists on a non-terminal tool at the budget edge gets exactly one repair round-trip, executes nothing, and keeps the explicit stalled outcome (3 LLM calls, 1 tool run, 1 parse_retry)
  • maxSteps=1 now 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 intentional

Validation

  • npm run lint (tsc --noEmit) — clean
  • npx vitest run src/agent/ — 13 files, 204/204 passed
  • Verified the cancellation regression fails against fix(agent): reserve final step for response #269's code without the catch fix (reason came back non-cancelled), passes with it

Fixes #107

mikemikimike and others added 2 commits August 28, 2026 19:24
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>
@plombeer31
plombeer31 merged commit 45c71a6 into main Aug 31, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Agent stalls at max_steps instead of reserving a final response

2 participants