Back to home

03 — ADVANCED PATTERNS

Sub-agents: Tools that think

12 min read

You've built an agent with tools. It reads files, searches code, makes edits. Works great for general tasks. But then you add payments integration. Suddenly your agent needs to understand Stripe's webhook system, handle test vs. production keys, coordinate database schema changes with API routes, and debug webhook signatures.

You could add all that knowledge to your main agent's prompt. But now it's 10,000 tokens long, and the agent gets confused about when to use which approach. Or you could create a dozen new tools—create_stripe_webhook, test_stripe_endpoint, setup_stripe_keys. But tools don't reason. They just execute.

What you need is specialization. An agent that only thinks about payments. One that understands Stripe end-to-end, uses the right tools, and figures out the steps. Then your main agent just says "handle the payment flow" and delegates.

That's what sub-agents do. They're complete agents—with their own prompts, tools, and reasoning—packaged as tools your main agent can call.

Why Sub-Agents Matter

Think about how teams work. You don't ask a backend engineer to also do design, DevOps, and write documentation. You have specialists. When you need a payment system, you bring in someone who knows payment systems.

Agents are the same way. One agent trying to handle everything becomes a generalist that's mediocre at each task. Multiple focused agents excel at their domain.

The Specialization Problem

Your main agent has to know:

  • General coding patterns
  • File operations
  • Testing approaches
  • Error handling
  • Database design
  • API development
  • Frontend components
  • Authentication flows
  • Payment processing
  • Design systems
  • Deployment strategies

That's too much. The prompt becomes massive. The agent second-guesses itself. It uses the wrong tool for the task because the context is overwhelming.

The Sub-Agent Solution

Instead, your main agent knows:

  • General coding
  • How to delegate

And you have specialized agents that know their domain deeply:

  • Payments Agent: Stripe integration, webhooks, subscription logic
  • Design Agent: Component systems, design tokens, asset management
  • Database Agent: Schema design, migrations, seeding, API routes
  • Auth Agent: OAuth flows, session management, permissions

Each agent has:

  • A focused system prompt with domain expertise
  • Domain-specific tools
  • Deep knowledge of best practices for that area
  • Examples and patterns for common scenarios

When your main agent sees "add Stripe subscriptions," it calls the payments sub-agent. That's it. The sub-agent figures out the implementation, coordinates tools, and handles edge cases.

What Sub-Agents Actually Are

A sub-agent isn't a special construct. It's a regular agent wrapped in a tool interface.

// This is a sub-agent
export const paymentsAgentTool = tool({
  name: "PaymentsAgent",
  description: "Specialized agent for Stripe payment integrations",
  parameters: z.object({
    prompt: z.string().describe(
      "Detailed prompt describing the payment feature to implement"
    ),
  }),
  async execute({ prompt }, runContext) {
    // Create a full agent instance
    const paymentsAgent = new PaymentsAgent({
      mux: runContext.context.mux,
      context: runContext.context,
      agentMode: "auto",
      abortSignal: runContext.context.abortSignal,
    });
 
    // Run it like any agent
    const result = await paymentsAgent.processRequest({
      userPrompt: prompt,
    });
 
    return result;
  },
});

Your main agent sees this as a tool. It calls PaymentsAgent with a prompt. Behind the scenes, a complete agent starts up, uses its own tools, reasons through the task, and returns results.

How Sub-Agents Work

Let's trace what happens when your main agent delegates to a sub-agent.

1. Main Agent Decides to Delegate

Your main agent sees a user request:

User: "Add Stripe checkout for subscriptions"

The main agent's tools include PaymentsAgent. The tool description says:

"Use this tool whenever implementing Stripe payment integrations.
Handles payment flows, checkouts, subscriptions, webhooks, and testing."

The agent decides this is a payments task. It calls:

PaymentsAgent({
  prompt: "Implement Stripe checkout flow for subscription plans.
          Support monthly and annual billing. Include webhook handling
          for payment success events."
})

2. Sub-Agent Starts Up

The tool wrapper creates a new agent instance:

export class PaymentsAgent {
  private agent: Agent<AgentContext>;
 
