All articles
AI & LLMsOctober 1, 2026 6 min read

Agent Loops That Don't Spiral: Hard Limits, Budgets, and Escape Hatches

Autonomous agents fail in two ways: they stop too early, or they never stop. Here's how we design loops that terminate cleanly, stay under budget, and tell you why they quit.

Agent Loops That Don't Spiral: Hard Limits, Budgets, and Escape Hatches

The first agent we shipped burned $180 in Claude tokens overnight trying to rename a CSV column. It wasn't broken — it was doing exactly what we told it to, which was "keep going until the task is done." The task was never done, because the tool it needed didn't exist, and nothing in the loop told it to give up.

That's the real engineering problem with agents in 2026. The models are good enough. The frameworks are fine. What kills you in production is loop control: knowing when to stop, how much to spend, and what to do when the agent is clearly stuck but technically still "making progress."

The two failure modes nobody warns you about

Agents fail in two directions, and they're mirror images of each other.

Premature termination is when the model declares victory too early. It returns a plausible-looking answer after one tool call, skips the verification step, and your eval catches it a week later. This is mostly a prompting and eval problem.

Runaway loops are the expensive one. The agent calls search_docs, gets nothing useful, calls search_docs again with a slightly different query, gets nothing useful, calls list_files, calls search_docs a third time. Each step is individually reasonable. The aggregate is a slow-motion wallet fire.

The fix isn't a smarter model. It's a loop that knows its own limits.

The four budgets every agent needs

We wrap every production agent in four hard budgets. If any one trips, the loop exits with a structured reason. No exceptions, no "just one more step."

@dataclass
class AgentBudget:
    max_steps: int = 25           # tool calls + model turns
    max_input_tokens: int = 200_000
    max_output_tokens: int = 20_000
    max_wall_seconds: int = 120
    max_cost_usd: float = 2.00    # computed from token usage

@dataclass
class AgentState:
    steps: int = 0
    input_tokens: int = 0
    output_tokens: int = 0
    started_at: float = field(default_factory=time.time)
    cost_usd: float = 0.0
    tool_history: list[ToolCall] = field(default_factory=list)

The numbers above are starting points, not gospel. For a code-editing agent we'll push max_steps to 60 and max_cost_usd to $5. For a customer-facing agent that answers one question, 8 steps and $0.10 is more realistic. The point is that every agent has explicit, logged, enforceable limits before it runs.

Why four budgets and not one

You need all four because they fail in different ways:

  • Steps catches tight loops that use cheap tools.
  • Tokens catches agents that stuff ever-growing context into each turn.
  • Wall time catches tool calls that hang (a flaky API, a slow DB query).
  • Cost is the backstop. If the other three are miscalibrated, this is the one your finance team cares about.

In our experience, cost and steps trip most often. Wall time is the one people forget until a provider has a bad afternoon.

Detecting the loop before the budget does

Budgets are a safety net. The real goal is to notice the agent is stuck and exit gracefully with a useful partial result, not with a BUDGET_EXCEEDED error.

Two heuristics have earned their keep for us.

Repetition detection

If the agent calls the same tool with semantically similar arguments three times in a row, something is wrong. We hash normalized tool calls and track a sliding window:

def is_repeating(history: list[ToolCall], window: int = 4) -> bool:
    if len(history) < window:
        return False
    recent = history[-window:]
    signatures = [
        (call.name, hash_normalized_args(call.args))
        for call in recent
    ]
    # same tool + same args appearing 3+ times in last 4 calls
    return max(signatures.count(s) for s in signatures) >= 3

When this fires, we don't just kill the loop. We inject a system message: "You've called search_docs with similar queries three times without new information. Either try a fundamentally different approach, or call give_up with a summary of what you tried." Giving the model an explicit exit tool changes behavior more than any prompt tweak.

No-progress detection

Harder, but higher leverage. After every N steps, we ask a cheap model (Haiku, Gemini Flash, GPT-4o-mini — pick your poison) to score whether the agent has made meaningful progress toward the stated goal since the last checkpoint. If the score is low twice in a row, we exit.

This is a classic LLM-as-judge pattern and it's noisy, but it only has to be right most of the time to save real money. Keep the judge prompt short, keep the context minimal, and don't let the judge see its own prior scores — it'll anchor.

The escape hatch tool

Every one of our agents has a tool it can call voluntarily:

{
  "name": "abort",
  "description": "Call this when you cannot complete the task. Explain what you tried, what blocked you, and what a human would need to do.",
  "input_schema": {
    "type": "object",
    "properties": {
      "reason": {"type": "string"},
      "attempted": {"type": "array", "items": {"type": "string"}},
      "needs_human": {"type": "string"}
    },
    "required": ["reason", "attempted"]
  }
}

Both Anthropic's tool use docs and OpenAI's function calling docs are explicit that models will use whatever tools you give them if the descriptions are clear. In practice, Claude Sonnet and GPT-4-class models call abort appropriately maybe 60–70% of the time they should. That's not perfect, but it's 60–70% of outages that come with a readable explanation instead of a timeout.

The attempted field is gold for debugging. Pipe it into your observability stack and you'll find bad tool descriptions, missing capabilities, and ambiguous prompts within a week.

Logging that makes post-mortems possible

When a budget trips at 2am, you need to answer three questions fast: what was the agent trying to do, what did it actually do, and where did it go off the rails. That means logging every single turn with enough structure to query.

At minimum, per step:

  • Step number and elapsed wall time
  • Model, input tokens, output tokens, cost
  • Tool called and arguments (redact secrets)
  • Tool result size and status
  • Running totals of all four budgets

Store it as structured JSON, not prose logs. We dump these into whatever the client is already using — Datadog, Honeycomb, a Postgres table, doesn't matter. The important thing is you can WHERE agent_run_id = ? and reconstruct the whole trajectory.

What good exit reasons look like

Every agent run should end with one of a small, enumerated set of reasons:

  • completed — model returned a final answer
  • aborted_by_model — called the escape hatch
  • budget_steps / budget_tokens / budget_time / budget_cost
  • repetition_detected
  • no_progress
  • tool_error_unrecoverable
  • provider_error

Track the distribution over time. If budget_cost is 15% of your runs, you have a calibration problem. If aborted_by_model is climbing, your tools or prompts are degrading. These ratios are better product health signals than raw success rate.

Where humans belong in the loop

For anything that writes to a production system — sending email, charging a card, merging a PR, updating a customer record — we don't let the agent act alone past a threshold. The loop pauses, serializes its proposed action, and waits for an approval webhook. Both Anthropic and OpenAI document patterns for this in their agent guides, and it's the single highest-leverage safety control we've shipped.

The threshold matters. Approving every tool call makes the agent useless. Approving nothing makes it dangerous. We usually gate on: writes to external systems, actions above a cost threshold, and anything the agent itself flags as uncertain via a confidence field in its tool arguments.

Where we'd start

If you've got an agent in production or close to it, do these three things this week:

  1. Add the four budgets as hard limits. Not warnings, not soft caps — the loop exits. Pick conservative numbers and loosen them with data.
  2. Add an abort tool with a required attempted field, and log the results somewhere you'll actually read.
  3. Instrument exit reasons as an enum and chart the distribution. You'll learn more from one week of that chart than from a month of reading transcripts.

Everything else — repetition detection, LLM judges, approval gates — is worth building, but only after you can see what your agent is actually doing. Observability first, cleverness second. If you want help designing or auditing an agent system, that's part of what our team does on the AI engineering side.

#agents#LLMs#architecture#production

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project