Tool Reentrancy Is the Bug Class Your Function-Calling Layer Doesn't Know Exists
The agent took four hundred milliseconds to answer a simple question, then crashed with a recursion-limit error. The trace showed twenty-five tool calls. Reading the trace top-to-bottom, an engineer would conclude the agent was confused — calling the same handful of tools in slightly different orders, never converging. That conclusion is wrong. The agent wasn't confused. It was stuck in a cycle: tool A invoked the model, the model picked tool B, tool B's implementation invoked the model again to format its output, and the formatter chose tool A. The trace UI rendered four nested calls as four sibling calls in a flat list, and the cycle was invisible to the only human who could have caught it.
This is tool reentrancy, and it's a bug class your function-calling layer almost certainly doesn't model. Concurrency-safe code has decades of primitives for it: reentrant mutexes that count nested acquisitions by the same thread, recursion limits at the language level, stack inspection APIs, and a cultural understanding that any function which calls back into the runtime needs a clear contract about what re-entry is allowed. Tool-calling layers default to fire-and-forget. There is no call stack the runtime can inspect, no cycle detector before dispatch, no reentrancy attribute on the tool definition, and the trace UI is shaped like a log, not a graph. The result is that every tool catalog past about a dozen entries silently becomes a recursion the framework can't see.
The Failure Mode Hides Inside "Successful" Tool Calls
The way teams notice tool reentrancy in production is rarely "the agent looped." That's the failure case obvious enough to land in a postmortem. The far more common case: requests that complete successfully but cost three or four times what the cost model predicted, and an eval suite that scores fine because the final output is correct. The cycle ran, the model burned tokens discovering it, the loop broke when the outer turn budget hit, and the answer came out the other side because the model is good at recovering from its own confusion. The bill is the only place the bug shows.
LangGraph's default recursion limit of twenty-five was chosen specifically to catch this — not because twenty-five is a reasonable number of supersteps for a complex workflow, but because it's a backstop against agents that can't stop. The error message tells you the loop hit the wall. It does not tell you why. Production teams see GraphRecursionError and the first instinct is to raise the limit. That converts a noisy bug into a quiet one. The cycle keeps happening; it just runs longer before it gives up. DeepAgents users have reported that subagents silently inherit the default limit regardless of parent configuration, so a parent graph configured for fifty supersteps spawns children that hit twenty-five and crash without surfacing the right cause to the caller. The cycle and the limit are in different places. So is the diagnosis.
The recent literature has started to call out an even uglier variant: hidden cycles, where the trajectory looks structurally non-cyclic — different tools, different arguments, different retrieved documents — but the agent is in fact revisiting the same logical state. Structural analysis of the call stack misses these entirely. They only show up if you can compare the semantic content of consecutive states, which most observability tooling does not do.
Reentrancy Is the Right Word, Not Recursion
Calling this "recursion" lumps it together with the case where an agent legitimately decomposes a task into sub-tasks and asks itself for help. That recursion is fine. ReDel and the broader recursive-language-model literature show useful patterns where a parent agent spawns child agents to handle complex sub-problems, and the call tree is well-formed and bounded. The pathological cases are not recursion in the well-formed sense. They are reentrancy.
Reentrancy is a property from systems programming: the question is whether a function can be safely re-entered before its previous invocation finishes. A reentrant function is one that can. A non-reentrant function called reentrantly is the bug. The systems-programming world solved this by giving every function a clear answer to "what happens if I'm called again while my previous call is still on the stack?" Some functions are safe (pure functions, well-designed library code). Some functions need a reentrant mutex that counts nested acquisitions by the same caller. Some functions are explicitly non-reentrant and the runtime is supposed to catch the violation.
Tools in agent frameworks don't have this property declared anywhere. The framework doesn't know whether it's safe for the model, mid-execution of tool A, to choose tool A again. There's no annotation, no enforcement, and the most common runtime behavior is to dispatch the call as if it were the first one. If tool A holds external state (an open transaction, a paginated cursor, a partially-written file), calling it reentrantly is the kind of bug whose stack trace is on a different machine three hops away.
The Trace UI Is Lying By Omission
Most agent observability platforms render tool calls as a hierarchical tree of spans, and they do this well for the simple case where the agent is a flat loop of model-then-tool-then-model. The hierarchy degenerates to a flat list and the timeline view tells you everything you need.
The case it does not handle: a tool whose implementation invokes the model again, which then chooses another tool. That nested model call is logically a child of the tool, and the second tool is a grandchild of the first. Most tracing libraries either flatten the whole thing into siblings of the outer agent loop (because the inner model call uses the same client and the tracing context didn't propagate) or render the parent-child relationship technically correctly but in a UI optimized for shallow trees, where deep nesting wraps off the right side of the screen and gets visually lost.
- https://en.wikipedia.org/wiki/Reentrant_mutex
- https://en.wikipedia.org/wiki/Reentrancy_(computing)
- https://docs.langchain.com/oss/python/langgraph/errors/GRAPH_RECURSION_LIMIT
- https://github.com/langchain-ai/langgraph/discussions/1725
- https://github.com/langchain-ai/langgraph/issues/6731
- https://github.com/langchain-ai/deepagents/issues/1698
- https://arxiv.org/html/2511.10650
- https://arxiv.org/html/2408.02248v1
- https://www.braintrust.dev/articles/agent-observability-tracing-tool-calls-memory
- https://www.langchain.com/articles/agent-observability
