Budgets, retries and validation
In AEL, the Agent Engineering Language, retries, output repair, timeouts and cancellation draw on one shared budget, so nested retries cannot multiply cost. Token, time, CPU, memory and concurrency budgets apply across every child agent.
One budget per operation
When a call is admitted, AEL opens one budget for it, the operation, and every piece of work it causes draws on that budget: model calls, tool calls, retries, repairs, verifiers, child agents, subagents and callbacks.
| The budget limits | For example |
|---|---|
| Tokens | Input and output tokens across every model call. |
| Time | One deadline for the whole operation. |
| CPU and memory | The processing and memory the work may use. |
| Concurrency | Runs, child agents, subagents and requests running at once. |
| Calls, steps and queues | Model calls, tool calls, loop steps, and queued messages and bytes. |
| Shape | How many children and subagents, how deep delegation goes, and how many branches a fan-out has. |
- The tightest limit wins. The effective limit is the smallest of the deployment's, the tenant's, the agent's and the run's.
- Nothing is unlimited by omission. A limit you leave out is inherited from the level above; it never means "no limit".
- Children share, never multiply. A child agent receives a part of its parent's remaining budget, reserved before it starts, never a new budget of its own.
- Subagents too. A subagent draws on the budget, deadline and cancellation of the agent that spawned it. The one budget covers every subagent spawned: each spawn reserves its share of what is left before the subagent starts, and spawning again never resets what has been spent or extends a deadline. See Subagents.
- Fan-out stays bounded. One agent can spawn more than a hundred subagents for a single request, while only a bounded number run at the same time. How many run at once, how many a run admits in total and how deep they nest are finite limits you declare.
- Deadlines only shrink. A child's deadline is its parent's or earlier. A retry or a restart never extends it.
- Spent stays spent. Cancelling work never refunds tokens or time already used, and a new run ID for a retried run never resets what the operation has spent.
- Unknown is never zero. When a provider does not report usage, the budget keeps the largest amount the call could have used.
- Reserved first. Work reserves its share before it starts, and work that does not fit is refused rather than stopped halfway.
Attempts and repairs
# nodes/summarize.node.ael
node summarize(input: Request) -> Summary {
config {
prompt: file("prompts/summarize.md");
model: binding("primary_model");
max_attempts: 2;
}
return write_summary(input);
}
max_attemptscounts the first attempt:2means the first attempt and at most one retry.- A repair is an extra model call that asks the model to fix an answer that failed its checks. Repairs have their own limit, separate from attempts, and each one is a model call on the same budget.
- A retry uses a whole new attempt. A repair does not reset the attempt count.
- The exactly-one-call policy allows one attempt and no repair.
For example, an answer repaired twice has used three model calls: the first answer and two repairs.
Which failures are retried
AEL classifies every failure:
| Class | Failures | Retry |
|---|---|---|
| Never | Invalid input, a denied permission, a permanent configuration error. | No. |
| Idempotent | A known temporary failure of work that is safe to repeat. | Yes, within attempts, deadline and budget. |
| Reconcile | Work that may have changed something outside the program before its reply was lost. | No blind retry: the outcome is uncertain until your code reconciles it. |
- Waits between retries grow exponentially with random jitter. A provider's request to wait is respected, and no wait goes past the deadline.
- A timed-out call that is not safe to repeat is never retried blindly.
You can also write your own retry decision: a function that receives the error, the attempt number, the time so far and the remaining budget, and returns whether to retry, wait, fall back or stop. It decides within your limits and can never exceed them.
Checking outputs
A model's answer passes through decoding, a structural check, your checks, an optional transformation and a second structural check before it becomes a typed output; see Typed inputs and outputs.
Each of your checks returns one of:
- accept;
- reject, with a reason: the node repairs the answer if it has repairs left, or applies its failure policy;
- retryable failure: the check itself could not finish, and may be tried again;
- unavailable: something the check needs is missing;
- pending review: the result waits for an outside review, with a deadline.
You choose whether checks stop at the first rejection or collect every failure. When the repairs run out, the node returns a typed error, uses a typed fallback or escalates, as you declare. A check that calls a model or a service declares that effect and draws on the same budget and deadline.
CPU and memory limits
Per-agent CPU and memory limits state whether each limit is hard-enforced or accounted:
- Accounted: agents that share one process are measured and limited by the program itself. One such agent cannot get a separate operating-system memory limit.
- Hard-enforced: an agent in its own process group, on an operating system that can enforce it, gets limits that the operating system enforces.
A request for a hard limit that the deployment cannot enforce is refused when you deploy, never silently weakened. A CPU limit caps processing; it does not guarantee a deadline. A model running on another machine is outside these local limits, and is limited by tokens and time instead.
Under load
Agents are multi-tenant from the start: a trusted tenant identity flows through every call, store, log and reply, with fair scheduling and predictable behaviour under overload.
- Each tenant has its own limits on runs, queues, connections and data in flight, and shares the program fairly with other tenants.
- When a limit is reached, new work gets a clear refusal or waits in a bounded queue, and the reason goes back to the right caller. Capacity is kept for cancellation and cleanup.
- A single-user program uses one explicit default tenant, and follows the same rules.