Skip to main content

Multi-agent patterns with the GitHub Copilot SDK

We've covered a lot of ground in this series: sessions and lifecycle, deployment and scaling, MCP integration, and skills. Each post added a new capability layer to a single agent. This post changes the shape of the problem: instead of one agent doing more things, we compose multiple agents doing the right things.

There are two different levels at which you can do this. Inside the Copilot SDK itself, custom agents let the Copilot runtime orchestrate specialised sub-agents automatically within a single session. Beyond the SDK, the Microsoft Agent Framework lets you compose Copilot SDK agents with agents from any other provider in structured multi-agent workflows.

In this post we stay inside the SDK. Our next and final post will look at the broader ecosystem and Microsoft Agent Framework integration.

The problem with a single agent

A single session with a broad system prompt is fine for many use cases. But as tasks grow more complex, the cracks show:

  • A general agent needs a large context to cover all domains, which drives up token usage and increases the chance of confusion
  • Tool access becomes an all-or-nothing decision — you either give the agent everything or nothing
  • The agent's behaviour is hard to constrain to specific task types without prompt gymnastics

Custom agents solve this by letting you define specialised agents with scoped prompts, restricted tool sets, and their own optional MCP servers — all within a single session. The Copilot runtime acts as the orchestrator, automatically delegating user requests to the most appropriate sub-agent based on the task.

Custom agents

Custom agents are lightweight agent definitions you attach to a session. Each agent has its own system prompt, tool restrictions, and optional MCP servers. When a user's request matches an agent's expertise, the Copilot SDK runtime automatically delegates to that agent as a sub-agent — running it in an isolated context while streaming lifecycle events back to the parent session.

Here's the minimal .NET setup:

using GitHub.Copilot.SDK;

var session = await client.CreateSessionAsync(new SessionConfig
{
    Model = "gpt-4.1",
    CustomAgents =
    [
        new CustomAgentConfig
        {
            Name = "researcher",
            DisplayName = "Research Agent",
            Description = "Explores codebases, reads files, and answers questions. " +
                          "Use for analysis, investigation, and understanding tasks.",
            Tools = ["grep", "glob", "view"],       // read-only tools only
            Prompt = "You are a research assistant. Analyse code and answer questions " +
                     "without making any changes."
        },
        new CustomAgentConfig
        {
            Name = "editor",
            DisplayName = "Editor Agent",
            Description = "Writes and modifies code. Use for implementation, " +
                          "refactoring, and code generation tasks.",
            Tools = ["grep", "glob", "view", "edit", "write"],
            Prompt = "You are a code editor. Make minimal, precise changes. " +
                     "Always verify the change compiles before finishing."
        }
    ]
});

With this setup, a prompt like "explain what this authentication middleware does" will route to the researcher agent, while "add retry logic to the HTTP client" will route to the editor agent. The user doesn't know which agent is handling their request — they just get a better, more focused answer.

On descriptions: a good description helps the runtime match user intent to the right agent. Be specific about the agent's expertise and capabilities. Think of the description the same way you'd write a skill description: it's a semantic index, not a display label.

Pre-selecting an agent

By default, the runtime picks the most appropriate agent for each turn. If you want to start a session with a specific agent active — for a workflow where you know the first task type — set the Agent property on SessionConfig:

var session = await client.CreateSessionAsync(new SessionConfig
{
    Model = "gpt-4.1",
    Agent = "researcher",   // start with the researcher active
    CustomAgents = [ /* ... */ ]
});

The value must match the Name of one of the agents defined in CustomAgents. This is useful when your application's entry point is task-specific — a "review this PR" button should probably start in the researcher agent rather than letting the runtime decide from a cold start.

The three orchestration patterns

Custom agents support three orchestration patterns, each suited to different workflow shapes.

Pattern 1: Automatic delegation

The simplest pattern. You define agents, send prompts, and the runtime routes automatically. The user (or calling code) stays unaware of which agent is active.

