Skip to content
MINH VO A working notebook
by an engineer in Vietnam
Foundation8 min read

Give an agent a budget it can actually obey

Give an agent explicit action, time, and cost limits, preserve enough budget for a useful handoff, and distinguish verified completion from exhausted effort.

Open notebook with connected abstract shapes and olive thread linking paper swatches

For an engineer running an agent, a stopping rule must be enforceable by the application. A prompt saying “work efficiently” does not prevent another tool call. Define the task’s completion evidence, the resources available, and the terminal outcomes before execution begins.

Consider a hypothetical repository investigation with a limit of twelve tool calls and ten minutes. The agent may read files and run tests, but it cannot edit or publish anything. Its goal is to identify a likely cause of a failing export and provide evidence. Those limits are example policy values, not a recommendation for every investigation.

Separate success from resource exhaustion

Completion means the required evidence exists. For the export task, that could be a reproduced failure, the relevant source path, and a causal explanation consistent with the observed trace. The application can require these fields while still leaving the final technical judgment to review.

Budget exhaustion means the system has used its permitted resources without satisfying that contract. It should return verified findings and the unresolved next check. Calling that state “complete” because the model produced a final paragraph would misrepresent what happened.

A third outcome is a necessary clarification. If the task names an unavailable repository or omits the failing input, the agent may need user information before useful work can continue. Distinguish that from an ordinary uncertainty it can investigate using existing tools.

Agent completion evidence and enforced resource limits lead to distinct completed, clarification, or exhausted outcomesView full-size image ↗

The figure shows three terminal outcomes. Each needs its own user-facing message and stored status. A later continuation can then tell whether it is resuming unfinished work or starting a new task after a completed result.

Reserve the handoff before spending the budget

If the agent uses every available token and second on investigation, it may be unable to explain what it found. Reserve a small portion for summarizing evidence, recording active operation handles, and listing the next action. The reserve should be part of the executor’s accounting rather than an optimistic request to the model.

A simple policy can allow at most ten investigation calls from a twelve-call budget, keeping the remainder for checking a running operation or producing a final evidence record. That exact allocation is illustrative. Some tasks need no extra tool call to finish, while others require a final verification step that should be protected explicitly.

One synthetic trace makes the call reserve concrete. After nine inspections, the agent has three calls left. Starting one test uses the tenth call but does not establish its result. The reserved calls can observe that existing operation and persist the final handoff:

StateCalls consumedCalls remainingPermitted next action
Inspection complete93Start one justified test
Test process launched102Observe the recorded handle
Terminal result observed111Persist evidence and handoff
Handoff stored120Stop issuing tools

If the observation shows a live process, the system must use its documented continuation or cancellation policy. It cannot label the test successful just because the call reserve is almost empty.

Store elapsed time using a monotonic clock. Record actual provider usage and tool execution counts where available. A model’s self-reported estimate of remaining tokens is not a reliable billing or enforcement mechanism.

Refuse the next call at the executor boundary

Here is a call-admission policy for the twelve-call example. Ten calls may investigate; the last two are reserved for observing an existing operation or writing the handoff. New investigation also stops thirty seconds before the deadline. These are teaching constants, not measured optimal settings. The function receives elapsed-clock readings in seconds and returns a new used count or None when admission is denied.

import math

def claim(used, kind, now, deadline):
    if type(used) is not int or not 0 <= used <= 12:
        raise ValueError("invalid used count")
    if type(kind) is not str or kind not in {"investigate", "handoff"}:
        raise ValueError("unknown call purpose")
    if any(type(t) not in (int, float) or not math.isfinite(t)
           for t in (now, deadline)):
        raise ValueError("finite clock readings required")
    if used == 12 or now >= deadline:
        return None
    if kind == "investigate" and (used >= 10 or now >= deadline - 30):
        return None
    return used + 1

print(claim(9, "investigate", 100, 600))
print(claim(10, "investigate", 100, 600))
print(claim(10, "handoff", 580, 600))
print(claim(11, "handoff", 590, 600))
print(claim(12, "handoff", 599, 600))

The outputs are 10, None, 11, 12, None. At second 570, a new investigation is refused even if only two calls have been used. A handoff call may still start then, provided capacity remains. At second 600 all admission stops. These boundary cases are as useful as the ordinary path because an off-by-one error would spend the protected capacity.

