Docs
Adding an Agent

Adding an Agent

Step-by-step guide to adding a new specialized agent to the swarm.

Overview

Adding an agent requires touching 3 files:

  1. lib/ai/agents/registry.ts — register the agent
  2. lib/ai/agents/your-agent.ts — implement the agent
  3. lib/ai/orchestrator.ts — add the dispatch case

Optionally: lib/ai/utils.ts if the agent produces assets.

Steps

Register the agent in registry.ts

Add an entry to AGENT_REGISTRY. The description field is critical — it's the only thing the orchestrator's routing LLM reads:

"invoice-parser": {
  id: "invoice-parser",
  name: "Invoice Parser",
  description:
    "Extracts structured data from invoice PDFs: vendor, line items, totals, due dates.",
  icon: "FileText",
},

Use kebab-case for the ID. snake_case and camelCase IDs will break routing. The ID must exactly match the key in AGENT_REGISTRY.

Create the agent file

Create lib/ai/agents/invoice-parser-agent.ts:

import { resolveModel } from "@/lib/ai/models";
import { saveAgentResponse } from "@/lib/ai/utils";
import { convertToModelMessages, ToolLoopAgent, UIMessage } from "ai";
 
export const maxDuration = 60;
 
export async function streamInvoiceParserAgent(
  messages: UIMessage[],
  team: any,
  conversationId: string,
  modelId?: string,
) {
  const agent = new ToolLoopAgent({
    model: resolveModel(modelId),
    tools: {
      read_document: readDocumentTool,
      extract_invoice_data: buildExtractInvoiceTool(team.id),
    },
    instructions: `
      You are an invoice parsing specialist.
      When given a PDF URL, use read_document to fetch it,
      then use extract_invoice_data to extract structured data.
    `,
    onFinish: async ({ response }) => {
      await saveAgentResponse(response, { conversationId, teamId: team.id });
    },
  });
 
  const modelMessages = await convertToModelMessages(messages);
  return agent.stream({ prompt: modelMessages });
}

Key points:

  • Export maxDuration to control the Vercel function timeout
  • Always call saveAgentResponse in onFinish
  • Use resolveModel(modelId) to support per-request model switching

Add the dispatch case in orchestrator.ts

// lib/ai/orchestrator.ts
import { streamInvoiceParserAgent } from "@/lib/ai/agents/invoice-parser-agent";
 
// Inside executeAgent switch:
case "invoice-parser":
  return await streamInvoiceParserAgent(messages, team, conversationId, modelId);

Register assets (optional)

If your agent produces outputs that should be saved as assets, add an entry to ASSET_REGISTRY in lib/ai/utils.ts:

extract_invoice_data: ({ input, output }) => {
  const value = (output as any)?.value ?? output;
  if (!value?.vendor) return null;
  return {
    type: "invoice",
    title: String(value.vendor ?? "Invoice").slice(0, 100),
    content: `**Vendor:** ${value.vendor}\n**Total:** ${value.total}`,
    metadata: { total: value.total, currency: value.currency },
  };
},

Writing Effective Agent Descriptions

The routing description must be distinct from all other agents:

Bad (ambiguous)Good (specific)
"Processes documents""Extracts structured data from invoice PDFs: vendor, line items, totals, due dates"
"Helps with content""Writes long-form scripts for YouTube videos, podcasts, and webinars"
"Analyzes data""Scans receipt images and extracts structured line-item data including totals and payment info"

If two agents have overlapping descriptions, the orchestrator will mis-route messages unpredictably.