The agent run loop
The agent (@grundlag/agent) is one of the platform’s consumers: it runs a
language model against a set of actions and lets the model call them as tools. This page
describes the loop it runs, how actions become tools, and why the entire state of a run
lives on the Prompt.
The loop
Section titled “The loop”A run is turn-based. Each turn is one model completion; the run keeps taking turns until the model has nothing more to ask for.
- Assemble the context. The system prompt plus the conversation (
prompts) are turned into chat messages. Each prompt’s user content becomes a user message; each prior output becomes an assistant message, a tool call with its result, or a rendered item. - Take a model turn. The run calls
client.chat.completions.createwithstream: trueand the available tools. Text arrives incrementally and is emitted asoutput:deltaevents while being accumulated into a text output. - Execute the tool calls. If the model requested tools, the run executes each one by
calling the matching action’s
execute, then records the return value (or error) on the tool call’s output entry. - Loop. The recorded results are now part of the conversation, so the run takes another turn. The model sees what the tools returned and decides what to do next.
- Complete. When a turn requests no tools, the loop ends. The run emits
completedand resolves withstatus: 'completed'.
Steps 2–4 repeat as many times as the model needs. A simple question resolves in one turn; a task that gathers data through several tools takes several.
How actions become tools
Section titled “How actions become tools”The agent does not define a separate notion of a tool. It takes the same core Action
objects every consumer uses and adapts them for the model. For each entry in the actions
map it converts the action’s Zod input schema to a JSON Schema with z.toJSONSchema, and
emits an OpenAI function tool whose name is the map key, whose description is the action’s
description, and whose parameters are that schema:
{ type: 'function', function: { name, // the actions-map key description: action.description, parameters, // z.toJSONSchema(action.input) },}When the model calls a tool, the run looks the name up in the actions map and invokes that
action’s execute — the identical execute a provider wrote once and everything else
consumes. This is why an action written for the HTTP API works unchanged as an agent tool:
the agent is just another caller. See Architecture and
Define actions.
State lives on the prompt
Section titled “State lives on the prompt”The run holds almost no state of its own. Everything it produces — streamed text, tool
calls with their inputs and results, emitted items, and any interrupts — is appended to the
output array of the active Prompt. The transcript is the state.
This has a deliberate consequence: a run is reconstructable. Because run.state is derived
entirely from the prompt, a brand-new AgentRun built over the same (persisted) prompts
reports the same position the original did — ready, or interrupted on a specific tool
call. You can persist the prompts, restart the process, rebuild the run, and continue
exactly where you left off, without the runtime having kept anything in memory. This is what
makes streaming (react as it happens) and persistence (reconstruct later) two views of the
same data rather than separate mechanisms. See
Stream output and react to events.
Where interrupts fit
Section titled “Where interrupts fit”Tool execution in step 3 is where a run can pause. If an action throws ActionInterrupt
instead of returning, the run stops looping and unwinds: it records the interrupt on the
tool call, emits interrupted, and resolves with status: 'interrupted' rather than
completed. The suspended tool call is omitted from the model’s context — there is no
dangling call to report — so the conversation stays valid.
Because the suspension is written onto the prompt, it survives persistence like everything
else. You later answer it with resume (re-run the action with your answer as its
resumption) or reject (record a failure the model sees), and the loop picks back up. See
Handle interrupts for the mechanics and
Interrupts for the cross-cutting model.