Call capacity and elapsed time independently restrict investigation, while a smaller handoff window remains before the absolute deadlineView full-size image ↗

The application, not the model, assigns the call purpose. Otherwise an agent could label every new search as a handoff. Once a claim is accepted, persist the new count before invoking the tool and count that attempt even if execution returns an error. A denied claim must not invoke anything. This pure function has no storage or concurrency control: two workers reading used=9 could both obtain10. A shared executor needs an atomic compare-and-update or transaction around admission and a stable attempt record.

The Python time documentation, retrieved September 21, 2026, states that the reference point of monotonic() is undefined and only differences between readings are meaningful. A running process can use one monotonic deadline; do not serialize the literal reading as a portable calendar timestamp for another machine. On recovery, apply a documented durable deadline policy and establish a new local clock budget.

Admission is not cancellation. A call accepted at 599 seconds could run beyond 600 unless the tool also receives a compatible timeout or deadline and supports enforcement. The reserve gives the executor an opportunity to finish responsibly, not a guarantee that a slow network or blocked process will cooperate. Test deadline propagation and active-process cleanup separately from this arithmetic function. Token and money limits also need independent accounting; twelve cheap file reads and twelve long model generations have different costs.

Marc Brooker’s Exponential Backoff And Jitter, published March 4, 2015, uses a contention simulation to explain why synchronized retries create unnecessary work. Its numeric results describe that simulation, not this agent. Retry timing also does not make a write safe to repeat. Stripe’s idempotency documentation, retrieved September 21, 2026, provides a concrete service contract for replaying the same key and parameters. An agent adapter must consult its actual service’s operation record or idempotency rules after an ambiguous timeout; a remaining call allowance alone cannot establish whether the first write committed.

Bound retries and repeated hypotheses

A tool can fail because of malformed arguments, a transient service error, denied access, or a missing resource. These failures should not all trigger the same retry loop. A malformed argument may be repairable once; an access denial should not lead to trying adjacent identifiers until something succeeds.

The OpenAI rate limits guide, retrieved on September 20, 2026, discusses backoff for rate-limited requests. Backoff still consumes the task’s time budget. Do not sleep and retry beyond the point where the result can be useful, and record failed attempts alongside any actual billed usage; a failed request does not necessarily have the same charge as a completed one.

Repeated actions can also indicate a reasoning loop. If the agent reads the same file under the same hypothesis three times without new evidence, require it to state a different next check or stop. The threshold is an application choice; the useful signal is repeated work without an evidence change.

Treat live operations as live

Suppose the agent starts a test process and the observation call times out. The process may still be running. Starting a second copy because no output arrived can waste resources or interfere with the first run. Preserve the process handle and poll or inspect its actual state.

A deadline policy should specify what happens to active work. The application may cancel it when supported, allow a bounded grace period, or return a durable operation reference for later observation. The final handoff must say which case occurred. “The test failed” is inaccurate when the only fact is that observation stopped before completion.

For tools with side effects, an ambiguous timeout is more serious. The operation may have committed before the response was lost. Consult a durable operation identifier or status record before retrying. A stopping rule needs to account for unfinished effects as well as unfinished reasoning.

Evaluate stopping behavior directly

The ReAct paper studies agents that alternate reasoning and actions. When adapting an action loop to a product, include stop behavior in the evaluation. A system that occasionally solves a task but routinely exceeds its allowed scope is not meeting the full contract.

Use fixtures where the answer is available immediately, where a useful clue appears late, where a tool repeatedly fails, and where the requested evidence does not exist. Record task success, honest incomplete outcomes, excess calls, and any action after a terminal state. These are separate outcomes; collapsing them into a single success percentage hides operational failures.

Inspect the handoff from an exhausted run. Another engineer should be able to identify the repository revision, files inspected, commands actually completed, observations, and remaining uncertainty. Avoid dumping the full transcript when a shorter evidence index is easier to use.

A well-defined stop does not require the agent to solve every task. It requires the system to tell the truth about what it accomplished, what remains active, and what would be needed to continue. That contract makes resource limits compatible with useful engineering work.

Sources & further reading

  1. ReAct: Synergizing Reasoning and Acting in Language Models
  2. OpenAI rate limits guide
  3. Python time: monotonic clocks
  4. Marc Brooker: Exponential Backoff And Jitter, 4 March 2015
  5. Stripe idempotent requests
← Back to the journal
All notes

Illustration

100%