FDE PulseFDE jobs open 434New in the last 7 days 27
VI

The newspaper of the Forward Deployed Engineer

Guides

When an agent must stop: four guardrails and the moment to hand over to a human

An agent with no stop condition can run forever and burn money without throwing a single error. The person left explaining that to the client is the FDE.

Ảnh kỹ sư ngồi trước nhiều màn hình theo dõi hệ thống trong văn phòng, gợi cảm giác một tiến trình đang chạy mãi cần người giám sát.
Photo: NASA Hubble / CC BY 2.0

In brief

  • An agent loop is essentially a while loop that only stops on its own when the goal is reached. That is why you always need to add step, budget and time limits.
  • Combine several stop conditions; the loop ends as soon as any one of them is met. The Vercel AI SDK stops after 20 steps by default.
  • Stopping does not mean going silent: every stop should return a reason and hand enough context to a human, especially before an irreversible action.
ShareLinkedInFacebookX

Picture a Monday morning at the client’s office. The refund-ticket agent you deployed last week has spent the whole night on a single ticket: look up the order, read the result, look up the order again, read it again. No error was thrown. There is just a log thousands of lines long and an API bill that has shot up.

The agent did not give a wrong answer. It simply did not know when to stop. For an FDE, this kind of incident is hard to explain to a client, because there is no error line to point at, only a system that kept running.

The good news is that preventing it involves nothing mysterious. It has two parts: putting limits on the loop, and designing the moments at which the agent hands work over to a human.

Why doesn’t an agent know when to stop?

The Hugging Face Agents Course describes the agent loop as a while loop: the cycle of thought, action and observation continues until the goal is achieved. The final action is sending the answer back to the user, and that is where the loop closes.

The problem lies in the word “until”. If the model never believes it has reached the goal, because data is missing, a tool returns an ambiguous error or the prompt contradicts itself, the natural stop condition is never met. The Vercel AI SDK documentation puts it bluntly: without a step limit, an agent can run indefinitely or rack up large costs.

That is why Anthropic, in its guidance on building agents, recommends adding stop conditions, such as a maximum number of iterations, to keep control. But where to set the threshold is a real trade-off. A tutorial from Metric Coders sums it up: stop too early and the result is incomplete; stop too late and the agent rambles, repeats itself or hallucinates.

Four guardrails, each catching a different failure

No single limit is enough on its own, because each catches a different kind of incident. The AI SDK lets you combine several stop conditions, and the loop ends when any one of them is met. Whatever framework you use, think about it the same way.

Guardrail What it catches Weakness to know about
Maximum steps Endless loops, logical cycles Too low a threshold cuts off long tasks
Token budget Costs ballooning even with few steps Capping only output tokens can cut off mid-sentence
Time deadline Slow tools, a user who is waiting Says nothing about the quality of the result
Repeat detection The same tool called with the same arguments many times You have to define what counts as “the same”

On steps, the AI SDK stops a ToolLoopAgent after 20 steps by default. That is a sensible starting point, not a magic number. On tokens, the Metric Coders piece notes that capping the number of generated tokens is simple but liable to cut sentences off, and recommends combining methods.

Stop sequences, which halt generation as soon as the model emits a predefined string, suit structured output. But the approach is fragile if the model does not emit exactly that string.

In the example below, repeat detection and the time deadline are both written by hand inside the loop, alongside the two familiar limits on steps and tokens.

Stopping is not giving up: handing over to a human

Anthropic describes agents pausing to get human feedback at checkpoints, or when they hit a blocker.

The site agentpatterns.ai makes this concrete with three positions. First, when confidence is low. Second, when the loop has consumed a configurable share of its budget in tokens, money or tool calls. Third, before any action the loop cannot undo from its own state.

With enterprise clients, the third is the most worrying. A wrong order lookup can be run again. A wrong refund or an email sent to the wrong customer cannot be taken back.

Example: a refund agent for an e-commerce marketplace

Suppose you are building an agent that handles refund tickets, with four tools: look up an order, look up the policy, issue a refund and send an email. The thresholds below are hypothetical figures for illustration: at most 20 steps, a budget of 40,000 tokens, handover to a staff member once 80% of the budget is used, and a 90-second deadline.

import time

MAX_STEPS = 20
TOKEN_BUDGET = 40_000
SOFT_FRACTION = 0.8          # chạm 80% ngân sách thì dừng để hỏi con người
DEADLINE_S = 90
MAX_REPEAT = 3               # cùng tool + cùng tham số quá 3 lần = đang lặp
IRREVERSIBLE = {"issue_refund", "send_email"}

