Local AI Agents: Advanced · Module 1 · Lesson 2 of 3
Bounding the loop, step limits and token budgets
Why an agent loop needs three budgets — step count, token spend and wall-clock — before it runs, how a local model's context window makes the token budget concrete, and what aborting safely actually requires, so a bounded loop that runs out returns a result instead of crashing or looping forever.
By the end, you can
- Implement an agent loop with configurable step limits and spend budgets that aborts safely when a budget is exhausted (LA-11 O2).
- Explain graceful degradation on a local model and why a smaller honest result beats a failed larger one (LA-11 O2).
Before you start
This lesson takes the loop from Lesson 1 and puts hard limits around it. It assumes you can already tell an agent from a prompt chain and can name the four loop steps, and it builds on LH-01's budget-based stop rules — the idea that a ceiling is fixed before the run and reaching it triggers a stop automatically. Here that idea becomes concrete for a loop running against a local model, where the constraints are not a cloud bill but your own machine's memory and time. This lesson shows a reference loop as annotated code to reason about; it is a structural pattern, not a lab to run, and it does not depend on any captured model output.
An unbounded loop is an unbounded consumption problem
A loop whose only exit is the model deciding it is finished has no reliable exit at all. The same adaptability that lets an agent recover from a bad observation lets it retry a failing step forever, chase a goal it will never reach, or grow its own context until the machine cannot hold it.
OWASP names this failure class directly. LLM10:2025 defines Unbounded Consumption as what "occurs when a Large Language Model (LLM) application allows users to conduct excessive and uncontrolled inferences, leading to risks such as denial of service (DoS), economic losses, model theft, and service degradation." The framing there is adversarial — an attacker driving cost — but the same mechanism fires by accident inside an agent loop with no ceiling: the loop itself becomes the source of excessive, uncontrolled inference. OWASP's mitigations are the loop's design brief in miniature: "Set timeouts and throttle processing for resource-intensive operations to prevent prolonged resource consumption," alongside strict input size limits and quotas. A bounded loop applies those controls to itself.
Three budgets, fixed before the run
An agent loop needs three separate ceilings, because each caps a different way the loop can run away.
None of the three is redundant. A loop can exhaust its steps while well under time, hang under its step count, or blow its token budget in three heavy passes. Fixing all three before the run — not letting the model reason about whether to continue — is what makes the loop bounded rather than merely usually-terminating.
- Step budget — the maximum number of iterations. This is the backstop against a loop that keeps taking actions without converging. It is a plain integer, checked at the top of every pass.
- Token budget — the maximum tokens the loop may spend across all its model calls. This caps the slow, invisible growth: each pass adds the last observation to the context, so a loop that never ends also never stops growing what it sends the model.
- Wall-clock budget — the maximum real time the run may take. A single step that hangs — a tool waiting on a network that never answers — is not caught by a step count, because the step count never advances. Time is the only budget that bounds a stuck step. This is OWASP's "set timeouts" applied to the loop as a whole.
A local model makes the token budget concrete
On a hosted API the token budget is mostly a money question. On your own machine it is a memory question, and that makes it easier to see and harder to ignore.
A local model holds its whole working context — the system prompt, the task, every prior action and observation — in the machine's memory at once. Ollama's context-length documentation is blunt about the cost: "Setting a larger context length will increase the amount of memory required to run a model." So the token budget is not abstract spend; it is RAM you either have or do not. The defaults reflect this: Ollama "defaults to the following context lengths based on VRAM: < 24 GiB VRAM: 4k context," stepping up only on machines with more. And agent workloads specifically need room — the same documentation notes that "Tasks which require large context like web search, agents, and coding tools should be set to at least 64000 tokens."
Those two facts pull against each other, and that tension is the whole point of a token budget on a local model. An agent loop wants a large context; a local machine pays for context in memory it may not have. The budget is where you resolve the conflict deliberately: decide the loop's token ceiling against the context length the machine can actually afford, so the loop stops on its own terms before the context outgrows the hardware.
Aborting safely means returning, not crashing
A budget that is exhausted by throwing an exception, or by the process being killed when memory runs out, is not a stop rule — it is a crash with a ceiling attached. Aborting safely has a specific meaning: when a budget is hit, the loop leaves the loop cleanly and returns a structured result that says which budget stopped it and what state the task was in. The caller gets an object to inspect, not a stack trace to decode or a hung process to kill.
Concretely, aborting safely requires three things:
- The budget is checked at a defined point — the top of each pass, and around each action — so the loop can notice it is over budget before starting more work, not after.
- Hitting the budget ends the loop through its normal exit, producing a result value, rather than raising past the caller.
- That result names the stop cause and carries whatever partial work exists, so the next step (a person, or Lesson 3's handoff) has something to act on.
Graceful degradation: a smaller honest result beats a failed larger one
Running out of budget is not automatically a failure. Often the loop has produced something useful before the ceiling — a partial summary, three of five findings, a draft that needs a last pass. Graceful degradation is the choice to return that partial result, clearly labelled as partial, rather than discard it or, worse, pad it into something that looks complete.
This matters more on a local model than on a large hosted one, because the local model is likelier to hit a real ceiling mid-task — the 4k default context fills, a smaller model loses the thread on a long chain. The honest response is to return what was genuinely established, marked as incomplete and with the stop cause attached, so a reader can tell a partial result from a finished one. The failure mode to avoid is the opposite: a loop that, unable to finish, produces confident-looking output that hides where it ran out. Anthropic's warning about "the potential for compounding errors" applies squarely here — a partial result dressed up as complete is exactly the kind of error the next step will build on without knowing it should not.
Reference shape: a guard-checked loop
The pattern below is a structural reference, not a runnable lab — the model call is left abstract as call_model(...), and there is no captured output. Read it for where the checks sit, not as a script to run. Every budget is checked at the top of the pass, before any new work; hitting one returns a result object naming the cause and carrying the work so far.
def run_agent(task, *, max_steps, token_budget, deadline_seconds, tools):
# Budgets are fixed here, before the loop, not decided inside it.
steps = 0
tokens_used = 0
started = now()
transcript = [] # plan/act/observe records, the partial work
while True:
# --- guard checks, before any new work this pass ---
if steps >= max_steps:
return stopped("step_budget", steps, tokens_used, transcript)
if tokens_used >= token_budget:
return stopped("token_budget", steps, tokens_used, transcript)
if now() - started >= deadline_seconds:
return stopped("wall_clock", steps, tokens_used, transcript)
# --- plan + act: exactly one model turn, one action ---
turn = call_model(task, transcript) # returns a plan and next action
tokens_used += turn.tokens # count spend as it happens
steps += 1
if turn.done:
return completed(turn.answer, steps, tokens_used, transcript)
# --- observe: run one action, record the result ---
observation = tools.run(turn.action) # itself time-bounded by deadline
transcript.append((turn.plan, turn.action, observation))
# loop back: reflect happens on the next call_model, against this observationThree properties make this bounded rather than merely usually-terminating. The budgets are parameters set by the caller, not constants the model can argue with. Every exit — over-budget or done — is a return of a result object, never an exception thrown past the caller. And the over-budget result carries the transcript, so a partial run is inspectable and can degrade gracefully instead of vanishing. What it does not yet do is decide *how* to stop well — logging the reason for triage and emitting a handoff a person can act on. That is Lesson 3.
Accessibility notes
This lesson is text-first. It contains one fenced code block, presented as a reference pattern and fully explained in the surrounding prose, so its meaning does not depend on reading the code itself; the code is plain text and carries no colour-only information. There are no images, audio, video or downloadable artifacts. The practice exercise's model answer sits behind a native, keyboard-operable disclosure control announced by screen readers, and the knowledge check uses native radio-button inputs with a visible question and options, posting its result to a live status region so the outcome is announced without a page reload.
Practice
Budget a loop for a real machine
You are configuring an agent loop to triage failed nightly jobs on a laptop with 16 GB of RAM and no dedicated GPU, running a 7B-class model under Ollama. The loop reads logs and configs (read-only tools) and proposes a probable cause. Runs happen unattended overnight, and a single stuck network read to the log server has hung a previous run for hours.
- Name the three budgets this loop needs, and for each, state the specific runaway it prevents here.
- Using the cited Ollama defaults, what context length will this GPU-less machine default to, and what does that imply for the token budget you can realistically set without raising memory use?
- Describe exactly what the loop should return when it hits the wall-clock budget mid-triage — what must be in that returned value.
- The overnight run reaches its step budget having identified a likely cause but not confirmed it. What is the graceful-degradation response, and what must NOT happen instead?
Compare with a bounded first version
The three budgets are a step budget, a token budget and a wall-clock budget. The step budget prevents a loop that keeps reading more logs and configs without ever converging on a cause. The token budget prevents the context from growing unbounded as each observation is appended, which on this machine is a memory ceiling, not just a cost one. The wall-clock budget is the only one that catches the known failure here — a stuck network read to the log server that hangs a step forever — because a hung step never advances the step count, so only elapsed time can stop it. With no dedicated GPU, Ollama has no VRAM to measure and defaults to its smallest tier, 4k context; the cited documentation says raising context length raises memory use, so on a 16 GB machine the realistic token budget is set against that small default rather than the 64000-token agent floor, which would cost memory this machine may not have to spare unattended. On hitting the wall-clock budget mid-triage, the loop should return a structured result — not throw and not hang — that names wall_clock as the stop cause, reports the steps and tokens used, and carries the transcript of what it read and concluded so far, so the morning reader has the partial work and the reason it stopped. When the step budget is reached with a likely-but-unconfirmed cause, graceful degradation means returning that likely cause clearly labelled as unconfirmed, with the evidence gathered and the stop cause attached; what must not happen is the loop presenting the unconfirmed cause as a confirmed finding, which would hide where it ran out and let the next step build on an error.
Knowledge check
Try the idea
Low-stakes practice only. A correct response marks this lesson complete; it does not affect your final assessment score.Sources and limits
This lesson synthesises the sources below into a practical learning model. It is not a security standard, legal advice or a guarantee that any particular agent design is safe.
- LLM10:2025 Unbounded Consumption — OWASP. Defines unbounded consumption as excessive, uncontrolled inference leading to denial of service and economic loss, and recommends input size limits, rate limiting and quotas, and setting timeouts and throttling for resource-intensive operations.
- Context length — Ollama. Setting a larger context length increases the memory required to run a model; Ollama defaults to 4k context below 24 GiB of VRAM; tasks that need large context like agents should be set to at least 64000 tokens.
- Building effective agents — Anthropic Engineering. Warns that the autonomous nature of agents means higher costs and the potential for compounding errors, and recommends testing in sandboxed environments with appropriate guardrails.
