Back to Blog
A red motor stop button on a metal control panel, representing a circuit breaker that halts a runaway agent loop

AI Agent Circuit Breakers: Stopping Runaway Tool Loops

8 min read

A tool-using agent can burn through model calls while repeating the same search, bouncing between two specialists, or retrying a business action whose result it failed to understand. A maximum-turn setting stops the run eventually, but it does not protect each side effect, identify lack of progress, or leave an operator enough evidence to resume safely. An AI agent circuit breaker should enforce several budgets, detect repeated states, and interrupt the run before another external mutation. This guide defines that controller, its state model, implementation order, failure behavior, and tests.

Why a maximum-turn limit is not enough

Most agent runtimes follow a simple cycle: ask the model, execute requested tools, append results, and ask the model again. The OpenAI Agents SDK describes this run loop and raises MaxTurnsExceeded when a configured limit is crossed. LangGraph applies a recursion limit when a graph reaches too many steps before a stop condition. Both controls are useful. Neither can decide whether step seven represents real progress or the seventh copy of the same failed action.

A single limit also combines different risks into one number. Twenty read-only catalog searches may be acceptable. Two attempts to submit the same invoice may not be. A run can stay under its turn limit while exceeding its cost budget, deadline, or permitted number of writes. It can also hit the turn limit immediately after a tool completes a side effect, leaving the workflow unable to tell whether another attempt would duplicate it.

Treat maximum turns as the final backstop. Put a progress-aware controller around each model and tool transition so the system can stop at the risk boundary rather than several steps after it.

Define terminal states before detecting loops

Loop detection starts with an explicit run contract. The controller needs to know which outcomes end normally, which can be retried automatically, and which require a person.

Use a small terminal-state set:

  • completed: the required output exists and all mandatory postconditions pass.
  • rejected: policy or business rules deny the request.
  • needs_input: required information is missing and the agent has a specific question.
  • interrupted: a budget or progress rule stopped execution.
  • failed: an unrecoverable platform or tool error ended the run.

Do not let free-form model text choose the state by itself. Validate completion against deterministic postconditions. A ticket-creation workflow is complete only when it has a stable ticket ID, the expected project, and the requested fields. A sentence saying the ticket was created is not evidence.

The same rule applies to missing input. The agent should name the missing field and show why it cannot proceed. Otherwise, needs_input can become another route for vague retries.

Give the run independent execution budgets

Create a budget object when the workflow starts. Keep each dimension separate because each controls a different failure mode.

@dataclass
class RunBudget:
    max_turns: int
    deadline_at: datetime
    max_model_cost_usd: Decimal
    max_tool_calls: int
    max_side_effects: int
    max_same_intent: int
    max_no_progress_steps: int

@dataclass
class RunCounters:
    turns: int = 0
    tool_calls: int = 0
    side_effects: int = 0
    model_cost_usd: Decimal = Decimal("0")
    no_progress_steps: int = 0

Check the deadline and remaining budget before every model request and tool call. Reserve estimated cost before calling a model, then reconcile against actual usage when the provider returns it. Reserve a side-effect slot before execution, not afterward. If no slot remains, the tool must not run.

The side-effect count should reflect business mutations, not HTTP requests. A payment API retry with one idempotency key is one intended mutation, although it may involve several transport attempts. A second payment intent is another side effect. Store that distinction in the tool adapter rather than asking the model to infer it.

Budget values should come from the workflow type and risk class. A read-only research agent can have a larger turn allowance than an accounts-payable agent. OWASP's excessive-agency guidance recommends limiting functionality, permissions, and autonomy, with human approval for high-impact actions. The budget is one enforcement layer for that policy, not a replacement for downstream authorization.

Detect progress from state changes

A loop detector needs a compact fingerprint of what the agent intended, what the tool observed, and what business state changed. Raw prompt comparison is too brittle because the model can rephrase the same request on every turn.

Build an intent fingerprint from normalized fields:

intent_fingerprint = hash(
  tool_name,
  trusted_tenant_id,
  normalized_resource_id,
  normalized_operation,
  canonical_arguments_without_nonce
)

outcome_fingerprint = hash(
  tool_status,
  stable_result_identifiers,
  normalized_error_class,
  business_version_after_call
)

Exclude timestamps, request IDs, tracing IDs, and generated wording. Those values change even when the agent makes no progress. Include tenant, resource, operation, and stable business version because those values distinguish actions that only look similar.

After each step, compare the new fingerprints with recent history. Increment no_progress_steps when any of these conditions holds:

  1. The same intent produces the same outcome.
  2. Two or more agents hand the task back without changing required fields or business state.
  3. The model requests a write that the policy layer already denied for the same reason.
  4. A read tool returns the same resource version and the plan does not change.
  5. The agent alternates between a small set of intents without satisfying a postcondition.

Reset the counter only when a named postcondition advances. More tokens, a longer plan, or different wording do not count.

Stop before the next mutation

Place the circuit-breaker decision between proposal and execution. The sequence should be model proposal, argument validation, authorization, budget reservation, progress check, approval when required, and tool execution. Checking after execution is too late for duplicate emails, payments, calendar events, or permission changes.

A practical controller can return one of four decisions:

class Decision(Enum):
    ALLOW = "allow"
    ALLOW_READ_ONLY = "allow_read_only"
    INTERRUPT = "interrupt"
    REQUIRE_APPROVAL = "require_approval"

