Skip to main content

Multi-Agent Architecture

CopilotKit supports running multiple agents simultaneously, each with their own tools, state, and message threads. This enables complex workflows where specialized agents handle different aspects of your application.

Overview

Multi-agent systems in CopilotKit provide:
  • Agent isolation - Each agent has its own state, tools, and message history
  • Scoped tools - Tools can be registered for specific agents using agentId
  • Independent execution - Agents run concurrently without blocking each other
  • Cross-agent coordination - Agents can communicate through shared state or tools

Registering Multiple Agents

Define multiple agents in your runtime configuration:
import { CopilotRuntime } from "@copilotkit/runtime";
import { BasicAgent } from "@copilotkit/agent";

const runtime = new CopilotRuntime({
  agents: {
    // Research agent specializes in information gathering
    research: new BasicAgent({
      model: "openai/gpt-4o",
      instructions: "You are a research assistant. Gather and summarize information."
    }),
    
    // Writing agent specializes in content creation
    writing: new BasicAgent({
      model: "openai/gpt-4o-mini",
      instructions: "You are a writing assistant. Create clear, engaging content."
    }),
    
    // Code agent specializes in programming tasks
    code: new BasicAgent({
      model: "anthropic/claude-3-5-sonnet",
      instructions: "You are a coding assistant. Write clean, efficient code."
    })
  }
});

Agent-Scoped Tools

Register tools that are only available to specific agents:
import { CopilotKitCore } from "@copilotkit/core";

const core = new CopilotKitCore();

// Tool only available to research agent
core.addTool({
  name: "searchDatabase",
  description: "Search internal knowledge base",
  agentId: "research",
  inputSchema: z.object({
    query: z.string()
  }),
  execute: async ({ query }) => {
    const results = await database.search(query);
    return results;
  }
});

// Tool only available to code agent
core.addTool({
  name: "executeCode",
  description: "Execute code in a sandbox",
  agentId: "code",
  inputSchema: z.object({
    code: z.string(),
    language: z.enum(["python", "javascript"])
  }),
  execute: async ({ code, language }) => {
    return await sandbox.execute(code, language);
  }
});

// Global tool available to all agents
core.addTool({
  name: "getCurrentTime",
  description: "Get the current time",
  inputSchema: z.object({}),
  execute: async () => {
    return new Date().toISOString();
  }
});

Independent State Management

Each agent maintains its own isolated state:
// Frontend - Access state for specific agents
function ResearchPanel() {
  const [researchState] = useAgentState("research");
  
  return (
    <div>
      <h2>Research Findings</h2>
      <ul>
        {researchState.findings?.map((finding) => (
          <li key={finding.id}>{finding.text}</li>
        ))}
      </ul>
    </div>
  );
}

function WritingPanel() {
  const [writingState] = useAgentState("writing");
  
  return (
    <div>
      <h2>Draft Content</h2>
      <p>{writingState.draft}</p>
    </div>
  );
}
States are tracked independently:
// Backend - Each agent's state is isolated
runtime.core.getStateByRun("research", "thread1", "run1");
// { findings: [...] }

runtime.core.getStateByRun("writing", "thread1", "run1");
// { draft: "..." }

// Cross-agent lookups return undefined
runtime.core.getStateByRun("research", "thread2", "msg1"); // undefined

Agent Selection

Automatic Agent Selection

CopilotKit can automatically route messages to the appropriate agent based on context:
import { useCopilotChat } from "@copilotkit/react";

function ChatInterface() {
  const { messages } = useCopilotChat({
    // Agents are selected based on available tools and instructions
    agentStrategy: "auto"
  });
  
  return <ChatView messages={messages} />;
}

Manual Agent Selection

Explicitly specify which agent should handle the conversation:
function SpecializedChat() {
  const { messages, sendMessage } = useCopilotChat({
    agentId: "research"
  });
  
  return (
    <div>
      <h2>Research Assistant</h2>
      <ChatView messages={messages} />
      <button onClick={() => sendMessage("Find information about AI")}>
        Ask Research Agent
      </button>
    </div>
  );
}

Dynamic Agent Switching

Switch between agents during a conversation:
function MultiAgentChat() {
  const [activeAgent, setActiveAgent] = useState("research");
  
  const { messages } = useCopilotChat({
    agentId: activeAgent
  });
  
  return (
    <div>
      <select value={activeAgent} onChange={(e) => setActiveAgent(e.target.value)}>
        <option value="research">Research</option>
        <option value="writing">Writing</option>
        <option value="code">Code</option>
      </select>
      <ChatView messages={messages} />
    </div>
  );
}

Cross-Agent Communication

Agents can coordinate through several mechanisms:

Shared State

