Skip to content

Bound the no-tool-call scolding loop - #38

Merged
shellygr merged 1 commit into
masterfrom
shelly/bound-no-tool-turns
Sep 8, 2026
Merged

Bound the no-tool-call scolding loop#38
shellygr merged 1 commit into
masterfrom
shelly/bound-no-tool-turns

Conversation

@shellygr

@shellygr shellygr commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

What breaks

_scolding_node exists because an AI turn with no tool call cannot advance the graph. It
appends "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 a
model 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_draft call and never recovered: 494 consecutive scoldings over roughly fifteen
minutes, which took the majority of that run's LLM spend, almost all of it cache-write on
re-sending the growing scolded conversation. The GraphRecursionError that ended it then
propagated and killed the whole run.

The bound

Three consecutive no-tool turns, then NoToolCallsError. The count walks the tail of the
message 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.

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>
@shellygr
shellygr requested a review from jtoman September 7, 2026 22:42

@jtoman jtoman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bizarre this happened!

@shellygr
shellygr merged commit 85be3db into master Sep 8, 2026
2 checks passed
@shellygr
shellygr deleted the shelly/bound-no-tool-turns branch September 8, 2026 18:47
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.

2 participants