This is the default behaviour shown above. It works well for interactive sessions where task types are mixed and unpredictable. The runtime's delegation logic is based on the agent descriptions. Precise descriptions produce more reliable routing.

Pattern 2: Agent handoffs

Handoffs work best when you want a clear human-in-the-loop moment, such as reviews, approvals, or sensitive checks. One agent completes its work and explicitly transfers control to another, optionally passing a summary of what it did.

A practical example: a security review followed by a remediation step, where you want to inspect the findings before allowing code changes:

var session = await client.CreateSessionAsync(new SessionConfig
{
    Model = "gpt-4.1",
    CustomAgents =
    [
        new CustomAgentConfig
        {
            Name = "auditor",
            Description = "Reviews code for security vulnerabilities. Read-only.",
            Tools = ["grep", "glob", "view"],
            Prompt = """
                You are a security auditor. Review the code for vulnerabilities.
                When done, produce a structured findings report and hand off to
                the 'remediator' agent with a summary of what needs fixing.
                """
        },
        new CustomAgentConfig
        {
            Name = "remediator",
            Description = "Applies security fixes identified by the auditor.",
            Tools = ["grep", "glob", "view", "edit", "write"],
            Prompt = """
                You are a security engineer. You receive findings from the auditor.
                Apply the minimum change necessary to address each finding.
                Explain each change as you make it.
                """
        }
    ]
});

// Auditor runs, produces findings, hands off to remediator
var result = await session.SendAndWaitAsync(new MessageOptions
{
    Prompt = "Audit the authentication module for security vulnerabilities and fix any issues found"
});

The handoff happens inside the session — the remediator picks up where the auditor left off, with access to the auditor's findings in context.

Pattern 3: Explicit sub-agent delegation

With explicit sub-agent delegation, a primary agent programmatically invokes other custom agents to handle well-defined subtasks. Unlike handoffs, the user does not switch agents. A parent orchestrator breaks down complex tasks and dispatches them to specialists, then assembles the results.

This is the most structured pattern, and the right choice for repeatable, multi-step pipelines:

var session = await client.CreateSessionAsync(new SessionConfig
{
    Model = "gpt-4.1",
    CustomAgents =
    [
        new CustomAgentConfig
        {
            Name = "orchestrator",
            Description = "Coordinates the full code review pipeline. Entry point for all requests.",
            Tools = [],   // orchestrator doesn't use tools directly
            Prompt = """
                You are a pipeline orchestrator. For every code review request:
                1. Delegate to the 'linter' agent to check style and formatting
                2. Delegate to the 'tester' agent to verify test coverage
                3. Delegate to the 'security' agent to check for vulnerabilities
                4. Synthesise all findings into a single structured report
                Never perform any of these tasks yourself — always delegate.
                """
        },
        new CustomAgentConfig
        {
            Name = "linter",
            Description = "Checks code style, formatting, and conventions.",
            Tools = ["grep", "glob", "view"],
            Prompt = "You are a linting specialist. Check code against established style conventions."
        },
        new CustomAgentConfig
        {
            Name = "tester",
            Description = "Reviews test coverage and test quality.",
            Tools = ["grep", "glob", "view"],
            Prompt = "You are a testing specialist. Evaluate test coverage and flag gaps."
        },
        new CustomAgentConfig
        {
            Name = "security",
            Description = "Reviews code for security vulnerabilities.",
            Tools = ["grep", "glob", "view"],
            Prompt = "You are a security specialist. Review for OWASP Top 10 and common .NET security issues."
        }
    ],
    Agent = "orchestrator"
});

The prompt always enters the orchestrator, which breaks the task into sub-tasks and dispatches each to the appropriate specialist. The caller writes one prompt; three specialised agents execute.

Giving Custom Agents Their Own MCP Servers

Custom agents can each have their own MCP server configuration, independent of what the session-level MCP servers provide. This is powerful for tight tool scoping: the researcher agent can read from a knowledge base, while the editor agent has access to a build system, and neither can use the other's tools:

