> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.thatdevguy.in/llms.txt
> Use this file to discover all available pages before exploring further.

# defineAgent: Create Streaming Anvil Agent Handlers

> Use defineAgent to turn any route file into a streaming AI agent. Configure the model client, system prompt, tools, and context pipeline in one place.

`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:

```ts theme={null}
// 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:

<ParamField body="client" type="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.
</ParamField>

<ParamField body="resolveClient" type="(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`.
</ParamField>

<ParamField body="model" type="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.
</ParamField>

<ParamField body="system" type="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.
</ParamField>

<ParamField body="tools" type="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).
</ParamField>

<ParamField body="maxIterations" type="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.
</ParamField>

<ParamField body="tracer" type="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.
</ParamField>

<ParamField body="traceName" type="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).
</ParamField>

<ParamField body="budget" type="BudgetConfig">
  Per-request cost cap. The runtime checks the accumulated cost before each model call and aborts the loop if the budget is exceeded.
</ParamField>

<ParamField body="guardrails" type="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.
</ParamField>

<ParamField body="context" type="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`.
</ParamField>

<ParamField body="getMessages" type="(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.
</ParamField>

## Attaching a client with middleware

Use `withLlm` when you want to share one `LlmClient` instance across many routes without importing it into every file:

```ts theme={null}
// 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)];
```

```ts theme={null}
// 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.',
});
```

<Tip>
  `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.
</Tip>

## Dynamic system prompt

Pass an async function to `system` to build the prompt from the incoming request:

```ts theme={null}
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:

```ts theme={null}
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:

```ts theme={null}
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;
  },
});
```