  constructor(options: PaymentsAgentOptions) {
    this.context = options.context;
    this.mux = options.mux;
 
    // Initialize with specialized prompt and tools
    this.agent = new Agent({
      name: 'Payments Agent',
      instructions: PAYMENTS_SYSTEM_PROMPT, // Stripe-specific knowledge
      tools: [
        readTool,
        writeTool,
        editTool,
        webSearchTool,        // Search current Stripe docs
        setupStripeTool,      // Create Stripe test account
        deployWebhookTool,    // Deploy webhook as edge function
        testWebhookTool,      // Test with signed requests
        // ... more payment-specific tools
      ],
      model: this.modelMapping['auto'],
    });
  }
}

3. Sub-Agent Executes

The sub-agent receives the prompt and runs its full reasoning loop:

async processRequest(options: PaymentsProcessOptions) {
  // Emit event so UI knows sub-agent started
  await this.mux.put({
    type: 'coding_agent.subagent.start',
    data: { subagent: 'payments', prompt: options.userPrompt }
  });
 
  // Add Stripe context if available
  if (!this.stripeContext) {
    this.stripeContext = await getStripeContext(this.context.projectId);
  }
 
  const messages = [
    { role: 'user', content: [
      { type: 'input_text', text: options.userPrompt },
      { type: 'input_text', text: this.stripeContext },
      { type: 'input_text', text: formatTodoContext(this.context.todos) }
    ]}
  ];
 
  // Run the agent - full reasoning loop with tool calls
  const stream = await run(this.agent, messages, {
    context: this.context,
    maxTurns: 100,
    stream: true,
  });
 
  // Stream events back through shared mux
  for await (const event of stream) {
    await this.mux.put(event);
  }
 
  // Return summary for main agent
  return `Payments agent completed. Implemented checkout flow with webhooks.`;
}

The sub-agent:

  • Searches Stripe docs for current best practices
  • Reads existing code to understand the project structure
  • Creates API routes for checkout sessions
  • Sets up webhook endpoints
  • Writes tests for payment flows
  • Tracks changes in shared context

All the tool calls stream through the shared mux, so the UI shows progress in real-time.

4. Sub-Agent Returns Results

When done, the sub-agent returns a summary:

return `✅ Payments implementation complete.
 
Tools used:
- web_search: 3 calls (Stripe docs lookup)
- read: 8 calls (existing code analysis)
- write: 4 calls (API routes + webhook)
- setup_stripe: 1 call (test account created)
- test_webhook: 2 calls (webhook validation)
 
Changes:
- Created /app/api/checkout/route.ts
- Created /app/api/webhooks/stripe/route.ts
- Updated database schema with subscription table
- Added Stripe SDK to dependencies
 
Next steps: Test checkout flow in browser`;

5. Main Agent Continues

Your main agent receives this result as tool output. It can:

  • Continue with the next task
  • Ask the user for confirmation
  • Delegate to another sub-agent
  • Make additional changes
Main Agent: "I've delegated the Stripe implementation to the payments
             specialist. It's set up checkout, webhooks, and database
             schema. Ready to test the flow when you are."

Building a Sub-Agent

Let's walk through creating a sub-agent from scratch.

Step 1: Define the Agent Class

Start with the structure from the agent class guide:

export class DatabaseAgent {
  private agent: Agent<DatabaseAgentContext>;
  private mux: AgentMux;
  private context: AgentContext;
  private helpers: DatabaseHelpers;
 
  constructor(projectId: string, mux: AgentMux, sandboxClient: any) {
    this.mux = mux;
    this.context = { projectId, sandboxClient, mux };
    this.helpers = new DatabaseHelpers(sandboxClient, mux);
    this.agent = this.initializeAgent();
  }
 
  private initializeAgent(): Agent<DatabaseAgentContext> {
    return new Agent({
      name: 'Database Agent',
      instructions: DATABASE_SYSTEM_PROMPT,
      tools: [
        manageTableSchemaTool,
        manageApiRoutesTool,
        generateSeederTool,
        executeSqlQueryTool,
        // Include general tools too
        readTool,
        writeTool,
        grepTool,
      ],
      model: this.getModel('claude-sonnet-4'),
    });
  }
 