new CustomAgentConfig
{
    Name = "azure-ops",
    Description = "Manages and queries Azure resources.",
    Prompt = "You are an Azure operator. Use Azure tools to answer questions and take actions.",
    McpServers = new Dictionary<string, McpServerConfig>
    {
        ["azure"] = new McpStdioServerConfig
        {
            Command = "npx",
            Args = ["-y", "@azure/mcp@latest", "server", "start"],
            Tools = ["azure_resource_group_list", "azure_storage_account_list"]
        }
    }
}

Observing sub-agent activity

Sub-agent lifecycle events flow through the same event system as the parent session. You can distinguish parent and sub-agent events by the AgentName property on the event data:

session.On<SubagentStartedEvent>(evt =>
    _logger.LogInformation(
        "[{Agent}] started: {Tool}",
        evt.Data.AgentDisplayName,
        evt.Data.ToolCallId));

session.On<SubagentCompletedEvent>(evt =>
    _logger.LogInformation(
        "[{Agent}]completed",
        evt.Data.AgentDisplayName));

If OpenTelemetry is configured, each sub-agent's work appears as a child span under the parent invoke_agent span, giving you a complete trace of delegation chains without additional instrumentation.

When to use custom agents vs. multiple sessions

Custom agents are the right tool when tasks share a conversation — when earlier context should inform later steps, and delegation happens within a single coherent workflow. Multiple independent sessions are the right tool when tasks are fully isolated — separate users, unrelated jobs, or workflows where shared context would be a liability rather than an asset.

A useful heuristic: if the agents need to read each other's outputs, use custom agents in one session. If they don't, use separate sessions.


What’s next

Although the support for custom agents in the GitHub Copilot SDK is a good starting point, the level of control and flexibility in a multi-agent setup is still limited compared to other frameworks like the Microsoft Agent Framework. The good news is that the GitHub Copilot SDK integrates nicely and that is exactly we’ll take a look at in our final post tomorrow. 

More information

Custom Agents

Popular posts from this blog

Podman– Command execution failed with exit code 125

After updating WSL on one of the developer machines, Podman failed to work. When we took a look through Podman Desktop, we noticed that Podman had stopped running and returned the following error message: Error: Command execution failed with exit code 125 Here are the steps we tried to fix the issue: We started by running podman info to get some extra details on what could be wrong: >podman info OS: windows/amd64 provider: wsl version: 5.3.1 Cannot connect to Podman. Please verify your connection to the Linux system using `podman system connection list`, or try `podman machine init` and `podman machine start` to manage a new Linux VM Error: unable to connect to Podman socket: failed to connect: dial tcp 127.0.0.1:2655: connectex: No connection could be made because the target machine actively refused it. That makes sense as the podman VM was not running. Let’s check the VM: >podman machine list NAME         ...

Azure DevOps/ GitHub emoji

I’m really bad at remembering emoji’s. So here is cheat sheet with all emoji’s that can be used in tools that support the github emoji markdown markup: All credits go to rcaviers who created this list.

VS Code Planning mode

After the introduction of Plan mode in Visual Studio , it now also found its way into VS Code. Planning mode, or as I like to call it 'Hannibal mode', extends GitHub Copilot's Agent Mode capabilities to handle larger, multi-step coding tasks with a structured approach. Instead of jumping straight into code generation, Planning mode creates a detailed execution plan. If you want more details, have a look at my previous post . Putting plan mode into action VS Code takes a different approach compared to Visual Studio when using plan mode. Instead of a configuration setting that you can activate but have limited control over, planning is available as a separate chat mode/agent: I like this approach better than how Visual Studio does it as you have explicit control when plan mode is activated. Instead of immediately diving into execution, the plan agent creates a plan and asks some follow up questions: You can further edit the plan by clicking on ‘Open in Editor’: ...