16. Unlimited Loop And Guardrails
This chapter explains why a fixed maximum round count is the wrong boundary for a production-style agent, and how the runtime still guards against repeated tool loops after removing that cap.
After reading this chapter, you should understand:
- why
maximum tool rounds: 5can fail valid tasks - why an unlimited loop still needs stop conditions
- how repeated call signatures detect loops
- why visible output commits before a fatal stop
Background
During real use, the agent hit:
Agent exceeded maximum tool rounds: 5.That was the wrong failure. Five rounds can be a normal coding-agent task.
The project removed the fixed round cap, but still needed a guard against hallucinated infinite tool loops.
Design Choice
Remove the global round cap.
Add a narrow repeated-tool-call guard.
The loop now stops when:
- a completed sampling round has no tool calls
- the end user aborts
- a fatal runtime error occurs
- the same tool call with the same output repeats too many times
Repeated Call Signature
The guard does not use a hash.
It compares:
tool name
normalized JSON arguments
model-visible tool outputArguments are normalized by parsing argumentsJson and stable-stringifying objects with sorted keys.
These are treated as the same:
{"path":"a.ts","limit":20}
{"limit":20,"path":"a.ts"}Input/Output Matching
The guard matches tool requests to tool executions by toolCallId, not by array position.
That means parallel tool execution does not break matching.
Output Commit Before Fatal Stop
If the repeated-call guard fires, the runtime first appends the current function_call_output to history, then stops.
This preserves the invariant:
function_call -> function_call_outputThat invariant matters for future resume/replay.
Self-Inspection
The current read tool can read the file that defines it:
lib/agent-builtins.tsThis is not reflection. It is normal file access through the current project path policy. It means the agent can inspect runtime source code, which is a useful diagnostic capability.
Tests
tests/agent-sampling-loop.test.ts verifies:
- more than five tool rounds can complete
- repeated identical
readcalls stop withREPEATED_TOOL_CALL - tool output is still committed before fatal stop
Current Status
This layer removes the artificial round cap and replaces it with runtime guardrails: abort, fatal errors, no-tool completion, and the repeated-call guard together decide when the loop ends.
Common Misunderstandings
Misunderstanding 1: Unlimited Means No Protection
Unlimited only removes the artificial round cap. The runtime still has abort, fatal errors, no-tool completion, and repeated-call guards.
Misunderstanding 2: Repetition Can Be Detected By Tool Name Alone
It cannot. The signature needs tool name, input, and output. Otherwise the same tool operating on different files would be misclassified.
Misunderstanding 3: The Last Tool Output Can Be Dropped When The Guard Fires
It should not be dropped. The model and debug surfaces need to see the last tool result before the runtime reports fatal stop.
Chapter Summary
This chapter moves the agent loop beyond a teaching limit: tasks are no longer cut off by a fixed round count, while semantic stop conditions and repeated-call guards still control runaway loops.
Chapter Checkpoint
The repeated-call guard has one case that can be run by name, no key needed:
npx tsx --test --test-name-pattern "repeated" tests/agent-sampling-loop.test.tsMeasured output:
✔ stops repeated identical tool-call loops without a global round limit (10.282708ms)
ℹ tests 1
ℹ pass 1
ℹ fail 0The case name is itself this chapter's design statement: there is no global round cap (allows more than five tool rounds before the final response in the same file proves runs can go past five rounds), but a repeated call with the same name, arguments, and output stops with REPEATED_TOOL_CALL, and the final tool output is still committed to history before the fatal stop.