Kritsana.prs

ChangelogContactResume

© Copyright 2026 Kritsana.Dev. All rights reserved.

Back to Blog
AIFeatured

Building Sub-Agents: How to Design Multi-Agent AI Systems

Learn how to architect and build sub-agent systems — breaking complex AI tasks into specialized agents that collaborate, delegate, and deliver better results.

April 6, 2026
·
6 min read
Building Sub-Agents: How to Design Multi-Agent AI Systems

As AI agents grow more capable, the tasks we assign them grow more complex. A single monolithic agent often struggles with multi-step workflows that require different expertise at each stage. The solution? Sub-agents — specialized agents that each handle one part of the problem, coordinated by a parent agent.

This guide covers the architecture, patterns, and practical implementation of sub-agent systems.

What is a Sub-Agent?

A sub-agent is a specialized AI agent that is invoked by a parent (orchestrator) agent to handle a specific subtask. Think of it like a team lead delegating work:

  • Orchestrator Agent — receives the user's request, breaks it into subtasks, and delegates
  • Sub-Agent A — handles research and information gathering
  • Sub-Agent B — handles code generation
  • Sub-Agent C — handles review and quality assurance

Each sub-agent can have its own system prompt, tools, context window, and even a different model.

Why Use Sub-Agents?

| Single Agent | Multi-Agent | | ----------------------------------- | --------------------------------------------- | | One system prompt for everything | Specialized prompts per task | | Single context window fills up fast | Each agent gets a fresh context | | One model for all tasks | Use cheaper models for simple tasks | | Hard to debug failures | Isolate failures to specific agents | | Difficult to extend | Add new agents without changing existing ones |

Architecture Patterns

Pattern 1: Sequential Pipeline

Agents execute in a fixed order, each passing output to the next.

User Request → [Research Agent] → [Writer Agent] → [Editor Agent] → Final Output
async function sequentialPipeline(userRequest: string) {
  // Step 1: Research
  const research = await researchAgent.execute({
    task: `Research the following topic: ${userRequest}`,
    tools: ['web_search', 'read_url'],
  });

  // Step 2: Write
  const draft = await writerAgent.execute({
    task: `Write an article based on this research: ${research.output}`,
    tools: ['file_write'],
  });

  // Step 3: Edit
  const final = await editorAgent.execute({
    task: `Review and improve this draft: ${draft.output}`,
    tools: ['file_edit'],
  });

  return final.output;
}

Pattern 2: Parallel Fan-Out

The orchestrator dispatches independent subtasks simultaneously.

                  ┌─→ [Frontend Agent] ──┐
User Request → [Orchestrator] ─→ [Backend Agent]  ──→ [Merge Agent] → Output
                  └─→ [Test Agent]     ──┘
async function parallelFanOut(userRequest: string) {
  const plan = await orchestrator.plan(userRequest);

  // Execute independent tasks in parallel
  const results = await Promise.all([
    frontendAgent.execute(plan.frontendTask),
    backendAgent.execute(plan.backendTask),
    testAgent.execute(plan.testTask),
  ]);

  // Merge results
  const merged = await mergeAgent.execute({
    task: 'Combine these outputs into a cohesive result',
    context: results.map(r => r.output),
  });

  return merged.output;
}

Pattern 3: Router (Dynamic Dispatch)

The orchestrator analyzes the request and routes to the appropriate agent.

async function routerPattern(userRequest: string) {
  // Classify the request
  const classification = await orchestrator.classify(userRequest);

  // Route to the right specialist
  switch (classification.type) {
    case 'code_generation':
      return codeAgent.execute(userRequest);
    case 'data_analysis':
      return dataAgent.execute(userRequest);
    case 'documentation':
      return docAgent.execute(userRequest);
    default:
      return generalAgent.execute(userRequest);
  }
}

Pattern 4: Supervisor Loop

The orchestrator iteratively delegates and reviews until quality criteria are met.

async function supervisorLoop(userRequest: string) {
  let result = await workerAgent.execute(userRequest);
  let approved = false;
  let iterations = 0;
  const maxIterations = 3;

  while (!approved && iterations < maxIterations) {
    const review = await reviewerAgent.evaluate(result.output);

    if (review.score >= 0.9) {
      approved = true;
    } else {
      result = await workerAgent.execute({
        task: userRequest,
        feedback: review.feedback,
        previousAttempt: result.output,
      });
    }
    iterations++;
  }

  return result.output;
}

Implementing a Sub-Agent System

Here is a minimal but practical implementation using TypeScript:

Define the Agent Interface