  async processRequest(prompt: string) {
    // Implementation
  }
}

Step 2: Write the System Prompt

Your sub-agent needs deep domain knowledge:

export const DATABASE_SYSTEM_PROMPT = `
You are a database specialist helping implement data models and API routes.
 
# Your Expertise
 
You understand:
- Relational database design (normalization, indexes, constraints)
- Drizzle ORM for schema definition and migrations
- RESTful API patterns and Next.js route handlers
- Data seeding for development and testing
- SQL query optimization
 
# Your Tools
 
- manage_table_schema: Modify database schema using Drizzle syntax
- manage_api_routes: Create/edit Next.js API route handlers
- generate_seeder_file: Create seed data for testing
- execute_sql_query: Run queries to inspect database state
 
# Your Approach
 
When implementing database features:
 
1. **Design Schema First**: Think about relationships, constraints, indexes
2. **Create Tables**: Use manage_table_schema with Drizzle syntax
3. **Build API Routes**: Use manage_api_routes for CRUD operations
4. **Add Seed Data**: Use generate_seeder_file for realistic test data
5. **Test**: Use execute_sql_query to verify data integrity
 
# Best Practices
 
- Always include timestamps (createdAt, updatedAt)
- Use proper foreign key constraints for relationships
- Add indexes for frequently queried columns
- Include input validation in API routes
- Use transactions for multi-step operations
- Return proper HTTP status codes
 
# Common Patterns
 
For a blog post system:
- posts table with userId foreign key
- Cascade delete for comments when post deleted
- Index on userId for user's posts query
- Seed with diverse content for testing
 
For e-commerce:
- products, orders, order_items tables
- Check constraints for positive prices
- Inventory tracking with transactions
- Generate realistic product catalogs
 
Work systematically. If you encounter errors, read the error message carefully
and fix the root cause. Don't move on until each step succeeds.
`;

The prompt gives the agent:

  • Clear identity and expertise
  • Tool descriptions in context
  • Step-by-step approach
  • Domain-specific patterns
  • Error handling guidance

Step 3: Create Domain-Specific Tools

Sub-agents often need specialized tools:

export const deployWebhookTool = tool({
  name: 'deploy_stripe_webhook',
  description: 'Deploys Stripe webhook handler as Supabase Edge Function',
  parameters: z.object({
    webhook_code: z.string().describe('TypeScript code for webhook handler'),
    event_types: z.array(z.string()).describe(
      'Stripe event types to handle (e.g., ["payment_intent.succeeded"])'
    ),
  }),
  async execute({ webhook_code, event_types }, runContext) {
    const { supabaseClient, helpers } = runContext.context;
 
    await helpers.emitStepStarted("Deploying webhook", "webhook");
 
    try {
      // Deploy to Supabase Edge Functions
      const functionName = 'stripe-webhook';
      await supabaseClient.functions.deploy(functionName, webhook_code);
 
      // Get webhook URL
      const webhookUrl = await supabaseClient.functions.getUrl(functionName);
 
      // Register with Stripe
      await registerStripeWebhook(webhookUrl, event_types);
 
      await helpers.emitStepCompleted("Deployed webhook", "webhook");
 
      return `✅ Webhook deployed to ${webhookUrl} and registered with Stripe`;
    } catch (error) {
      await helpers.emitStepFailed("Deploy webhook", error.message);
      return `❌ Deployment failed: ${error.message}`;
    }
  }
});

These tools encapsulate domain complexity. The agent calls deploy_stripe_webhook instead of manually running supabase functions deploy, figuring out URLs, and calling Stripe APIs.

Step 4: Wrap in Tool Interface

Make your agent callable as a tool:

export const databaseAgentTool = tool({
  name: "DatabaseAgent",
  description: `
Specialized agent for database schema, API routes, and data seeding.
 
Use this when:
- Creating or modifying database tables
- Implementing CRUD API endpoints
- Generating seed data for testing
- Designing data models and relationships
 
The agent will handle schema design, API route implementation,
and database setup end-to-end.
  `,
  parameters: z.object({
    prompt: z.string().describe(
      "Detailed description of database features to implement"
    ),
  }),
  async execute({ prompt }, runContext) {
    const databaseAgent = new DatabaseAgent(
      runContext.context.projectId,
      runContext.context.mux,
      runContext.context.sandboxClient
    );
 
    const result = await databaseAgent.processRequest(prompt);
    return result;
  },
});

