Bound the no-tool-call scolding loop - #38
Merged
Merged
Conversation
A turn that ends without a tool call cannot advance the graph, so the scolding node tells the model that every turn must end with one and routes back for another try. When the model answers every scolding the same way, that is a cycle whose only exit is langgraph's recursion limit, hundreds of paid calls later. One run hit this after an agent wrote a rough draft: it returned an empty message, was scolded, returned another, and went round 494 times until the 1000-step limit fired. Three other agents in the same run reached the scolding node once each and recovered on the next turn, which is what makes a small bound safe. Stop after MAX_CONSECUTIVE_NO_TOOL_TURNS consecutive such turns and raise NoToolCallsError instead. A caller that supplies its own no_tools_fn, such as the interactive conversation handler, keeps its current behavior. Co-Authored-By: Claude Opus 5 (1M context) <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.
What breaks
_scolding_nodeexists because an AI turn with no tool call cannot advance the graph. Itappends "every AI turn must end with at least one tool call" and the graph routes
NO_TOOLS -> TOOL_RESULT -> router -> NO_TOOLS. Nothing counts how often that happens, so amodel that keeps ending its turn the same way spins until langgraph's recursion limit.
That happened in a cloud run. An agent returned an empty message right after a
write_rough_draftcall and never recovered: 494 consecutive scoldings over roughly fifteenminutes, which took the majority of that run's LLM spend, almost all of it cache-write on
re-sending the growing scolded conversation. The
GraphRecursionErrorthat ended it thenpropagated and killed the whole run.
The bound
Three consecutive no-tool turns, then
NoToolCallsError. The count walks the tail of themessage list looking for AI turns with no tool calls, allowing only this node's own scoldings
in between, so any real progress resets it.
Three is deliberately small. In that same run, three other agents reached the scolding node
exactly once each and recovered on the next turn, so the working case never sees the limit. An
agent that has ignored three scoldings is not going to be talked round by a fourth.
It raises rather than ending the graph because callers expect a result, and returning an empty
one would be a worse failure than a named error that says what happened.
The bound applies only to the default scolding node. A caller that supplies its own
no_tools_fn, the interactive conversation handler for instance, keeps its current behavior.Not covered here
#20 catches a tool call whose arguments were truncated by the output-token cap. That is a
different shape: there the model does call a tool. Here it calls nothing at all, so the router
never reaches the tools node and #20's handler never runs. Each one closes a loop the other
leaves open.