|
| 1 | +--- |
| 2 | +title: Custom agents and sub-agent orchestration |
| 3 | +shortTitle: Custom agents |
| 4 | +intro: Define specialized agents with scoped tools and prompts, and let {% data variables.product.prodname_copilot_short %} orchestrate them as sub-agents within a single session. |
| 5 | +product: '{% data reusables.gated-features.copilot-sdk %}' |
| 6 | +versions: |
| 7 | + feature: copilot |
| 8 | +contentType: how-tos |
| 9 | +category: |
| 10 | + - Author and optimize with Copilot |
| 11 | +--- |
| 12 | + |
| 13 | +{% data reusables.copilot.copilot-sdk.technical-preview-note %} |
| 14 | + |
| 15 | +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 {% data variables.copilot.copilot_sdk_short %} 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. For a visual overview of the delegation flow, see the [`github/copilot-sdk` repository](https://github.com/github/copilot-sdk/blob/main/docs/features/custom-agents.md#overview). |
| 16 | + |
| 17 | +| Concept | Description | |
| 18 | +|---------|-------------| |
| 19 | +| **Custom agent** | A named agent config with its own prompt and tool set | |
| 20 | +| **Sub-agent** | A custom agent invoked by the runtime to handle part of a task | |
| 21 | +| **Inference** | The runtime's ability to auto-select an agent based on the user's intent | |
| 22 | +| **Parent session** | The session that spawned the sub-agent; receives all lifecycle events | |
| 23 | + |
| 24 | +## Defining custom agents |
| 25 | + |
| 26 | +Pass `customAgents` when creating a session. At minimum, each agent needs a `name` and `prompt`. |
| 27 | + |
| 28 | +```typescript |
| 29 | +import { CopilotClient } from "@github/copilot-sdk"; |
| 30 | + |
| 31 | +const client = new CopilotClient(); |
| 32 | +await client.start(); |
| 33 | + |
| 34 | +const session = await client.createSession({ |
| 35 | + model: "gpt-4.1", |
| 36 | + customAgents: [ |
| 37 | + { |
| 38 | + name: "researcher", |
| 39 | + displayName: "Research Agent", |
| 40 | + description: "Explores codebases and answers questions using read-only tools", |
| 41 | + tools: ["grep", "glob", "view"], |
| 42 | + prompt: "You are a research assistant. Analyze code and answer questions. Do not modify any files.", |
| 43 | + }, |
| 44 | + { |
| 45 | + name: "editor", |
| 46 | + displayName: "Editor Agent", |
| 47 | + description: "Makes targeted code changes", |
| 48 | + tools: ["view", "edit", "bash"], |
| 49 | + prompt: "You are a code editor. Make minimal, surgical changes to files as requested.", |
| 50 | + }, |
| 51 | + ], |
| 52 | + onPermissionRequest: async () => ({ kind: "approved" }), |
| 53 | +}); |
| 54 | +``` |
| 55 | + |
| 56 | +For examples in Python, Go, and .NET, see the [`github/copilot-sdk` repository](https://github.com/github/copilot-sdk/blob/main/docs/features/custom-agents.md#defining-custom-agents). |
| 57 | + |
| 58 | +## Configuration reference |
| 59 | + |
| 60 | +| Property | Type | Required | Description | |
| 61 | +|----------|------|----------|-------------| |
| 62 | +| `name` | `string` | ✅ | Unique identifier for the agent | |
| 63 | +| `displayName` | `string` | | Human-readable name shown in events | |
| 64 | +| `description` | `string` | | What the agent does—helps the runtime select it | |
| 65 | +| `tools` | `string[]` or `null` | | Names of tools the agent can use. `null` or omitted = all tools | |
| 66 | +| `prompt` | `string` | ✅ | System prompt for the agent | |
| 67 | +| `mcpServers` | `object` | | MCP server configurations specific to this agent | |
| 68 | +| `infer` | `boolean` | | Whether the runtime can auto-select this agent (default: `true`) | |
| 69 | + |
| 70 | +> [!TIP] |
| 71 | +> A good `description` helps the runtime match user intent to the right agent. Be specific about the agent's expertise and capabilities. |
| 72 | +
|
| 73 | +In addition to per-agent configuration, you can set `agent` on the **session config** to pre-select which custom agent is active when the session starts. |
| 74 | + |
| 75 | +| Session config property | Type | Description | |
| 76 | +|-------------------------|------|-------------| |
| 77 | +| `agent` | `string` | Name of the custom agent to pre-select at session creation. Must match a `name` in `customAgents`. | |
| 78 | + |
| 79 | +## Selecting an agent at session creation |
| 80 | + |
| 81 | +You can pass `agent` in the session config to pre-select which custom agent should be active when the session starts. The value must match the `name` of one of the agents defined in `customAgents`. |
| 82 | + |
| 83 | +```typescript |
| 84 | +const session = await client.createSession({ |
| 85 | + customAgents: [ |
| 86 | + { |
| 87 | + name: "researcher", |
| 88 | + prompt: "You are a research assistant. Analyze code and answer questions.", |
| 89 | + }, |
| 90 | + { |
| 91 | + name: "editor", |
| 92 | + prompt: "You are a code editor. Make minimal, surgical changes.", |
| 93 | + }, |
| 94 | + ], |
| 95 | + agent: "researcher", // Pre-select the researcher agent |
| 96 | +}); |
| 97 | +``` |
| 98 | + |
| 99 | +For examples in Python, Go, and .NET, see the [`github/copilot-sdk` repository](https://github.com/github/copilot-sdk/blob/main/docs/features/custom-agents.md#selecting-an-agent-at-session-creation). |
| 100 | + |
| 101 | +## How sub-agent delegation works |
| 102 | + |
| 103 | +When you send a prompt to a session with custom agents, the runtime evaluates whether to delegate to a sub-agent: |
| 104 | + |
| 105 | +1. **Intent matching**—The runtime analyzes the user's prompt against each agent's `name` and `description` |
| 106 | +1. **Agent selection**—If a match is found and `infer` is not `false`, the runtime selects the agent |
| 107 | +1. **Isolated execution**—The sub-agent runs with its own prompt and restricted tool set |
| 108 | +1. **Event streaming**—Lifecycle events (`subagent.started`, `subagent.completed`, etc.) stream back to the parent session |
| 109 | +1. **Result integration**—The sub-agent's output is incorporated into the parent agent's response |
| 110 | + |
| 111 | +### Controlling inference |
| 112 | + |
| 113 | +By default, all custom agents are available for automatic selection (`infer: true`). Set `infer: false` to prevent the runtime from auto-selecting an agent—useful for agents you only want invoked through explicit user requests: |
| 114 | + |
| 115 | +```typescript |
| 116 | +{ |
| 117 | + name: "dangerous-cleanup", |
| 118 | + description: "Deletes unused files and dead code", |
| 119 | + tools: ["bash", "edit", "view"], |
| 120 | + prompt: "You clean up codebases by removing dead code and unused files.", |
| 121 | + infer: false, // Only invoked when user explicitly asks for this agent |
| 122 | +} |
| 123 | +``` |
| 124 | + |
| 125 | +## Listening to sub-agent events |
| 126 | + |
| 127 | +When a sub-agent runs, the parent session emits lifecycle events. Subscribe to these events to build UIs that visualize agent activity. |
| 128 | + |
| 129 | +### Event types |
| 130 | + |
| 131 | +| Event | Emitted when | Data | |
| 132 | +|-------|-------------|------| |
| 133 | +| `subagent.selected` | Runtime selects an agent for the task | `agentName`, `agentDisplayName`, `tools` | |
| 134 | +| `subagent.started` | Sub-agent begins execution | `toolCallId`, `agentName`, `agentDisplayName`, `agentDescription` | |
| 135 | +| `subagent.completed` | Sub-agent finishes successfully | `toolCallId`, `agentName`, `agentDisplayName` | |
| 136 | +| `subagent.failed` | Sub-agent encounters an error | `toolCallId`, `agentName`, `agentDisplayName`, `error` | |
| 137 | +| `subagent.deselected` | Runtime switches away from the sub-agent | — | |
| 138 | + |
| 139 | +### Subscribing to events |
| 140 | + |
| 141 | +```typescript |
| 142 | +session.on((event) => { |
| 143 | + switch (event.type) { |
| 144 | + case "subagent.started": |
| 145 | + console.log(`▶ Sub-agent started: ${event.data.agentDisplayName}`); |
| 146 | + console.log(` Description: ${event.data.agentDescription}`); |
| 147 | + console.log(` Tool call ID: ${event.data.toolCallId}`); |
| 148 | + break; |
| 149 | + |
| 150 | + case "subagent.completed": |
| 151 | + console.log(`✅ Sub-agent completed: ${event.data.agentDisplayName}`); |
| 152 | + break; |
| 153 | + |
| 154 | + case "subagent.failed": |
| 155 | + console.log(`❌ Sub-agent failed: ${event.data.agentDisplayName}`); |
| 156 | + console.log(` Error: ${event.data.error}`); |
| 157 | + break; |
| 158 | + |
| 159 | + case "subagent.selected": |
| 160 | + console.log(`🎯 Agent selected: ${event.data.agentDisplayName}`); |
| 161 | + console.log(` Tools: ${event.data.tools?.join(", ") ?? "all"}`); |
| 162 | + break; |
| 163 | + |
| 164 | + case "subagent.deselected": |
| 165 | + console.log("↩ Agent deselected, returning to parent"); |
| 166 | + break; |
| 167 | + } |
| 168 | +}); |
| 169 | + |
| 170 | +const response = await session.sendAndWait({ |
| 171 | + prompt: "Research how authentication works in this codebase", |
| 172 | +}); |
| 173 | +``` |
| 174 | + |
| 175 | +For examples in Python, Go, and .NET, see the [`github/copilot-sdk` repository](https://github.com/github/copilot-sdk/blob/main/docs/features/custom-agents.md#listening-to-sub-agent-events). |
| 176 | + |
| 177 | +## Building an agent tree UI |
| 178 | + |
| 179 | +Sub-agent events include `toolCallId` fields that let you reconstruct the execution tree. Here's a pattern for tracking agent activity: |
| 180 | + |
| 181 | +```typescript |
| 182 | +interface AgentNode { |
| 183 | + toolCallId: string; |
| 184 | + name: string; |
| 185 | + displayName: string; |
| 186 | + status: "running" | "completed" | "failed"; |
| 187 | + error?: string; |
| 188 | + startedAt: Date; |
| 189 | + completedAt?: Date; |
| 190 | +} |
| 191 | + |
| 192 | +const agentTree = new Map<string, AgentNode>(); |
| 193 | + |
| 194 | +session.on((event) => { |
| 195 | + if (event.type === "subagent.started") { |
| 196 | + agentTree.set(event.data.toolCallId, { |
| 197 | + toolCallId: event.data.toolCallId, |
| 198 | + name: event.data.agentName, |
| 199 | + displayName: event.data.agentDisplayName, |
| 200 | + status: "running", |
| 201 | + startedAt: new Date(event.timestamp), |
| 202 | + }); |
| 203 | + } |
| 204 | + |
| 205 | + if (event.type === "subagent.completed") { |
| 206 | + const node = agentTree.get(event.data.toolCallId); |
| 207 | + if (node) { |
| 208 | + node.status = "completed"; |
| 209 | + node.completedAt = new Date(event.timestamp); |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + if (event.type === "subagent.failed") { |
| 214 | + const node = agentTree.get(event.data.toolCallId); |
| 215 | + if (node) { |
| 216 | + node.status = "failed"; |
| 217 | + node.error = event.data.error; |
| 218 | + node.completedAt = new Date(event.timestamp); |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | + // Render your UI with the updated tree |
| 223 | + renderAgentTree(agentTree); |
| 224 | +}); |
| 225 | +``` |
| 226 | + |
| 227 | +## Scoping tools per agent |
| 228 | + |
| 229 | +Use the `tools` property to restrict which tools an agent can access. This is essential for security and for keeping agents focused: |
| 230 | + |
| 231 | +```typescript |
| 232 | +const session = await client.createSession({ |
| 233 | + customAgents: [ |
| 234 | + { |
| 235 | + name: "reader", |
| 236 | + description: "Read-only exploration of the codebase", |
| 237 | + tools: ["grep", "glob", "view"], // No write access |
| 238 | + prompt: "You explore and analyze code. Never suggest modifications directly.", |
| 239 | + }, |
| 240 | + { |
| 241 | + name: "writer", |
| 242 | + description: "Makes code changes", |
| 243 | + tools: ["view", "edit", "bash"], // Write access |
| 244 | + prompt: "You make precise code changes as instructed.", |
| 245 | + }, |
| 246 | + { |
| 247 | + name: "unrestricted", |
| 248 | + description: "Full access agent for complex tasks", |
| 249 | + tools: null, // All tools available |
| 250 | + prompt: "You handle complex multi-step tasks using any available tools.", |
| 251 | + }, |
| 252 | + ], |
| 253 | +}); |
| 254 | +``` |
| 255 | + |
| 256 | +> [!NOTE] |
| 257 | +> When `tools` is `null` or omitted, the agent inherits access to all tools configured on the session. Use explicit tool lists to enforce the principle of least privilege. |
| 258 | +
|
| 259 | +## Attaching MCP servers to agents |
| 260 | + |
| 261 | +Each custom agent can have its own MCP (Model Context Protocol) servers, giving it access to specialized data sources: |
| 262 | + |
| 263 | +```typescript |
| 264 | +const session = await client.createSession({ |
| 265 | + customAgents: [ |
| 266 | + { |
| 267 | + name: "db-analyst", |
| 268 | + description: "Analyzes database schemas and queries", |
| 269 | + prompt: "You are a database expert. Use the database MCP server to analyze schemas.", |
| 270 | + mcpServers: { |
| 271 | + "database": { |
| 272 | + command: "npx", |
| 273 | + args: ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"], |
| 274 | + }, |
| 275 | + }, |
| 276 | + }, |
| 277 | + ], |
| 278 | +}); |
| 279 | +``` |
| 280 | + |
| 281 | +## Patterns and best practices |
| 282 | + |
| 283 | +### Pair a researcher with an editor |
| 284 | + |
| 285 | +A common pattern is to define a read-only researcher agent and a write-capable editor agent. The runtime delegates exploration tasks to the researcher and modification tasks to the editor: |
| 286 | + |
| 287 | +```typescript |
| 288 | +customAgents: [ |
| 289 | + { |
| 290 | + name: "researcher", |
| 291 | + description: "Analyzes code structure, finds patterns, and answers questions", |
| 292 | + tools: ["grep", "glob", "view"], |
| 293 | + prompt: "You are a code analyst. Thoroughly explore the codebase to answer questions.", |
| 294 | + }, |
| 295 | + { |
| 296 | + name: "implementer", |
| 297 | + description: "Implements code changes based on analysis", |
| 298 | + tools: ["view", "edit", "bash"], |
| 299 | + prompt: "You make minimal, targeted code changes. Always verify changes compile.", |
| 300 | + }, |
| 301 | +] |
| 302 | +``` |
| 303 | + |
| 304 | +### Keep agent descriptions specific |
| 305 | + |
| 306 | +The runtime uses the `description` to match user intent. Vague descriptions lead to poor delegation: |
| 307 | + |
| 308 | +```typescript |
| 309 | +// ❌ Too vague — runtime can't distinguish from other agents |
| 310 | +{ description: "Helps with code" } |
| 311 | + |
| 312 | +// ✅ Specific — runtime knows when to delegate |
| 313 | +{ description: "Analyzes Python test coverage and identifies untested code paths" } |
| 314 | +``` |
| 315 | + |
| 316 | +### Handle failures gracefully |
| 317 | + |
| 318 | +Sub-agents can fail. Always listen for `subagent.failed` events and handle them in your application: |
| 319 | + |
| 320 | +```typescript |
| 321 | +session.on((event) => { |
| 322 | + if (event.type === "subagent.failed") { |
| 323 | + logger.error(`Agent ${event.data.agentName} failed: ${event.data.error}`); |
| 324 | + // Show error in UI, retry, or fall back to parent agent |
| 325 | + } |
| 326 | +}); |
| 327 | +``` |
0 commit comments