Stopping Conditions and Budgets

Why Agents Need Limits

An agent decides its own steps, so it can loop forever, call the same tool again and again, or spend a lot of money. Always put hard limits in your code, not only in the prompt.

Why not just tell the model "stop after 5 steps"? Because a prompt is a request, not a guarantee. The model may lose count, misjudge that it is "almost done", or follow an instruction hidden in a tool result. A limit in code is a guardrail: it holds no matter what the model writes. Think of a prepaid metro card: the gate stops you when the balance runs out, whatever you intended.

Agents usually get stuck in a few recognisable ways: loops (the same search again and again because the result did not contain what it hoped), drift (wandering into side tasks), and runaway cost (long tool outputs filling the context so every later call is expensive). Each limit below catches one of these.

LimitExample valueWhat to do when hit
Max steps (model calls)8stop, return a partial answer or hand over to a human
Token / cost budget50,000 tokens or ₹5 per conversationstop and summarise what was found
Wall-clock timeout60 sstop, tell the user it is taking long
Repeated identical callsame tool + same input twicetell the model it already has that result, or stop
Risky actionrefund above ₹2,000pause for human approval

Example

Example

An agent loop with a step limit, a token budget and a repeat detector. The scripted stand-in model gets stuck calling the same tool.

def stuck_model(step):  # stand-in for the model; it keeps asking the same thing
    return {"tool": "search_policy", "input": {"q": "exchange"}, "tokens": 1800}

def run_agent(model, max_steps=8, token_budget=6000):
    used, seen = 0, set()
    for step in range(1, max_steps + 1):
        action = model(step)
        used += action["tokens"]
        if used > token_budget:
            return f"stopped at step {step}: token budget {token_budget} exceeded ({used})"
        key = (action["tool"], tuple(sorted(action["input"].items())))
        if key in seen:
            return f"stopped at step {step}: repeated call {action['tool']}{action['input']}"
        seen.add(key)
        print(f"step {step}: {action['tool']}({action['input']}) tokens so far={used}")
    return f"stopped: reached max_steps={max_steps}"

print(run_agent(stuck_model))
print(run_agent(stuck_model, token_budget=1000))

Output

step 1: search_policy({'q': 'exchange'}) tokens so far=1800
stopped at step 2: repeated call search_policy{'q': 'exchange'}
stopped at step 1: token budget 1000 exceeded (1800)

With the default budget, the loop runs step 1, then catches the identical second call and stops. With a tiny budget of 1,000 tokens, the very first step already overspends, so the budget check fires first. Order matters: check the cheapest, most certain limits before running the tool.

Graceful Stopping

Hitting a limit should not look like a crash to the user. A good agent degrades gracefully: it returns what it found so far ("I found your order but could not confirm the refund status"), logs why it stopped, and offers a next step such as a human agent. When you detect a repeated call, a softer option is to send a tool result that says "you already have this result; answer with what you know", which often breaks the loop without stopping it.

Note: Misconception: "a bigger step limit makes the agent more capable". Usually the opposite. If a support task needs 20 steps, the tools are too low-level or the task should be a workflow. Set limits from real traces: look at how many steps successful runs take, and put the limit a little above that.

Human in the Loop

For actions with side effects (refunds, cancellations, emails), make the tool itself check a rule and ask for approval. The model never gets the power to skip it.

@beta_tool
def issue_refund(order_id: str, amount_inr: int) -> str:
    """Refund a ShopEase order. Call only after the customer confirms.

    Args:
        order_id: The order ID.
        amount_inr: Refund amount in rupees.
    """
    if amount_inr > 2000:
        create_approval_ticket(order_id, amount_inr)      # a support agent approves later
        return "Refund needs manager approval. Tell the customer it will be reviewed within 24 hours."
    return payments.refund(order_id, amount_inr)
  • Limits belong in code; prompts can be ignored or overridden.
  • Use max steps, a token or cost budget, a timeout and a repeat detector together.
  • Stop gracefully: partial answer, logged reason, handover option.
  • Risky tools enforce their own rules and ask a human above a threshold.