## Why Parallel research track to the Claude Code plugins research already committed. Needed to understand GitHub Copilot's extensibility model before designing cross-tool plugin compatibility for the kyberforge plugin system. ## Implementation Notes Three distinct Copilot extension tracks are covered: CLI plugins (plugin.json + marketplaces), cloud/IDE custom agents (frontmatter .md files committed to repos), and the SDK programmatic API. The SDK track got its own topic file (sdk.md) because the content doesn't fit neatly into the default topic list. Sources include Context7 (/websites/github_en_copilot), both user-provided reference URLs, and four additional deepened pages. ## Impact Provides a reference baseline for evaluating .claude-plugin/ / plugin.json compatibility between Claude Code and Copilot CLI — the two formats share a manifest discovery path and the strict:false field enables cross-tool plugin distribution. --- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
6.5 KiB
topic, source_keys
| topic | source_keys | ||
|---|---|---|---|
| sdk |
|
Copilot SDK — Custom Agents
The Copilot SDK is the programmatic interface for building Copilot-powered applications. Custom agents in the SDK context are runtime configurations attached to a session rather than files on disk.
Core Concepts
- Custom agent — a named configuration with its own system prompt, restricted tool set, and optional MCP servers.
- Sub-agent — a custom agent invoked by the Copilot runtime to handle a portion of a task in an isolated context window.
- Inference — the runtime's automatic selection of an agent based on user intent. Enabled by default (
infer: true). Setinfer: falseon agents that should only run when explicitly requested (especially destructive agents).
Session Creation
Pass customAgents to client.createSession() along with a permission handler. Permission handling is session-wide — there is no per-agent override.
import { CopilotClient } from "@github/copilot-sdk";
const client = new CopilotClient();
await client.start();
const session = await client.createSession({
model: "gpt-4.1",
customAgents: [
{
name: "researcher",
displayName: "Research Agent",
description: "Read-only codebase exploration",
tools: ["grep", "glob", "view"],
prompt: "You are a research assistant. Do not modify files.",
},
{
name: "editor",
displayName: "Editor Agent",
description: "Makes targeted code changes",
tools: ["view", "edit", "bash"],
prompt: "You are a code editor. Make minimal changes.",
},
],
onPermissionRequest: async () => ({ kind: "approve-once" }),
});
CustomAgentConfig Fields
| Field | Required | Default | Notes |
|---|---|---|---|
name |
Yes | — | Identifier; must match agent pre-selection exactly |
displayName |
No | — | Label shown in lifecycle events |
description |
No | — | Used for runtime intent matching; quality directly affects routing accuracy |
tools |
No | all session tools | null or omitted = all; explicit list = restrict |
prompt |
Yes | — | System prompt injected into this agent's context |
mcpServers |
No | — | Agent-scoped MCP config; not inherited from session; not visible to other agents |
infer |
No | true |
Set false for agents that should not be auto-selected |
skills |
No | — | Skill names from skillDirectories; eagerly injected at startup; not inherited by sub-agents |
Session-Level Fields Relevant to Agents
| Field | Type | Notes |
|---|---|---|
customAgents |
array | Agent definitions |
agent |
string | Pre-activate a named agent; exact name match required |
skillDirectories |
string[] | Directories for resolving skills names |
defaultAgent.excludedTools |
string[] | Hides tools from main agent; sub-agents still see them |
model |
string | Default model for all agents |
Tool Scoping
Two orthogonal mechanisms control tool access:
Per-agent tools list — restricts what that agent can call. null or omitting the field means all session tools are available.
defaultAgent.excludedTools — hides tools from the built-in (main/orchestrator) agent while keeping them available to named sub-agents. Use this to prevent the orchestrator from using heavy-context or destructive tools directly.
Precedence: session-level excludedTools beats defaultAgent.excludedTools. If a tool appears in both, no agent gets it.
Permission Handling
// TypeScript
onPermissionRequest: async () => ({ kind: "approve-once" })
// Python
on_permission_request=lambda req, inv: PermissionDecisionApproveOnce()
// Go
OnPermissionRequest: func(req, inv) (rpc.PermissionDecision, error) {
return &rpc.PermissionDecisionApproveOnce{}, nil
},
// .NET
OnPermissionRequest = (req, inv) => Task.FromResult(PermissionDecision.ApproveOnce())
// Java
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
Sub-Agent Lifecycle Events
Subscribe to all session events via session.on(handler). Filter by event.type:
| Event Type | Key Fields |
|---|---|
subagent.selected |
agentName, agentDisplayName, tools |
subagent.started |
toolCallId, agentName, agentDisplayName, agentDescription |
subagent.completed |
toolCallId, agentName, agentDisplayName |
subagent.failed |
toolCallId, agentName, agentDisplayName, error |
subagent.deselected |
(no data) |
toolCallId is stable across started/completed/failed for a single invocation — key an agent-tree map on it.
const agentTree = new Map();
session.on((event) => {
if (event.type === "subagent.started") {
agentTree.set(event.toolCallId, {
name: event.agentName,
status: "running",
startedAt: Date.now(),
});
}
if (event.type === "subagent.completed") {
const node = agentTree.get(event.toolCallId);
if (node) node.status = "done";
}
if (event.type === "subagent.failed") {
const node = agentTree.get(event.toolCallId);
if (node) { node.status = "failed"; node.error = event.error; }
}
});
Language-Specific Notes
TypeScript: camelCase field names (customAgents, displayName, onPermissionRequest).
Python: snake_case (custom_agents, display_name, on_permission_request).
Go: PascalCase struct fields (CustomAgents, DisplayName); copilot.CustomAgentConfig struct.
.NET: SessionConfig object initializer with CustomAgents = new List<CustomAgentConfig> { new() { ... } }.
Java: builder pattern — new CustomAgentConfig().setName(...).setTools(List.of(...)).setPrompt(...).
Key Gotchas
- Description quality directly affects routing. Vague descriptions produce bad inference. Be specific about what the agent does and what it will not do.
infer: trueis the default. Explicitly setinfer: falseon agents with destructive capability (file deletion, schema migrations) so the runtime cannot select them autonomously.- Sub-agents do not inherit parent skills. Each agent must declare its own
skillslist. - Agent-scoped MCP is not shared. An MCP server in a sub-agent's
mcpServersblock is invisible to the main agent and other sub-agents. subagent.failedrequires explicit handling. The runtime does not retry or fall back automatically.agentpre-selection is an exact match. A mismatch silently falls through to inference.