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.