agent(), flowAgent(), and tool() are factory functions that return plain objects. There are no classes, no new, no this. The returned objects expose methods like .generate() and .stream(), but they are closures over configuration -- not class instances.
const myAgent = agent({
name: "helper",
model: openai("gpt-4.1"),
system: "You are helpful.",
});
// myAgent is a plain object: { generate, stream, fn }
const generate = myAgent.fn();
const result = await generate("Hello");Every public method returns Result<T> -- a discriminated union. Pattern-match on ok instead of wrapping calls in try/catch. Success fields are flat on the object alongside ok: true. Failure carries a structured error with code, message, and optional cause.
const result = await myAgent.generate({ prompt: "Hello" });
if (!result.ok) {
// result.error.code: 'VALIDATION_ERROR' | 'AGENT_ERROR' | ...
console.error(result.error.code, result.error.message);
return;
}
// Success -- fields are flat
console.log(result.output);
console.log(result.usage.totalTokens);Flow agent state is just variables in your handler function. There is no state machine, no reducer, no context object to thread through. Your handler is an async function -- use let, loops, and conditionals as you normally would.
const counter = flowAgent(
{
name: "retry-loop",
input: z.object({ prompt: z.string() }),
output: z.object({ answer: z.string() }),
},
async ({ input, $ }) => {
let attempts = 0;
let answer = "";
while (attempts < 3 && answer.length === 0) {
const result = await $.agent({
id: `attempt-${attempts}`,
agent: writer,
input: input.prompt,
});
if (result.ok) {
answer = result.output;
}
attempts += 1;
}
return { answer };
},
);Small functions composed together, not large option bags. An agent with tools is just an agent config with a tools record. A flow agent that calls agents is just a handler that calls $.agent(). Subagents are agents passed in the agents config -- automatically wrapped as tools.
const researcher = agent({ name: "researcher", model, system: "..." });
const writer = agent({ name: "writer", model, system: "..." });
// Subagents as tools -- the orchestrator delegates via LLM decisions
const orchestrator = agent({
name: "orchestrator",
model,
system: "Coordinate research and writing.",
agents: { researcher, writer },
});Zero hidden state
There are no singletons, no module-level registries, no global configuration. Every agent carries its own config. Loggers are passed explicitly. Provider registries are created and injected, not imported from a shared module.
// Each agent is self-contained
const a = agent({ name: "a", model: openai("gpt-4.1"), system: "..." });
const b = agent({ name: "b", model: openai("gpt-4.1-mini"), system: "..." });
// No shared state between a and bThe StepBuilder ($) provides observability -- every $.step(), $.agent(), $.map() call becomes an entry in the execution trace. But it is not required. You can call agents directly, use plain loops, or mix traced and untraced operations. The trace just will not include untraced work.
const pipeline = flowAgent(
{
name: "mixed",
input: z.object({ text: z.string() }),
output: z.object({ result: z.string() }),
},
async ({ input, $ }) => {
// Traced -- appears in result.trace
const analysis = await $.agent({ id: "analyze", agent: analyzer, input: input.text });
// Untraced -- plain function call, not in trace
let analysisText;
if (analysis.ok) {
analysisText = analysis.output;
} else {
analysisText = input.text;
}
const cleaned = cleanText(analysisText);
// Traced again
const final = await $.step({ id: "format", execute: () => formatOutput(cleaned) });
if (final.ok) {
return { result: final.output };
}
return { result: cleaned };
},
);Both satisfy the Runnable interface -- same .generate(), .stream(), .fn(). The difference is internal:
agent() |
flowAgent() |
|
|---|---|---|
| What drives execution | LLM tool loop | Your handler code |
| When to use | Single model call, optionally with tools/subagents | Multi-step orchestration, conditional logic, parallelism |
| State | Managed by AI SDK | Variables in your handler closure |
| Trace | Tool-loop steps via hooks | $ step builder entries |
| Model required | Yes | No (sub-agents provide their own models) |
| Nesting | Can delegate to subagents | Can call $.agent() with any Agent or FlowAgent |
Use agent() when a single LLM boundary is enough. Use flowAgent() when you need to coordinate multiple agents, implement retry logic, run parallel work, or apply custom control flow around LLM calls.