On this page
- Why max turns are not a control loop
- The progress ledger contract
- Minimal implementation pattern
- Tradeoffs and failure modes
- What to instrument
- Decision rule for senior teams
- FAQ
- Is a progress ledger just another max-turn counter?
- What counts as progress?
- Where should the ledger live?
- How does this interact with retries?
- References
A production agent loop should earn its next iteration. The practical rule is simple: let the loop continue only when the previous turn records a measurable state delta, a narrower objective, or an explicit stop reason. Max-turn limits remain useful fuses, but progress ledgers are the control surface.
Why max turns are not a control loop
Most agent runtimes expose a turn cap because every loop needs a hard fuse. The OpenAI Agents SDK describes a runner that invokes an agent, checks for final output, follows handoffs, runs tool calls, and repeats until the workflow finishes. The same reference says max_turns can raise MaxTurnsExceeded when the loop runs too long. That is a necessary safety feature, not a complete operating model.
Graph runtimes make the same distinction visible. LangGraph documents GRAPH_RECURSION_LIMIT as a graph reaching the maximum number of steps before hitting a stop condition. The page notes that this often indicates an infinite cycle, while complex graphs can raise the configured recursion_limit when many steps are expected. In other words, the limit detects excessive repetition, but the application still has to define what useful progress looks like.
The engineering mistake is to treat the fuse as the policy. A ten-turn cap does not tell you whether turn six learned anything new. A recursion limit does not tell you whether a graph edge narrowed the task. A timeout does not distinguish a careful repair attempt from a loop that keeps rewriting the same invalid tool call. Senior teams need a richer invariant: repeat only when the system can explain why another repetition is justified.
That invariant matters because loops fail quietly before they fail loudly. They consume budget, fill traces with similar spans, generate duplicate tool calls, and make incident review feel like reading a chat transcript. By the time a cap fires, the useful question is already historical: which turn stopped making progress?
The progress ledger contract
A progress ledger is a small append-only record owned by the runtime. It is not a prompt convention and not a natural-language scratchpad. Each iteration writes structured evidence that the loop either changed state, reduced uncertainty, advanced a subgoal, or intentionally stopped.
A useful ledger entry has seven fields:
iteration: the runtime turn or graph step number.objective: the current work item in one sentence.input_digest: a stable hash or summary of the state presented to the model.action: the model call, tool call, handoff, retrieval, or human checkpoint attempted.delta: the concrete state change observed after the action.remaining: the next narrower objective, if more work is justified.stop_reason: success, no-progress, policy-blocked, needs-human, retry-scheduled, or error.
The decision rule is deliberately strict: a loop may continue only if delta or remaining changes in a way the controller can verify. If the agent says “I will try again” but the tool input, retrieved evidence, validation error, and plan are unchanged, the next iteration should not be granted automatically.
This pattern borrows a production instinct from retry systems. Temporal's documentation for retry policies names explicit properties such as initial interval, backoff coefficient, maximum interval, maximum attempts, and non-retryable errors. A retry loop is not just “try until it works”; it is a policy that separates transient failure from waste. Agent loops need the same separation between productive iteration and repetitive motion.
Minimal implementation pattern
The ledger does not need to be heavy. Start with a gate around the model-tool loop. The gate compares the new entry with the previous entry and decides whether the next turn is allowed.
from dataclasses import dataclass
from hashlib import sha256
from typing import Literal
StopReason = Literal[
"continue",
"success",
"no-progress",
"policy-blocked",
"needs-human",
"retry-scheduled",
"error",
]
@dataclass(frozen=True)
class LoopEntry:
iteration: int
objective: str
input_digest: str
action: str
delta: str
remaining: str
stop_reason: StopReason
def digest_state(state: str) -> str:
return sha256(state.encode("utf-8")).hexdigest()[:16]
def allows_next_turn(previous: LoopEntry | None, current: LoopEntry) -> bool:
if current.stop_reason != "continue":
return False
if previous is None:
return True
changed_input = current.input_digest != previous.input_digest
changed_delta = current.delta and current.delta != previous.delta
narrowed_goal = current.remaining and current.remaining != previous.remaining
return changed_input or changed_delta or narrowed_goal
def record_validation_turn(iteration: int, state: str, error: str, fix_plan: str) -> LoopEntry:
return LoopEntry(
iteration=iteration,
objective="repair the generated API request",
input_digest=digest_state(state),
action="validate_request_schema",
delta=f"validator returned {error}",
remaining=fix_plan,
stop_reason="continue" if fix_plan else "needs-human",
)
This example is intentionally small, but the placement is the important part. The controller creates the entry after each model response or tool result, before scheduling the next turn. The model can propose the remaining objective, but the runtime should compute or validate the evidence fields. Hash the relevant state. Record the validator error. Compare tool arguments. Check whether retrieved documents changed. If the evidence does not move, stop or escalate.
For graph systems, the same pattern belongs on edges. A node should not route back to itself because the model asked politely. It should route back because a reducer changed state, a tool produced new evidence, a checkpoint resumed with new input, or a retry policy scheduled another attempt. LangGraph's recursion-limit documentation is a reminder that cycles are normal in graphs, but every cycle needs a stop condition and a reason to continue LangChain Docs.
Tradeoffs and failure modes
The first tradeoff is overhead. A ledger adds a little state, a few comparisons, and more disciplined controller code. That cost is usually lower than debugging a runaway loop from unstructured chat history. It also gives you a compact incident artifact: iteration, action, delta, remaining objective, stop reason.
The second tradeoff is false progress. A model can produce a different sentence without moving the task. Do not use natural-language novelty as the only signal. Prefer machine-checkable deltas: a passing test count changed, a schema validation error changed, a retrieved document set changed, a tool result ID changed, a human approval receipt arrived, or the remaining objective became smaller.
The third tradeoff is retry ambiguity. Some failures deserve another attempt even when business state did not change. Network timeouts, rate limits, and locked resources are operational conditions, not reasoning progress. Put those attempts under a retry policy with backoff and maximum attempts, similar to the properties named in Temporal's retry policy documentation. Mark the ledger entry as retry-scheduled rather than pretending the agent reasoned its way forward.
The fourth tradeoff is user experience. Stopping early can feel conservative when the agent might have fixed the issue on the next try. The answer is not to remove the gate. The answer is to add better continuation evidence. For example, allow one repair turn when the validator error changes. Allow a second retrieval turn when the query changes and the document set changes. Require human input when the same invalid payload fails twice.
What to instrument
A progress ledger becomes more valuable when it shows up in traces. OpenTelemetry publishes Generative AI semantic conventions for recording AI operations under a shared vocabulary. Teams do not have to wait for a perfect agent-observability standard to start. Attach the ledger fields as span attributes or structured events around each model call, tool call, handoff, and graph transition.
At minimum, emit these operational signals:
loop.iteration: the turn or graph-step number.loop.objective: a short current objective.loop.action: model, tool, retrieval, handoff, checkpoint, or retry.loop.delta_kind: state-change, evidence-change, validation-change, no-progress, or retry.loop.stop_reason: success, no-progress, policy-blocked, needs-human, retry-scheduled, or error.loop.allowed_next: whether the controller granted another iteration.
Those fields let reviewers answer concrete questions. Which loop stopped making progress? Which tools cause repeated no-progress turns? Which graph edges hit recursion limits? Which tasks need better validators instead of bigger model budgets? Without a ledger, those questions turn into log archaeology.
Decision rule for senior teams
The memorable rule is this: a loop may spend another turn only after it writes a receipt for progress.
Use max turns as the outer guardrail. Use recursion limits to catch unexpected cycles. Use retry policies for transient infrastructure failures. But put the product decision in the progress ledger: did the last turn change the state, reduce the problem, or produce a clear stop reason?
This rule also improves prompts because it changes what the model is asked to do. Instead of “continue until done,” the controller asks the model to propose a next objective that can be checked against the ledger. Instead of “try another tool,” the runtime asks whether the next tool call differs from the previous one in a meaningful way. Instead of hiding failures in a longer context window, the system turns failure into a structured stop reason.
Production loops are not valuable because they are persistent. They are valuable because they can make bounded, inspectable progress. The ledger is how the runtime proves that progress before it spends the next turn.
There is also a governance benefit. A ledger turns continuation into a reviewable decision rather than an emergent property of prompt wording. Platform owners can set different continuation rules for low-risk summarization, write-path code changes, external API calls, and human-facing messages. Security reviewers can see when a policy block stopped the loop. Product owners can see when the loop asked for human input instead of burning more model calls. The mechanism is small, but it moves loop control from vibes into runtime policy.
FAQ
Is a progress ledger just another max-turn counter?
No. A max-turn counter answers “how many iterations have happened?” The ledger answers “what changed, and why is another iteration allowed?” Runtime limits such as max_turns in the OpenAI Agents SDK and recursion_limit in LangGraph are still useful, but they are fuses around the loop, not the loop policy itself.
What counts as progress?
Progress should be observable outside the model's private reasoning. Examples include changed state, a new validation error, a passing test, a different retrieved evidence set, a successful tool result, a narrower remaining objective, or an explicit stop reason. Reworded intent is not enough.
Where should the ledger live?
Put it in the runtime or graph controller, not only in the prompt. The model can draft the next objective, but the system should compute state digests, compare tool inputs, record validator outputs, and decide whether another turn is allowed.
How does this interact with retries?
Retries should be represented separately from reasoning progress. Infrastructure failures can use explicit retry policy fields such as backoff and maximum attempts, as described by Temporal. The ledger should mark those turns as retry-scheduled so incident reviewers can distinguish transient operations from agent reasoning.