TokenPolice
Docs
Recipes

Block runaway agents

Stop a single agent run that loops or blows past a hard ceiling — a loop rule plus the catch snippet.

An agent stuck calling the same thing over and over, or one that just keeps going, can burn a lot of money in one run. Loop detection watches each run — one trace — on its own and stops it before it runs away.

The rule

This isn't a budget-over-time rule; it's a per-run circuit breaker. On the Firewall page, switch to the Runaway loops tab and click New rule from template to open Catch a workflow stuck looping. Two templates cover most cases:

Stop the same request on repeat — fires when the same prompt goes out three times in a row inside one trace, or a short back-and-forth keeps repeating. Catches retry storms.

Limit how many model calls one trace can make — the backstop: a hard ceiling per trace (default 25 calls). There are USD and token variants too: Limit how much one trace can spend and Limit tokens per trace.

Under When this happens, the templates default to Alert only — just notify me. Choose Block — stop the next model call on this trace if you want the run halted, not just flagged. Like every other rule, it is created in Dry-run — switch it to Enforce once you've watched it on real traffic.

The block lands one call late. The call that crosses the threshold is allowed; TokenPolice stops a later call on the same trace. Fast back-to-back calls can slip past. Treat it as a circuit breaker, not an exact ceiling.

A loop rule watches one trace — not a session or a conversation. For "cap this whole conversation", use a regular firewall rule grouped by session_id. Full detail in Loop detection.

The snippet

Two things make this work in your code: wrap the run so its calls share one trace, and catch the block when the detector trips.

import token_police as tp

tp.init(
    api_key=os.environ["TOKENPOLICE_API_KEY"],
    base_url="https://collect.tokenpolice.ai",
    firewall="enforce",          # blocking only happens under enforce
)

@tp.workflow(name="research_agent")     # one run = one trace the rule can watch
def run_agent(user_id: str, session_id: str, goal: str):
    while not done:
        try:
            step = client.chat.completions.create(...)
        except tp.TokenPoliceBlockedError as e:
            # the loop detector stopped this run
            return f"Run halted: {e.reason}"
import { workflow, TokenPoliceBlockedError } from "token-police";

const runAgent = workflow({ name: "research_agent" }, async (input) => {
  try {
    return await client.chat.completions.create(/* ... */);
  } catch (e) {
    if (e instanceof TokenPoliceBlockedError) return `Run halted: ${e.reason}`;
    throw e;
  }
});

Your app sees a TokenPoliceBlockedError — never an HTTP status. Its kind carries the detector that fired and reason explains it. Everything else stays fail-open; see Errors for the full field list.

Next