Step 5: Register with Main Agent

Add to your main agent's tool list:

// tools/index.ts
export const allCodingTools = [
  readTool,
  writeTool,
  grepTool,
  // ... general tools
  databaseAgentTool,    // Sub-agent for database tasks
  paymentsAgentTool,    // Sub-agent for payments
  designAgentTool,      // Sub-agent for design
];

Context Sharing Between Agents

Sub-agents need access to shared state. That's why they receive runContext:

async execute({ prompt }, runContext?: RunContext<AgentContext>) {
  const context = runContext.context;
 
  // Shared state
  const projectId = context.projectId;
  const sandboxClient = context.sandboxClient;
  const mux = context.mux;
  const abortSignal = context.abortSignal;
  const todos = context.todos;
 
  // Sub-agent can read and modify shared context
  const subAgent = new PaymentsAgent({
    context,
    mux,
    abortSignal,
  });
}

What Gets Shared

File System Access: Both agents read/write the same project files through sandboxClient.

Event Stream: Sub-agent events go through the same mux, so the UI shows unified progress.

Todo List: Sub-agents can read and update the shared todo context.

Abort Signal: If user cancels, both agents stop.

Project State: Sub-agents can access project metadata, environment variables, configuration.

What Doesn't Get Shared

Conversation History: Each agent has its own message history. The main agent doesn't see sub-agent's internal reasoning.

Tool Call Context: Sub-agent tool calls are independent. The main agent just sees the final result.

Model State: Each agent uses its own model, settings, and caching.

This separation is intentional. The main agent delegates and moves on. The sub-agent reasons independently.

When to Use Sub-Agents

Not every task needs a sub-agent. Here's when they make sense:

Use Sub-Agents For:

Complex Domains: Payment processing, auth flows, design systems. Things with deep expertise, multiple tools, and nuanced decisions.

Multi-Step Workflows: Database setup requires schema design → API routes → seed data → testing. Let a sub-agent coordinate.

Specialized Knowledge: Stripe's webhook signatures, OAuth2 flows, design token systems. Domain-specific patterns that clutter your main prompt.

Reusable Capabilities: If multiple projects need the same expertise (payments, design, database), build it once as a sub-agent.

Use Regular Tools For:

Simple Operations: Reading files, searching code, running commands. No reasoning needed.

Single Purpose: If a tool does one thing, it doesn't need agent-level reasoning.

Immediate Results: Tools that return instantly (list directory, check file exists).

Low Complexity: String manipulation, data formatting, status checks.

Real Example: Payments Sub-Agent

Let's see how the payments sub-agent handles a real request.

User asks: "Add Stripe subscriptions with monthly and annual plans"

Main Agent Delegates

// Main agent calls
PaymentsAgent({
  prompt: `Implement Stripe subscription checkout:
  - Two plans: Pro Monthly ($20/mo) and Pro Annual ($200/yr)
  - Checkout page at /pricing
  - Handle successful payments with webhook
  - Store subscription status in database`
})

Sub-Agent Executes

The payments agent runs through its workflow:

1. Search Stripe Docs

web_search("Stripe subscription checkout best practices 2024")
web_search("Stripe webhook signature verification Next.js")

2. Read Existing Code

read("package.json")           // Check if Stripe SDK installed
read("src/db/schema.ts")       // See current database structure
grep("stripe", "src/")         // Find any existing Stripe code

3. Update Database Schema

edit_string_replace({
  file_path: "src/db/schema.ts",
  old_string: `export const users = ...`,
  new_string: `export const users = ...
 
export const subscriptions = pgTable('subscriptions', {
  id: serial('id').primaryKey(),
  userId: integer('user_id').references(() => users.id),
  stripeSubscriptionId: text('stripe_subscription_id').notNull(),
  stripePriceId: text('stripe_price_id').notNull(),
  status: text('status').notNull(),
  currentPeriodEnd: timestamp('current_period_end'),
  createdAt: timestamp('created_at').defaultNow(),
});`
})