def decide(run, proposed_call):
    if run.deadline_expired() or run.model_budget_exhausted():
        return Decision.INTERRUPT
    if proposed_call.is_mutation and run.side_effect_budget_exhausted():
        return Decision.INTERRUPT
    if run.same_intent_count(proposed_call) >= run.budget.max_same_intent:
        return Decision.INTERRUPT
    if run.no_progress_steps >= run.budget.max_no_progress_steps:
        return Decision.ALLOW_READ_ONLY if proposed_call.can_diagnose else Decision.INTERRUPT
    if proposed_call.requires_human_approval:
        return Decision.REQUIRE_APPROVAL
    return Decision.ALLOW

The optional read-only allowance helps the agent collect one final diagnostic fact without permitting another mutation. Keep it narrow. It should call an approved status endpoint, not trigger a broad new planning cycle.

Do not disable the outer runtime limit. The OpenAI run API exposes configurable turn limits and error handling, while LangGraph's recursion limit catches graph-level cycles. Keep those hard stops above the controller in case a bug bypasses your progress logic.

Persist an interruption record

An interruption is an operational outcome, not an exception string. Store a typed record before releasing the worker:

{
  "run_id": "run_0187",
  "state": "interrupted",
  "reason": "repeated_intent_same_outcome",
  "budget_snapshot": {
    "turns": 8,
    "tool_calls": 6,
    "side_effects": 1,
    "model_cost_usd": "0.42"
  },
  "last_intent": "update_crm_contact",
  "last_stable_resource_id": "contact_42",
  "business_version": "17",
  "side_effect_status": "confirmed",
  "safe_next_actions": ["inspect", "resume_read_only", "close"],
  "resume_token_version": 3
}

Store references to protected payloads rather than copying secrets or personal data into the record. Include the last confirmed business version and side-effect status. If the tool response was lost after a request left your system, mark the side effect unknown and force reconciliation against the provider before resume.

Durable workflow systems separate workflow failure, activity failure, timeout, and retry context. Temporal's Python error-handling guidance shows why typed failures matter: retry behavior depends on what failed and how the failure is represented. Apply the same principle even if your orchestrator is a queue and database rather than Temporal.

Resume with a new decision, not the old loop

Never resume by replaying the last model output. Reload trusted business state, verify the workflow version, and create a new execution decision.

Require these checks:

  • The operator is authorized for the tenant and action.
  • The workflow definition and policy versions are compatible with the saved checkpoint.
  • Every previous mutation is classified as confirmed, rejected, or unknown.
  • Unknown mutations are reconciled through a provider status or idempotency lookup.
  • Remaining budgets are recalculated instead of reset automatically.
  • Any approved exception names the exact action, resource version, expiry, and allowed attempt count.

If the operator changes the inputs, start a new run linked to the interrupted one. This keeps the audit trail honest and avoids treating a materially different request as continuation.

Test the controller with hostile and ordinary runs

Unit tests should cover each decision rule, but the useful failures appear in complete runs. Build fixtures that drive the real model-tool loop through these cases:

  1. A search tool returns the same empty result under three rephrased queries.
  2. Two specialist agents hand off to each other without changing state.
  3. A write succeeds, its response times out, and the agent proposes the write again.
  4. A policy denial is rephrased and submitted repeatedly.
  5. A legitimate long-running investigation makes measurable progress for more turns than the usual limit.
  6. Model cost reaches its budget before the turn limit.
  7. The deadline expires while a tool is running.
  8. An operator resumes after the business record changed.

Assert the negative behavior: no tool mutation occurs after interruption, no budget can go below zero, and no unknown side effect is retried without reconciliation. Also assert useful behavior. A long run with changing resource versions and advancing postconditions should not trip the no-progress breaker.

Track interruption reason, workflow type, tool, and whether an operator resumed or closed the run. A rising repeated_intent_same_outcome rate usually indicates poor tool-result semantics or an unreachable terminal condition. A rising cost-budget rate may require a cheaper model path or a smaller task. Do not respond by raising every limit globally.

Implementation checklist

Start with one side-effecting workflow and implement the boundary in this order:

  • Define deterministic completion and failure postconditions.
  • Classify every tool as read-only or side-effecting.
  • Add turn, deadline, cost, tool-call, and side-effect budgets.
  • Create stable intent and outcome fingerprints.
  • Detect repeated outcomes and cyclic handoffs.
  • Run the breaker before authorization is converted into execution.
  • Persist a typed interruption record with side-effect certainty.
  • Add reconciliation and version checks to resume.
  • Test repeated writes, lost responses, stale resumes, and legitimate long runs.
  • Keep the framework's hard turn or recursion limit as a final backstop.

Apply the checklist to the workflow with the most expensive or consequential repeated action. Run the eight end-to-end cases, then inspect every interruption record before enabling automatic resume.

References

  1. OpenAI Agents SDK running agents supports the description of the model-tool loop and maximum-turn failure.
  2. OpenAI Agents SDK run API supports the current turn-limit and error-handler configuration.
  3. LangGraph recursion-limit error supports the graph-level limit and infinite-loop failure mode.
  4. OWASP LLM06:2025 Excessive Agency supports limiting functionality, permissions, autonomy, and high-impact actions.
  5. Temporal Python error handling supports typed failure, retry, and timeout handling in durable workflows.

About Fire In Belly: Independent senior engineering from Tallinn, Estonia. We design and build AI workflow automation, internal tools, and custom software with the execution budgets and circuit breakers described above, at published fixed prices. Schedule a call to discuss your next project.