Agents can read and write to a common state namespace:
// Research agent writes findings
await researchAgent.run({
  state: {
    sharedFindings: ["Finding 1", "Finding 2"]
  }
});

// Writing agent reads findings and creates content
await writingAgent.run({
  state: {
    draft: createDraftFrom(sharedFindings)
  }
});

Agent-to-Agent Tools

Create tools that enable one agent to invoke another:
core.addTool({
  name: "askResearchAgent",
  description: "Query the research agent for information",
  agentId: "writing",
  inputSchema: z.object({
    question: z.string()
  }),
  execute: async ({ question }) => {
    // Invoke research agent
    const response = await runtime.runAgent("research", {
      messages: [{ role: "user", content: question }]
    });
    return response.text;
  }
});

Event-Based Coordination

Subscribe to agent events for cross-agent coordination:
researchAgent.subscribe({
  onRunFinishedEvent: ({ event, state }) => {
    // Notify writing agent when research completes
    if (state.findings?.length > 0) {
      writingAgent.run({
        messages: [{
          role: "system",
          content: `Research findings available: ${JSON.stringify(state.findings)}`
        }]
      });
    }
  }
});

Message Thread Management

Each agent maintains separate message threads:
// Track messages by agent
const researchMessages = runtime.core.getMessagesForAgent("research", "thread1");
const writingMessages = runtime.core.getMessagesForAgent("writing", "thread1");

// Associate messages with runs
const runId = runtime.core.getRunIdForMessage("research", "thread1", "msg1");

Multi-Agent Workflow Example

Here’s a complete example of a content creation workflow using multiple agents:
// Backend - Agent setup
import { CopilotRuntime } from "@copilotkit/runtime";
import { BasicAgent } from "@copilotkit/agent";

const runtime = new CopilotRuntime({
  agents: {
    researcher: new BasicAgent({
      model: "openai/gpt-4o",
      instructions: `
        You are a research specialist. When asked to research a topic:
        1. Search for relevant information
        2. Summarize key findings
        3. Update state with findings array
      `
    }),
    
    writer: new BasicAgent({
      model: "openai/gpt-4o",
      instructions: `
        You are a content writer. Create articles based on research findings.
        Check the shared state for research findings before writing.
      `
    }),
    
    editor: new BasicAgent({
      model: "openai/gpt-4o-mini",
      instructions: `
        You are an editor. Review drafts and suggest improvements.
        Focus on clarity, grammar, and structure.
      `
    })
  }
});

// Frontend - Workflow orchestration
function ContentCreationWorkflow() {
  const [step, setStep] = useState(1);
  const [researchState] = useAgentState("researcher");
  const [writingState] = useAgentState("writer");
  
  return (
    <div>
      {step === 1 && (
        <AgentChat 
          agentId="researcher"
          onComplete={() => setStep(2)}
        />
      )}
      
      {step === 2 && (
        <div>
          <h3>Research Complete</h3>
          <ul>
            {researchState.findings?.map((f) => <li key={f.id}>{f.text}</li>)}
          </ul>
          <AgentChat 
            agentId="writer"
            initialContext={{ findings: researchState.findings }}
            onComplete={() => setStep(3)}
          />
        </div>
      )}
      
      {step === 3 && (
        <div>
          <h3>Draft Created</h3>
          <article>{writingState.draft}</article>
          <AgentChat 
            agentId="editor"
            initialContext={{ draft: writingState.draft }}
          />
        </div>
      )}
    </div>
  );
}

Agent Constraints and Validation

CopilotKit validates agent IDs to ensure consistency:
// Agent IDs must match between registration and usage
const core = new CopilotKitCore();

core.addTool({
  name: "myTool",
  agentId: "myAgent", // Must match registered agent
  execute: async () => { /* ... */ }
});

// This will work
runtime.runAgent("myAgent", { /* ... */ });

// This will fail - agent not registered
runtime.runAgent("unknownAgent", { /* ... */ }); // Error!

Performance Considerations

Parallel Execution

Agents run independently and can execute in parallel for better performance

Model Selection

Use faster models (gpt-4o-mini) for simple tasks and more capable models (gpt-4o) for complex reasoning

Tool Scoping

Limit tool availability per agent to reduce context size and improve response quality

State Isolation

Keep agent states separate to avoid memory overhead and maintain clear boundaries

Best Practices

  1. Clear Responsibilities - Each agent should have a well-defined purpose and expertise
  2. Minimal Tool Sets - Only provide tools relevant to each agent’s responsibilities
  3. Explicit Handoffs - Use clear signals when passing work between agents
  4. State Namespacing - Use consistent naming conventions for shared state fields
  5. Error Handling - Handle agent failures gracefully without affecting other agents

Shared State

Learn about state management in multi-agent systems

Runtime Middleware

Intercept and coordinate agent requests with middleware