interface AgentConfig {
  name: string;
  model: string;
  systemPrompt: string;
  tools: Tool[];
  maxTokens?: number;
}

interface AgentResult {
  output: string;
  tokensUsed: number;
  toolCalls: ToolCall[];
}

class SubAgent {
  private config: AgentConfig;

  constructor(config: AgentConfig) {
    this.config = config;
  }

  async execute(input: string): Promise<AgentResult> {
    const response = await llm.chat({
      model: this.config.model,
      system: this.config.systemPrompt,
      messages: [{ role: 'user', content: input }],
      tools: this.config.tools,
      maxTokens: this.config.maxTokens ?? 4096,
    });

    return {
      output: response.content,
      tokensUsed: response.usage.totalTokens,
      toolCalls: response.toolCalls,
    };
  }
}

Create Specialized Agents

const researchAgent = new SubAgent({
  name: 'researcher',
  model: 'claude-4-5-sonnet-20260101',
  systemPrompt: `You are a research specialist. Your job is to gather 
    accurate, relevant information on a given topic. Always cite sources 
    and distinguish facts from opinions.`,
  tools: [webSearchTool, urlReaderTool],
});

const codeAgent = new SubAgent({
  name: 'coder',
  model: 'claude-4-5-sonnet-20260101',
  systemPrompt: `You are a senior software engineer. Write clean, 
    well-tested, production-ready code. Follow best practices and 
    include error handling.`,
  tools: [fileReadTool, fileWriteTool, terminalTool],
});

const reviewAgent = new SubAgent({
  name: 'reviewer',
  model: 'claude-4-5-haiku-20260101', // Cheaper model for review
  systemPrompt: `You are a code reviewer. Check for bugs, security 
    issues, and style violations. Be concise and actionable.`,
  tools: [fileReadTool],
});

Build the Orchestrator

class Orchestrator {
  private agents: Map<string, SubAgent>;

  constructor() {
    this.agents = new Map();
  }

  registerAgent(name: string, agent: SubAgent) {
    this.agents.set(name, agent);
  }

  async run(task: string): Promise<string> {
    // Step 1: Plan
    const plan = await this.planTask(task);

    // Step 2: Execute sub-tasks
    const results: string[] = [];
    for (const step of plan.steps) {
      const agent = this.agents.get(step.agentName);
      if (!agent) throw new Error(`Agent ${step.agentName} not found`);

      const result = await agent.execute(step.instruction);
      results.push(`[${step.agentName}]: ${result.output}`);
    }

    // Step 3: Synthesize
    return results.join('\n\n');
  }

  private async planTask(task: string) {
    // Use LLM to decompose the task
    const response = await llm.chat({
      model: 'claude-4-5-haiku-20260101',
      system: 'Decompose this task into steps. Return JSON.',
      messages: [{ role: 'user', content: task }],
    });

    return JSON.parse(response.content);
  }
}

Best Practices

1. Keep Agents Focused

Each sub-agent should do one thing well. If an agent's system prompt exceeds a paragraph, consider splitting it.

2. Use the Right Model for the Job

Not every subtask needs the most powerful model:

  • Planning/Orchestration — use a strong model (Sonnet, GPT-4o)
  • Simple classification — use a fast model (Haiku, GPT-4o-mini)
  • Code generation — use a code-specialized model

3. Limit Context Passing

Only pass relevant output between agents. Sending entire conversation histories wastes tokens and dilutes focus.

4. Add Error Boundaries

async function safeExecute(agent: SubAgent, input: string) {
  try {
    return await agent.execute(input);
  } catch (error) {
    console.error(`Agent ${agent.name} failed:`, error);
    return { output: `Error: ${error.message}`, tokensUsed: 0, toolCalls: [] };
  }
}

5. Log Everything

Track which agent handled what, how many tokens were used, and what tools were called. This is essential for debugging and cost optimization.

Wrapping Up

Sub-agent architectures let you decompose complex AI tasks into manageable, specialized units. Whether you use a sequential pipeline, parallel fan-out, router, or supervisor loop depends on your specific use case.

Start simple with two agents (worker + reviewer), measure the results, and scale up as needed. The key is to treat each agent as a composable building block — focused, testable, and replaceable.

More in AI

Getting Started with OpenCode: The Open-Source Alternative to Claude Code

Getting Started with OpenCode: The Open-Source Alternative to Claude Code

A complete guide to installing and using OpenCode — the open-source, terminal-native AI coding assistant that supports 75+ LLM providers as a powerful alternative to Claude Code.

Read article
AI in Web Development

AI in Web Development

Explore how artificial intelligence is transforming web development and what it means for developers.

Read article
View all in AI