Skip to main content
Anvil’s multi-agent system is built from the same primitives as everything else: route handlers, tools, and LlmClient. AgentRegistry lets you register named agents and invoke them from code or from inside another agent’s tool loop. Sub-agents run entirely in-process — there are no HTTP round-trips, no additional servers, and no separate orchestration framework to learn.

AgentRegistry

Import AgentRegistry from anvil/agent:
import { AgentRegistry } from 'anvil/agent';

const registry = new AgentRegistry();

registry.register(name, config)

Register a named agent with its RunAgentOptions (the same options runAgent accepts, minus messages):
registry.register('summarizer', {
  client,
  system: 'You are an expert summarizer. Return a concise summary in 2–3 sentences.',
  maxIterations: 2,
});

registry.register('researcher', {
  client,
  system: 'You are a research assistant. Use tools to find accurate information.',
  tools: [searchTool, fetchPageTool],
  maxIterations: 8,
});
register returns this, so you can chain calls.

registry.call(name, input, overrides?)

Invoke a registered agent by name. Pass a string for a single user message, or a full ModelMessage[] conversation. Returns AgentRunResult.
const result = await registry.call('summarizer', 'Summarize the French Revolution in 3 sentences.');
console.log(result.text);
console.log(result.iterations);   // how many model turns it took
console.log(result.totalCostUsd); // accumulated cost
Pass overrides to adjust config for a specific call without modifying the registration:
const result = await registry.call('researcher', userQuery, {
  maxIterations: 3,           // tighten the cap for this call
  model: 'claude-haiku-3-5',  // use a cheaper model
});
text
string
The final text response from the sub-agent.
messages
ModelMessage[]
The full conversation, including all tool calls and results.
iterations
number
The number of model turns that ran.
totalUsage
{ inputTokens: number, outputTokens: number }
Accumulated token counts across all turns.
totalCostUsd
number
Accumulated USD cost.
stoppedAtCap
boolean
true if the loop stopped because it hit maxIterations rather than finishing naturally.

registry.has(name) and registry.names()

Check membership and list registered names:
registry.has('summarizer'); // true
registry.names();           // ['summarizer', 'researcher']

agentAsTool

Wrap a registered agent as an AgentTool so an orchestrator agent can delegate to it during its own tool loop. The sub-agent’s final text becomes the tool result:
import { agentAsTool } from 'anvil/agent';

const researchTool = agentAsTool(registry, 'researcher', {
  toolName: 'research',          // optional; defaults to `call_researcher`
  description: 'Delegate a research task to a specialist agent. Provide a detailed question.',
});
registry
AgentRegistry
required
The registry that contains the target agent.
name
string
required
The name of the registered agent to wrap.
options.toolName
string
The tool name advertised to the parent model. Defaults to call_<name>.
options.description
string
The tool description. Defaults to a generic delegation prompt that includes the agent name.
options.overrides
Partial<AgentConfig>
Config overrides applied every time the parent model delegates to this sub-agent.
The returned tool uses a fixed message input schema — the parent model passes a plain string describing the subtask, which becomes the sub-agent’s first user message.

withAgents middleware and callAgent helper

Use withAgents to attach a registry to ctx.state.agents, then use callAgent anywhere in your handler tree:
// server/routes/_middleware.ts
import { withAgents } from 'anvil/agent';
import { registry } from '../agents';

export default [withAgents(registry)];
// server/routes/report/post.ts
import { callAgent } from 'anvil/agent';
import type { Context } from 'anvil';

export default async function handler(ctx: Context) {
  const body = await ctx.body<{ topic: string }>();
  const result = await callAgent(ctx, 'researcher', body.topic);
  return { summary: result.text };
}
callAgent(ctx, name, input) is sugar for getAgents(ctx).call(name, input). It throws a clear error if no registry is attached.

Full orchestration example

This example registers a researcher and a summarizer, then wires them together in a parent orchestrator agent that the client talks to:
// server/agents.ts
import { AgentRegistry } from 'anvil/agent';
import { LlmClient, AnthropicDriver } from 'anvil/llm';
import { searchTool } from './tools/search';

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

export const registry = new AgentRegistry()
  .register('researcher', {
    client,
    system: 'You are a research assistant. Use the search tool to find accurate, up-to-date information.',
    tools: [searchTool],
    maxIterations: 6,
  })
  .register('summarizer', {
    client,
    system: 'You are an expert summarizer. Produce a concise 2–3 sentence summary from the provided text.',
    maxIterations: 2,
  });
// server/routes/report/agent.ts
import { defineAgent, agentAsTool } from 'anvil/agent';
import { registry } from '../../agents';
import { client } from '../../llm';

export default defineAgent({
  client,
  system: `You are an orchestrator. For each user question:
1. Use the research tool to gather information.
2. Use the summarize tool to condense it.
3. Return the final summary to the user.`,
  maxIterations: 6,
  tools: [
    agentAsTool(registry, 'researcher', {
      toolName: 'research',
      description: 'Research a question using live search. Provide a detailed question.',
    }),
    agentAsTool(registry, 'summarizer', {
      toolName: 'summarize',
      description: 'Summarize a block of text into 2–3 sentences. Provide the full text.',
    }),
  ],
});
The POST /report route now orchestrates the two sub-agents. The orchestrator model decides when to call research and summarize; each sub-agent runs to completion in-process and returns its text as the tool result.
Sub-agents run synchronously in the parent’s event loop via runAgent — no network calls, no spawned processes. The parent agent’s maxIterations counts its own turns; sub-agent turns are independent.

Sub-agent execution model

agentAsTool calls registry.call(name, message) inside execute, which calls runAgent directly. The entire sub-agent loop runs to completion before execute returns. This means:
  • No HTTP round-trips — the sub-agent never makes an HTTP request to itself.
  • No shared streaming — the parent stream does not include the sub-agent’s intermediate events; only its final text appears as a tool result.
  • Nested cost tracking — each sub-agent call uses the client’s internal cost accumulator, so client.totalCostUsd reflects the total across all agents sharing that client.
Register sub-agents with their own LlmClient instances to use cheaper or faster models for specific tasks, while reserving your most capable model for the top-level orchestrator.