Repository navigation
Discard a failed turn's tool calls and admit the last allowed turn - #102
Merged
Merged
Conversation
A turn now runs the tool calls it emitted only when it ended in TOOL_CALLS or STOP. A turn that ended in ERROR (a stream error, a cancellation or a stream without its MessageStop), LENGTH, REFUSAL or CONTENT_FILTER discards them: they never run, emit no tool event and never reach the history, and the turn keeps its own outcome, so StopClassifier builds its error terminal instead of the loop going on. StopClassifier checks the turn limit only for a turn that would continue the loop, so maxTurns = N admits N model turns and a turn that ends the session on its own keeps its terminal on the last one.
…turn A PreStopHook Inject on the last allowed turn reversed its Success and started turn N+1 unchecked. The loop now classifies such a turn again as one with a message queued, so it ends in ErrorMaxTurns. Drop the stale turn-ceiling entry from the StopClassifier priority list.
A turn whose tool calls survive ended in TOOL_CALLS or STOP, never ERROR, so the tool-call branch and the text branch share one append rule. Co-Authored-By: Claude Opus 5.5 <[email protected]>
AgentLoop.iterate classifies an injected turn again, so its class javadoc says so rather than PreStopDecision's. Trim the StopClassifier turn-ceiling entry and the CHANGELOG paragraph of what the table rows already say. Co-Authored-By: Claude Opus 5.5 <[email protected]>
twoAllowedTurnsAdmitAToolTurnAndItsAnswer scripts the same tool turn and text turn and asserts the same single run and Success. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…mean Co-Authored-By: Claude Opus 5.5 <[email protected]>
No test failed when TurnRunner ran tool calls only for TOOL_CALLS turns, so a model that reports STOP with its calls (an unknown stop spelling maps to STOP) would have lost every tool call unnoticed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The cancellation test ran the loop by hand and so skipped the shared check that the run ended on the LoopEnded event carrying its terminal, which the spec asks of every test that proves a tool did not run. It now runs through the same helper on its virtual thread. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
Spec
audit-turn-outcomes(audit F08, F18).Tool calls of a failed turn no longer run.
TurnRunnerruns a turn's collected tool calls only when the turn ended inTOOL_CALLSorSTOP(endedCleanly, which replacesisRefused).ERROR(stream error, cancellation, a stream without itsMessageStop),LENGTH,REFUSALandCONTENT_FILTERdiscard them: no dispatch, noToolResult/ToolOutcome, no tool events, no tool calls in thePostModelTurnresponse, nothing in the history. The turn keeps its own outcome, soStopClassifierbuilds the matching terminal. The assistant message is appended in one place.maxTurns = Nadmits N turns.StopClassifierchecks the turn limit only when the turn's own ending would continue the loop (TOOL_CALLS, orSTOPwith queued messages). Wall-clock, budget and cancellation keep their place before it. When aPreStopHookInjectreverses aSuccess,AgentLoopclassifies the turn again as one with a message queued, so the limits still apply on the last allowed turn.Main code: +18 net lines (56 added, 38 removed across 7 files, mostly javadoc:
ResultMessage.ErrorMaxTurns,SessionLimits.maxTurns,PreStopHooknow say what the last allowed turn does).SessionSecretRedactionTest's stream-error case now asserts the redacted error text at thePostModelTurnhook, the one place that text still goes.Tests:
TurnOutcomesTestcovers acceptance cases 1–4 at the loop level (stream error,LENGTH,CONTENT_FILTER, cancellation while the stream is open, missingMessageStop, aSTOPturn carrying a tool call, the turn-limit cases and a last-turn pre-stopInject); twoStopClassifierTestcases cover the last allowed turn. Pre-merge review: hand mutation of every claimed behaviour (all killed), 20k–30k repetitions ofTurnOutcomesTestwith no failure.mvn -B clean verify javadoc:jar: 7857 tests run, 0 failures, 62 skipped (tests that need live provider keys); PMD, CPD and coverage gates pass.🤖 Generated with Claude Code