Aviary
Concepts

The agent loop

One turn is model → tools → model, bounded by maxSteps, with a usage row appended every time the model is called.

A turn is the unit of work behind one chat message: the model responds, the loop runs whatever tools it asked for, and feeds the results back — repeating until the model stops calling tools or a step ceiling is hit. Everything else in the library (governance, cost, HITL) hangs off this one iteration.

model → tools → model

Each pass through the loop is one step:

  1. The model is called with the system prompt, the thread's messages so far, and the tools the actor is allowed to see. This is one ModelProvider.runTurn call.
  2. A usage row is appended — unconditionally, whether or not the model asked for a tool.
  3. If the model returned zero tool calls, the loop breaks — the turn is done.
  4. Otherwise, each requested tool call is resolved by its kind, and the results are fed back to the model as the next step's input.
Each pass through the loop is one step: the model is called, a usage row is appended, any requested tools are resolved by kind (read tools auto-execute, action tools suspend for approval), and the results are fed back to the model. When the model returns no tool calls, the loop breaks and the turn is done.MODELTOOLSLEDGERresults →next stepturn donereturned no toolsread · getWeatherauto-executedaction · purgeCacheapproved · executedusage · step 0usage · step 1✓ turn done
Tools
The model returns zero tool calls → the loop breaks. The turn is done.

Same loop, whether or not a step is dispatched

This is the loop from its own point of view: step 1 (the model call) and step 4's tool executions happen exactly as described whether they run in-process or — under the durable runner's default dispatchedSteps: true — as routed steps that can execute on a different worker than the one running the turn. The loop's hooks (step, dispatchLlm, dispatchTool) are what change underneath it; the model → tools → model shape above doesn't. See Architecture for what changes cross-process when a step is dispatched, and Runners for the dispatchedSteps flag itself.

read vs action

A tool call's kind decides whether it runs unattended or waits for a human — see Tools for the full decorator surface:

kindWhat happensUse for
readAuto-executes immediately, same stepLookups, queries — anything side-effect-free
actionLoop suspends for an explicit approve/reject before execute runsAnything that mutates, spends, sends, or deletes
agentSynthesized, never hand-authored — delegates to a named sub-agentMulti-agent orchestration

A rejected action call doesn't throw — its result ({ rejected: true, reason }) is fed back to the model like any other tool result, so the model can adapt within the same turn instead of failing it.

Bounded by maxSteps

The loop is a plain bounded iteration, not a "runs until done" open loop:

for (let i = 0; i < maxSteps; i += 1) {
  // one model call, then its tool calls
}

maxSteps comes from the agent that runs the turn — set it per agent with @Agent({ maxSteps }) — and defaults to 8 when omitted. It exists to stop a model that keeps calling tools from running away with an unbounded turn — hitting the ceiling ends the turn with whatever text the last step produced.

Every turn appends a usage row

Step 2 above isn't conditional on tool calls: every model call writes a row to the usage ledger, with the actor, thread, model id, and token counts — the raw material the cost and quota machinery rolls up. A turn with three internal steps writes three usage rows, not one.

Optional: follow-up suggestions after the loop ends

With AgentModule.forRoot({ followUps: true }), once the loop breaks (step 3 above) the turn makes one more model call — outside the maxSteps-bounded loop — asking for short follow-up questions. The result lands on the assistant message's followUps field and writes its own usage row (purpose: 'follow_ups'). It's off by default: the loop's job is answering the turn; follow-ups are an opt-in, separately-costed extra. See Cost & Governance.

Where to go next

On this page