Skip to main content
defineAgent is the single function you need to build an agent route. It returns a route handler that runs the model↔tool loop and streams the Vercel AI SDK data stream protocol back to the client. Export it as the default from an agent.ts file (or any post.ts) and Anvil mounts it automatically.

Quick start

The example below creates a fully working agent route at POST /chat that uses Anthropic’s Claude, answers questions, and can call a weather tool:
// server/routes/chat/agent.ts
import { defineAgent } from 'anvil/agent';
import { LlmClient, AnthropicDriver } from 'anvil/llm';
import { z } from 'zod';

const client = new LlmClient({
  drivers: [new AnthropicDriver({ apiKey: process.env.ANTHROPIC_API_KEY })],
  defaultModel: 'claude-opus-4-8',
  fallback: ['gpt-4o'], // requires an OpenAIDriver too
});

export default defineAgent({
  client,
  system: 'You are a helpful assistant.',
  tools: [
    {
      name: 'get_weather',
      description: 'Get current weather for a city',
      zodSchema: z.object({ city: z.string() }),
      execute: ({ city }) => fetchWeather(city),
    },
  ],
});

Configuration reference

defineAgent(config: DefineAgentConfig) accepts the following options:
client
LlmClient
The LlmClient instance to use for model calls. If omitted, the runtime falls back to resolveClient or the client attached by a withLlm() middleware.
resolveClient
(ctx: Context) => LlmClient | undefined
A per-request factory for the LlmClient. Useful when the client depends on request state (e.g. a per-tenant API key stored in ctx.state). Takes precedence over a middleware-attached client but not over an inline client.
model
string
Override the default model for this specific agent route. The value is passed through to the LlmClient and must be supported by one of its drivers.
system
string | (ctx: Context) => string | Promise<string>
The system prompt. Pass a plain string for a static prompt, or an async function to build the prompt from request context — for example, to inject the authenticated user’s name.
tools
AgentTool[] | (ctx: Context) => AgentTool[] | Promise<AgentTool[]>
The tools available to the model during this run. Pass an array for a fixed set or a function to assemble tools dynamically from request context (e.g. scoped by user permissions).
maxIterations
number
Hard cap on the number of model↔tool turns. Defaults to 10. When the cap is reached the stream ends with a final event — the model’s last text (if any) is still returned. Use this to bound cost and latency on open-ended agent tasks.
tracer
Tracer
A Tracer instance for trace instrumentation. Each request opens a named trace; model calls and tool executions are recorded as child spans. Traces appear in the /_anvil dashboard.
traceName
string | (ctx: Context) => string
The name given to the trace opened for each request. Defaults to "agent <path>". Pass a string for a static name, or a function to derive it from the request context (e.g. to include the user ID).
budget
BudgetConfig
Per-request cost cap. The runtime checks the accumulated cost before each model call and aborts the loop if the budget is exceeded.
guardrails
Guardrail[]
An ordered array of guardrail policies applied to model output text and tool calls. Use the built-in helpers (contentFilter, toolPolicy, injectionGuard) or implement the Guardrail interface directly.
context
ContextStep[]
A context assembly pipeline that runs once before the loop starts. Steps can inject retrieved documents as system addenda (RAG), trim the message history to a token budget, or add static context fragments. Built-in steps: retrievalContext, tokenBudget, systemContext.
getMessages
(ctx: Context) => ModelMessage[] | Promise<ModelMessage[]>
Custom message extractor. By default defineAgent reads body.messages — the standard AI SDK useChat request shape. Override this when your request body has a different structure.

Attaching a client with middleware

Use withLlm when you want to share one LlmClient instance across many routes without importing it into every file:
// server/routes/_middleware.ts
import { withLlm } from 'anvil/agent';
import { LlmClient, AnthropicDriver } from 'anvil/llm';

const client = new LlmClient({
  drivers: [new AnthropicDriver({ apiKey: process.env.ANTHROPIC_API_KEY })],
  defaultModel: 'claude-opus-4-8',
});

export default [withLlm(client)];
// server/routes/chat/agent.ts
import { defineAgent } from 'anvil/agent';

// No `client` needed — withLlm() middleware attached it to ctx.state.llm
export default defineAgent({
  system: 'You are a helpful assistant.',
});
withLlm is a regular Anvil middleware. Place it in a _middleware.ts file to scope it to a subtree of routes, or at the root to share the client across your entire application.

Dynamic system prompt

Pass an async function to system to build the prompt from the incoming request:
export default defineAgent({
  client,
  system: async (ctx) => {
    const user = await getUser(ctx.state.userId);
    return `You are a helpful assistant for ${user.name}. Their account tier is ${user.tier}.`;
  },
  tools: [...],
});

Custom message extraction

Override getMessages when your frontend sends a request body that doesn’t match the AI SDK useChat shape:
export default defineAgent({
  client,
  system: 'You are a support bot.',
  getMessages: async (ctx) => {
    const body = await ctx.body<{ conversation: Array<{ from: string; text: string }> }>();
    return body.conversation.map((m) => ({
      role: m.from === 'agent' ? 'assistant' : 'user',
      content: m.text,
    }));
  },
});

Dynamic tools from context

Load tools based on the authenticated user’s permissions:
export default defineAgent({
  client,
  system: 'You are a helpful assistant.',
  tools: async (ctx) => {
    const scopes = ctx.state.scopes as string[];
    const tools = [alwaysAvailableTool];
    if (scopes.includes('admin')) tools.push(adminTool);
    return tools;
  },
});