4. Create Checkout API Route

write({
  file_path: "src/app/api/checkout/route.ts",
  content: `import Stripe from 'stripe';
import { NextRequest, NextResponse } from 'next/server';
 
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
 
export async function POST(req: NextRequest) {
  const { priceId, userId } = await req.json();
 
  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: process.env.NEXT_PUBLIC_URL + '/success',
    cancel_url: process.env.NEXT_PUBLIC_URL + '/pricing',
    metadata: { userId },
  });
 
  return NextResponse.json({ url: session.url });
}`
})

5. Deploy Webhook Handler

deploy_stripe_webhook({
  webhook_code: `// Webhook handler with signature verification...`,
  event_types: ['customer.subscription.created', 'customer.subscription.updated']
})

6. Test the Flow

test_webhook_endpoint({
  endpoint: '/api/webhooks/stripe',
  event_type: 'customer.subscription.created'
})

7. Return Summary

return `✅ Stripe subscriptions implemented:
 
Created:
- Database table for subscriptions
- Checkout API at /api/checkout
- Webhook handler at /api/webhooks/stripe
- Test mode configured
 
Plans configured:
- Pro Monthly: price_xxx ($20/month)
- Pro Annual: price_yyy ($200/year)
 
Webhook registered for events:
- customer.subscription.created
- customer.subscription.updated
 
Test the checkout flow at /pricing. Use test card 4242 4242 4242 4242.`;

Main Agent Receives Result

The main agent sees the summary and continues:

Main Agent: "Subscription system is ready. The payments specialist handled
             the Stripe setup, database schema, and webhook configuration.

             You can test it at /pricing with card number 4242 4242 4242 4242.

             Would you like me to create the pricing page UI next?"

Benefits of This Architecture

Focused Expertise: Each sub-agent knows its domain deeply. Better results than one agent trying to do everything.

Cleaner Prompts: Main agent prompt stays concise. Domain knowledge lives in sub-agent prompts.

Reusable Capabilities: Build the payments agent once, use it in every project that needs payments.

Easier Testing: Test sub-agents independently. Verify they handle edge cases correctly.

Parallel Development: Different developers can work on different sub-agents. Clear interfaces.

Better Token Usage: Main agent doesn't load payment expertise until needed. Sub-agent caching optimizes repeated calls.

Common Patterns

Parent-Child Delegation

Main agent delegates entire features:

// Main agent
const result = await DatabaseAgent({
  prompt: "Set up user management with posts and comments"
});
 
// Later
const authResult = await AuthAgent({
  prompt: "Add JWT authentication to protect /api/posts routes"
});

Peer Collaboration

Sub-agents can call other sub-agents:

// Design agent calls database agent
async processRequest(prompt: string) {
  // Generate design system
  const designSystem = await this.generateDesignSystem(prompt);
 
  // Delegate data model creation to database agent
  const dbResult = await databaseAgentTool.execute({
    prompt: `Create database schema for: ${designSystem.dataRequirements}`
  }, this.runContext);
 
  return designSystem;
}

Sequential Specialization

Break complex tasks into phases:

// 1. Design agent creates the design system
const designResult = await DesignAgent({ prompt: userRequest });
 
// 2. Database agent implements data layer
const dbResult = await DatabaseAgent({
  prompt: `Implement database for: ${designResult.dataModels}`
});
 
// 3. Main agent implements UI using design system and API routes
// ... implement UI components

What We're Skipping

Sub-agent orchestration (when multiple sub-agents need to coordinate), sub-agent conflict resolution (what if two sub-agents modify the same file), cost optimization (caching strategies across agents), monitoring and observability (tracking sub-agent performance).

These matter for production systems, but the basics we've covered will get you started.

What's Next

You now understand how to build specialized agents and compose them into a system. In the next guide, we'll explore agent context and state management—how to maintain shared state, pass data between tools and agents, and coordinate complex workflows.

That's where your agent system becomes truly powerful.