Agents
Bounded agents — tools are Flows, maxSteps and budget.maxCostPerRun cap the loop, invoke with fx.run.
An agent (ai.agent) is a bounded tool-calling loop. Tools are your app’s Flows (and
allowlisted MCP refs) — each step goes through fx.call. The loop stops at maxSteps or
budget.maxCostPerRun.
For developers who need a planner or support assistant without writing a custom tool runtime —
declare the bag, run with fx.run.
The one rule
Pass Flow names (or handles) in tools, set maxSteps and budget.maxCostPerRun, then
fx.run(agent, {message}). There is no instructions / in / out on ai.agent — shape the
user message yourself.
Smallest Example
Declare model + agent
import { ai } from "okengine";
export const smart = ai.model("smart", {
provider: "openrouter",
model: "openrouter/free",
apiKey: process.env.OPENROUTER_API_KEY,
});
export const supportAgent = ai.agent("support.assistant", {
model: "smart",
tools: ["docs.search", "tickets.create"],
maxSteps: 5,
budget: { maxCostPerRun: 0.1 },
});Run from a Flow
import { on, flow, http } from "okengine";
import { z } from "zod";
import { supportAgent } from "@/core/ai";
import { member } from "@/core/gate";
export const assist = on(
http
.post({
in: z.object({ query: z.string().min(1) }),
})
.gate(member),
flow({
do: async ({ query }, fx) => {
return await fx.run(supportAgent, {
message: `Help the member: ${query}`,
});
},
}),
);Runtime records the agent name under asks. Prefer declaring effects: { asks: ["support.assistant"] }
when inference does not see fx.run.
Bound termination
Default maxSteps is 6 when omitted. Hitting the step cap ends the loop; exceeding
budget.maxCostPerRun throws AiBudgetExceededError:
ai: agent "…" exceeded maxCostPerRun N.
Progressive Patterns
String names or Flow handles — same capability path as fx.call:
import { searchDocs } from "@/flows/docs/search";
import { createTicket } from "@/flows/tickets/create";
export const supportAgent = ai.agent("support.assistant", {
model: smart,
tools: [searchDocs, createTicket],
maxSteps: 8,
});Options
| Option | Type | Default | Meaning |
|---|---|---|---|
model | AiModelDecl | string | first model | Logical binding for the loop |
tools | Flow / MCP refs | [] | Callable via fx.call |
maxSteps | number | 6 | Hard cap on tool rounds |
budget | { maxCostPerCall?, maxCostPerRun? } | — | Cost contracts for the run |
fx.run input
Pass a string or { message: string } (and any extra fields your tools need in context). The
agent declaration does not take Zod in / out — validate on the surrounding Flow.
await fx.run(supportAgent, "Summarize open tickets");
await fx.run(supportAgent, { message: "Summarize open tickets" });Agents vs ask-with-tools
| Surface | When |
|---|---|
ai.agent + fx.run | Reusable tool bag, shared step/budget policy |
fx.ask(prompt, …, { tools }) | One prompt, occasional tools |
Both default maxSteps to 6. Denied or unknown tools surface as
ai: all tool calls denied for prompt "…" / ai: model requested unknown tool "…".
Troubleshooting
fx.run named an agent that was never declared, or the declaring module was not imported before
oke().
AiBudgetExceededError — raise budget.maxCostPerRun, lower maxSteps, or shrink tools.
The model emitted a tool name outside the declared tools bag. Align names with Flow / MCP refs,
or tighten instructions in the user message.
Runtime gates fx.run as an ask of the agent name. List effects: { asks: ["support.assistant"] }
when the compiler does not infer fx.run.
Learn more
- Prompts —
fx.askand ask-time tools - MCP — inbound tools and outbound servers
- Flow — tools are ordinary Flows
- AI — guardrails figure and drivers