An AI agent is a language model in a loop with tools. Nothing more exotic than that. The model reads a goal, picks a tool, reads the result, and picks again until it decides it is done. Cazi is where we run ours, so this post explains agents by showing what Cazi has to do to keep one honest.
A model in a loop
A chat model answers once. An agent keeps going. Each pass round the loop is a turn, and in each turn the model may call a tool: run a scan, read a file, submit a job, send a message. The tool’s output goes back into the model’s context, and the next turn starts from there.
That loop is the whole difference between a clever text box and a thing that acts in the world. It is also where every risk lives.
- The model is not deterministic. Ask twice, get two plans. A tool call it invents on turn seven has the same real effect as one you asked for.
- The loop has no natural end. Left alone it will retry, rephrase and wander. Each turn costs money and may cost more than money.
- The tools are the hands. Whatever a tool can reach, the agent can reach. Hand it a shell and you have handed it everything the shell can see.
So the question is never “how smart is the agent”. It is “what can it touch, who decided that, and what will we know afterwards”. Cazi answers those three questions by construction, which is why it is a useful way to explain what an agent is.
What an agent is made of
In Cazi an agent is not a prompt. It is a small, versioned record with four parts, and three of them are about limits.
| Part | What it holds | Who sets it |
|---|---|---|
| Persona | The role, in plain words, and the skills it works from | Whoever creates the agent |
| Allowlist | The tools it may call, by name. Empty means none | A tenant admin reviews every change |
| Scope | What those tools may touch: access level, images, nodes, connections, threads, and the subjects it may work on | A tenant admin reviews every change |
| Version | An immutable snapshot of all of the above, with the allowlist resolved to concrete tools | Made automatically on every edit |
Two of these deserve a closer look, because they are what most people miss when they picture an agent.
The allowlist starts empty. A new agent can call nothing. Tools are added by name, and * for “everything” has to be written out, and a background run refuses it, because nobody is there to approve anything. Tools that manage agents can never be on an allowlist at all. An agent may propose a change to itself or to another agent, but the proposal waits for a tenant admin, and until then the agent keeps the version it had.
Scope is argument-level. Being allowed to call create_task does not mean being allowed to run any image on any node against any host. Scope lists the images, nodes and connections the agent may name, the access level it holds (read, write, destructive, learn), and the subjects it may work on: hosts, domains, repositories, accounts. Subject scope is the one that decides whether a port scan is authorised testing or an attack on a stranger. A subject marked off-limits refuses work from everyone, agent or person, and every refusal is kept as evidence.
A run pins the agent’s version when it starts. Editing the agent mid-run changes nothing about what that run may do.
One turn, step by step
Here is what happens every time the model wants to do something.
One turn. Nothing runs until the checkpoint says so. Drawn as text, rendered by Vizo.
The model proposes. The checkpoint decides. Only an allowed call becomes a task, and a task is an ordinary Cazi container with its own spec, events and logs. Whatever comes back is scrubbed of credentials before the model sees it, and the decision itself, allowed or denied, is written to the record with the actor, the accountable person and the agent version in force.
The model never holds a credential. Its turns run on a worker that has no database, no provider key and no API token. The tool call travels to the control plane, which checks it again against the same allowlist and scope. So even a worker that has been compromised can do no more than the run it is working on is allowed to do.
Goals the agent cannot write
A run starts with a goal, and the goal has four parts: an objective, success criteria, constraints and a budget. A person writes it. The agent never writes its own criteria, and only a person may amend them later. Each amendment is a new goal version, so the record shows what the agent was asked at every point.
Before the agent changes anything, it has to propose a plan. Every planned step is checked against the pinned allowlist and scope, and a plan that reaches outside them is refused before a single tool runs. Then, as the run proceeds, each tool call is matched to a planned step. A call that serves no step is flagged as drift. A little drift is noted. Repeated drift trips a breaker.
When the agent believes it is finished, it reports an outcome against each criterion. Some criteria carry a check the control plane can verify itself: did the work succeed, was an analysis posted, does the artifact exist. The rest count as met only when the agent gives evidence. A run succeeds only when every criterion is met. Otherwise it pauses as unmet and lists what is missing. A turn that starts nothing and reports nothing gets one nudge, then pauses too.
This is the part that turns “the model stopped talking” into “the job is done”. They are not the same thing, and the first generation of our runs confused them.
What stops a runaway agent
The loop has no natural end, so Cazi gives it eleven artificial ones. Each is a ceiling, and the run driver consults them before every model call and every tool call.
| Breaker | Trips when |
|---|---|
| tokens | the stretch has used its token budget |
| turns | the stretch has used its model turns |
| turns_total | the run has used its lifetime turns, and no resume gets past this |
| tool_calls | one turn made too many tool calls |
| wall_clock | the stretch ran too long, waits included |
| repetition | the same tool with the same arguments, too many times |
| failures | too many failed or denied calls in a row |
| drift | too many calls that serve no step of the plan |
| tree_budget | the run and everything it delegated used the shared budget |
| spend | the owner’s or tenant’s daily allowance is used up |
| provider | the model provider refused: credit, quota, auth, overload |
A stretch is the stretch of work from the ask, or a resume, to the next pause. When a breaker trips, every further tool call in that turn is refused with a message telling the model to stop, and the run pauses for a person. The record names which breaker stopped it. A person can raise the ceiling or reply to continue, and the run resumes under the same pinned version.
Two details matter more than they look. A denial counts as a failure, because an agent repeatedly hitting the walls of its own allowlist is stuck and needs a person, not another turn. And budgets only ever narrow: a tenant’s settings sit under the instance’s, a person’s budget on a run sits under the tenant’s, and a delegated run’s budget sits under its parent’s. An agent that could raise its own ceiling would have none.
Agents working together
One agent can hand part of a job to another. In Cazi that is a delegate call, and it starts a child run. The interesting rule is what the child inherits: the intersection of its own allowlist and scope with its parent’s, plus any further narrowing the parent asked for. Authority flows downhill only.
The tree has hard edges. Depth is capped at three, each run may start five children, and a tree holds twenty runs at most. An agent already in the chain cannot be delegated to again, which closes the loop where two agents keep handing the same job back and forth. Tokens and turns are counted across the whole tree, so a parent cannot buy itself more budget by splitting the work.
Results come back up as structured data, marked as coming from another agent and never as instructions. Cancelling a run cancels everything below it. Pausing a run pauses its active children, and they resume with it. No child outlives its parent.
The audit trail follows the chain. Every entry of a delegated run carries the root run and the actor chain from the owner, through each parent agent, to the agent that acted.
How an agent gets better without getting more powerful
People worry, rightly, about agents that learn. The worry is usually about the wrong thing. An agent that remembers a working command is useful. An agent that can rewrite its own instructions is a different animal. Cazi keeps the two apart with one rule: autonomy grows, authority does not.
Learnings are advice, never authority. Every failed task, failed run and tripped breaker becomes a proposed learning. An agent may also propose one, with provenance recorded and severity capped at warning. Only a person can promote a learning to active, and only active learnings act: a warning surfaces in the response to a matching submission, an error blocks it. Lessons about a specific agent reach that agent’s context marked as data, never as instructions, naming who reviewed them. Learnings nobody has triggered for 90 days go stale and need re-review.
Skills are named playbooks for a recurring kind of work, loaded on demand. Templates are specs that worked once, saved with a TARGET placeholder so the next run substitutes rather than reinvents. Both are versioned and reviewed like anything else in the catalog.
Versions are where improvement is measured. Each agent version keeps its own run statistics: runs, successes, pauses, failures, breaker trips and average tokens. Rolling back is copying an old version forward, under the same review rule. So “is the new persona better” is a comparison of two rows, not a feeling.
None of these can add a tool to an allowlist or widen a scope. That is the whole point. What an agent tries can evolve freely. What it may do changes only when a person says so.
Where to start
A first agent is three decisions, and none of them is about the model.
- Name the tools. Start from empty and add only what the job needs, by name. A recon agent gets
create_task_from_template,get_taskandlist_templates. It does not getcancel_task, and it never gets*. - Draw the fence. List the images it may run, the nodes it may use and, above all, the subjects it may work on. If the subject list is
*, you have not drawn a fence. - Write the goal. An objective, two or three criteria the control plane can check, a budget in tokens and money. Then let the agent propose its plan and read it before you approve the run.
After the run, read the record before you read the result. Which calls were denied, which breaker tripped, how many turns it took. That is where you learn what the agent actually is, as opposed to what it says it is.
An agent is a model in a loop with tools. The loop is yours to shape. Cazi’s contribution is to make sure that shaping it is a decision a person makes, written down, and checked on every turn.
Discussion
0 commentsNo comments yet. The first perspective opens the thread.