Agents
An agent is the lowest rung of the ladder — one LLM call, optionally wrapped in a tool loop, optionally producing structured output. It’s stateless across calls: each agent.execute() runs in fresh isolation, even if the same agent instance is shared.
This page is the mental model. For the API surface see Run agent.
One agent, one trip loop
Section titled “One agent, one trip loop”When you call agent.execute(input), the agent runs a trip loop. A trip is a single round-trip to the model.
trip 0: send messages → receive response ├─ if no tool calls → finalize, return └─ if tool calls → dispatch tools, append resultstrip 1: send updated messages → receive response ├─ if no tool calls → finalize, return └─ if tool calls → dispatch, repeat...trip N: bound by maxTrips (default 10)The loop terminates when:
- The model returns a response with no tool calls — the agent finalizes and returns.
- Every tool call this trip is
mode: "silent"— see Define tools. maxTripsis hit — the agent returns with the latest text and a warning logged.
Each trip is recorded on result.report.trips[] with its index, finish reason, usage, and timing.
What “stateless across calls” means
Section titled “What “stateless across calls” means”The agent factory holds configuration only. Per-call state — the messages list, the tool-call records, the running usage, the trip index — lives in a fresh internal Execution object spawned for each execute() call.
That means:
- Two calls with the same agent don’t share history. Pass
history: Message[]explicitly when you want one to follow the other. - Anonymous agents (no
namefield) get a deterministic fingerprint based on provider, model, and tools. Same config → same synthetic name across process restarts. - Sharing an agent across requests in a Node server is safe. There’s no internal mutable state to corrupt.
Structured output
Section titled “Structured output”Pass an output schema and you get a typed data field back:
const schema = v.object({ title: v.string(), tags: v.array(v.string()) });
const myAgent = ai.agent({ model, output: schema });
const { data } = await myAgent.execute(input);// ^? { title?: string; tags?: string[] } | undefinedTwo execution paths under the hood:
- Native — adapters whose
capabilities.structuredOutput === trueforward the schema as JSON Schema to the provider (response_format: json_schemaon OpenAI). The provider constrains the output at decode time. - Soft — adapters without native support get a system-prompt addition: “respond as JSON matching this schema”. The agent then validates client-side.
Client-side validation always runs. Set repair: { maxAttempts: 1 } to re-ask the model on validation failure.
Tool dispatch
Section titled “Tool dispatch”Tools are typed async functions the model can call. The agent:
- Tells the model what tools exist (name, description, JSON Schema of input).
- When the model emits a tool call, validates input against the tool’s schema.
- Invokes
tool.execute(input, ctx). - Stringifies the result and feeds it back on the next trip.
If input validation fails or the tool throws, the error is recorded on the ToolCall and reported back to the model on the next trip. The model gets a chance to correct itself, bounded by maxTrips.
There’s a side-channel — ctx.artifacts — for system-only data the model should never see (renderable blocks, citations, telemetry). See Define tools.
Streaming
Section titled “Streaming”agent.stream(input) returns an async iterable of typed events plus a .result promise:
const stream = myAgent.stream(input);
for await (const event of stream) { if (event.type === "agent.trip.streaming") { process.stdout.write(event.delta); }}
const result = await stream.result;The same lifecycle events fire whether you call execute or stream — streaming just opens up the per-token deltas in addition. Token deltas are NOT logged at framework level (agent.trip.streaming is not pushed to channels) — trip boundaries carry the same information at a saner volume.
Events — three subscription tiers
Section titled “Events — three subscription tiers”Lifecycle events fire in three places, in order:
- Factory level —
ai.agent({ on: {...} }). Fires for every execution of this agent. - Instance level —
myAgent.on("agent.error", handler). Returns an unsubscribe function. - Per-call level —
myAgent.execute(input, { on: {...} }). Fires only for this call.
All matching handlers fire in that order. Every event payload carries runId and rootRunId so you can stitch nested runs (workflow → agent → tool) into one trace.
Cancellation
Section titled “Cancellation”Pass an AbortSignal:
const ctrl = new AbortController();const promise = myAgent.execute(input, { signal: ctrl.signal });
setTimeout(() => ctrl.abort("too slow"), 30_000);
const { error, report } = await promise;Between-trip cancellation is guaranteed. Mid-trip is best-effort — the provider SDK decides whether the in-flight request can be aborted. report.status reads "cancelled" and error is an AgentExecutionError carrying the abort reason.
Sessions
Section titled “Sessions”sessionId is a caller-supplied string that gets stamped onto every report node produced during a run. Use it to group cost dashboards or trace logs by user session:
const sessionId = "user_42_2026-05-12";
await myAgent.execute("what's my order?", { sessionId });await myAgent.execute("cancel it", { sessionId });The framework doesn’t interpret it. No persistence, no implicit history — it’s metadata for your downstream pipelines.
Where state actually lives
Section titled “Where state actually lives”The agent doesn’t remember anything across calls. State you need to persist lives in:
- The
historyoption — passMessage[]on the next call to continue a conversation. - Tool side effects — write to your DB inside
tool.execute. - Middleware
ctx.state— per-execute scratch space for middleware, fresh on every call. - Workflow snapshots — when you graduate to
ai.workflow(), the snapshot store keeps a per-step checkpoint. - Supervisor snapshots — same idea for supervisors, per-iteration.
If you want a long-lived conversation that “remembers” across runs, today the pattern is: persist messages yourself, replay them via history. The ai.orchestrator() primitive (v2) will own this for you.
Related
Section titled “Related”- Run agent — the API surface in depth.
- Define tools — tool authoring.
- Write system prompts — composable prompts.
- Workflows — when one agent isn’t enough.