def run(task):
    s = {"steps": 0, "tokens": 0, "history": [], "calls": {}}
    start = time.monotonic()
    while True:
        if s["steps"] >= MAX_STEPS:
            return handoff(task, s, "max_steps")
        if s["tokens"] >= TOKEN_BUDGET * SOFT_FRACTION:
            return handoff(task, s, "budget")
        if time.monotonic() - start > DEADLINE_S:
            return handoff(task, s, "timeout")

        step = llm_next_step(task, s["history"])
        s["steps"] += 1
        s["tokens"] += step.usage.total_tokens

        if step.is_final:
            return {"status": "done", "answer": step.answer, "steps": s["steps"]}

        key = (step.tool, str(sorted(step.args.items())))
        s["calls"][key] = s["calls"].get(key, 0) + 1
        if s["calls"][key] > MAX_REPEAT:
            return handoff(task, s, "repeat_loop", pending=step)

        if step.tool in IRREVERSIBLE:
            return handoff(task, s, "needs_approval", pending=step)

        obs = call_tool(step.tool, step.args)
        s["history"].append((step, obs))

def handoff(task, s, reason, pending=None):
    return {
        "status": "needs_human",
        "reason": reason,
        "steps": s["steps"],
        "tokens": s["tokens"],
        "summary": summarize(s["history"]),
        "pending_action": pending,
    }

(The two code comments read: stop and ask a human once 80% of the budget is reached; the same tool with the same arguments more than 3 times means the agent is looping.)

Go back to the scenario at the top. The agent looks up the order, gets a timeout from the orders API, then looks it up again with the same order ID. On the fourth call the repeat counter fires and returns repeat_loop with a summary of the history, instead of spinning all night.

On another ticket, the agent looks up the order, checks the policy, concludes the customer is eligible and prepares to call issue_refund. The loop stops with the reason needs_approval, along with the action awaiting approval. The customer service agent only has to review it and click approve or reject, without reading everything from the start.

Notice that the handoff function does not just report “failure”. It returns the reason, the step count, the token count and a summary. That lets whoever picks it up carry on within seconds.

Each stop reason should lead to a specific human task. For repeat_loop, the recipient checks what is wrong with the tool and handles the ticket manually if needed. For budget or timeout, a staff member reads the summary and decides whether to let the agent continue or finish the job themselves. max_steps is a job for an engineer: look for the cycle before thinking about raising the limit.

Doing it on your own project

Start by listing every tool and sorting them into three groups: read-only, writes that can be undone, and writes that cannot. The last group must always pass through an approval point, at least in the early stages of deployment.

Next, run the agent on a sample of real tasks and record the step and token counts of the successful runs. Set the limits a sensible margin above the typical figures rather than guessing. Then agree with the client who picks up the work when the agent stops, through which channel, and what they need to see.

Finally, turn “stop reason” into a metric. If the share of runs stopping on max_steps rises after a prompt change, you know immediately that something has broken.

Common mistakes

The first mistake to avoid is raising the limit as soon as you see the agent being cut off. The LangGraph documentation on the GRAPH_RECURSION_LIMIT error points out that if you did not expect the graph to loop many times and it still hits the limit, there is most likely a cycle. You can increase recursionLimit, but check the logic for infinite loops first.

The second is relying solely on an output token limit, so users receive answers that break off midway. The third is stopping silently: the loop ends and the user receives nothing, which breaks the principle that a loop should close with an answer.

The fourth is harder to spot: handing over to staff without context. The recipient has to open the logs and reread everything. At that point it is easy to understand why they start to see the agent as a burden rather than a helper.

Turning this skill into an edge in interviews

When reading FDE job descriptions, look for mentions of human-in-the-loop or guardrails. If they appear, that is your opening to bring this skill up. On your CV, do not write something generic like “built AI agents”.

State clearly that you designed stop conditions and a human handoff flow, with a measurable figure, such as the share of tasks that had to be passed to staff.

This week’s exercise: take an agent you have written, deliberately make one tool always return an error, and run it. If the agent stops on its own after a few attempts and returns a handoff that can be understood in ten seconds, you have a concrete story to tell at your next interview.

6 sources
Read next on the roadmap · Stage 5: DeploymentGuardrails for a client's LLM app: filter the input, lock down the output, add moderationPrompt injection has no complete fix yet, so FDEs have to stack several layers of defence, so that when one layer fails the damage stays small.