diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md
index f3c508922..0d27fe873 100644
--- a/docs/features/custom-agents.md
+++ b/docs/features/custom-agents.md
@@ -759,6 +759,154 @@ const session = await client.createSession({
> **Note:** 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.
+## Agent-Exclusive Tools
+
+Use the `defaultAgent` property on the session configuration to hide specific tools from the default agent (the built-in agent that handles turns when no custom agent is selected). This forces the main agent to delegate to sub-agents when those tools' capabilities are needed, keeping the main agent's context clean.
+
+This is useful when:
+- Certain tools generate large amounts of context that would overwhelm the main agent
+- You want the main agent to act as an orchestrator, delegating heavy work to specialized sub-agents
+- You need strict separation between orchestration and execution
+
+
+Node.js / TypeScript
+
+```typescript
+import { CopilotClient, defineTool, approveAll } from "@github/copilot-sdk";
+import { z } from "zod";
+
+const heavyContextTool = defineTool("analyze-codebase", {
+ description: "Performs deep analysis of the codebase, generating extensive context",
+ parameters: z.object({ query: z.string() }),
+ handler: async ({ query }) => {
+ // ... expensive analysis that returns lots of data
+ return { analysis: "..." };
+ },
+});
+
+const session = await client.createSession({
+ tools: [heavyContextTool],
+ defaultAgent: {
+ excludedTools: ["analyze-codebase"],
+ },
+ customAgents: [
+ {
+ name: "researcher",
+ description: "Deep codebase analysis agent with access to heavy-context tools",
+ tools: ["analyze-codebase"],
+ prompt: "You perform thorough codebase analysis using the analyze-codebase tool.",
+ },
+ ],
+});
+```
+
+
+
+
+Python
+
+```python
+from copilot import CopilotClient
+from copilot.tools import Tool
+
+heavy_tool = Tool(
+ name="analyze-codebase",
+ description="Performs deep analysis of the codebase",
+ handler=analyze_handler,
+ parameters={"type": "object", "properties": {"query": {"type": "string"}}},
+)
+
+session = await client.create_session(
+ tools=[heavy_tool],
+ default_agent={"excluded_tools": ["analyze-codebase"]},
+ custom_agents=[
+ {
+ "name": "researcher",
+ "description": "Deep codebase analysis agent",
+ "tools": ["analyze-codebase"],
+ "prompt": "You perform thorough codebase analysis.",
+ },
+ ],
+ on_permission_request=approve_all,
+)
+```
+
+
+
+
+Go
+
+
+```go
+session, err := client.CreateSession(ctx, &copilot.SessionConfig{
+ Tools: []copilot.Tool{heavyTool},
+ DefaultAgent: &copilot.DefaultAgentConfig{
+ ExcludedTools: []string{"analyze-codebase"},
+ },
+ CustomAgents: []copilot.CustomAgentConfig{
+ {
+ Name: "researcher",
+ Description: "Deep codebase analysis agent",
+ Tools: []string{"analyze-codebase"},
+ Prompt: "You perform thorough codebase analysis.",
+ },
+ },
+})
+```
+
+
+
+
+C# / .NET
+
+
+```csharp
+var session = await client.CreateSessionAsync(new SessionConfig
+{
+ Tools = [analyzeCodebaseTool],
+ DefaultAgent = new DefaultAgentConfig
+ {
+ ExcludedTools = ["analyze-codebase"],
+ },
+ CustomAgents =
+ [
+ new CustomAgentConfig
+ {
+ Name = "researcher",
+ Description = "Deep codebase analysis agent",
+ Tools = ["analyze-codebase"],
+ Prompt = "You perform thorough codebase analysis.",
+ },
+ ],
+});
+```
+
+
+
+### How It Works
+
+Tools listed in `defaultAgent.excludedTools`:
+
+1. **Are registered** — their handlers are available for execution
+2. **Are hidden** from the main agent's tool list — the LLM won't see or call them directly
+3. **Remain available** to any custom sub-agent that includes them in its `tools` array
+
+### Interaction with Other Tool Filters
+
+`defaultAgent.excludedTools` is orthogonal to the session-level `availableTools` and `excludedTools`:
+
+| Filter | Scope | Effect |
+|--------|-------|--------|
+| `availableTools` | Session-wide | Allowlist — only these tools exist for anyone |
+| `excludedTools` | Session-wide | Blocklist — these tools are blocked for everyone |
+| `defaultAgent.excludedTools` | Main agent only | These tools are hidden from the main agent but available to sub-agents |
+
+Precedence:
+1. Session-level `availableTools`/`excludedTools` are applied first (globally)
+2. `defaultAgent.excludedTools` is applied on top, further restricting the main agent only
+
+> **Note:** If a tool is in both `excludedTools` (session-level) and `defaultAgent.excludedTools`, the session-level exclusion takes precedence — the tool is unavailable to everyone.
+
## Attaching MCP Servers to Agents
Each custom agent can have its own MCP (Model Context Protocol) servers, giving it access to specialized data sources:
diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs
index 668d090f5..3941abbec 100644
--- a/dotnet/src/Client.cs
+++ b/dotnet/src/Client.cs
@@ -501,6 +501,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance
config.McpServers,
"direct",
config.CustomAgents,
+ config.DefaultAgent,
config.Agent,
config.ConfigDir,
config.EnableConfigDiscovery,
@@ -627,6 +628,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes
config.McpServers,
"direct",
config.CustomAgents,
+ config.DefaultAgent,
config.Agent,
config.SkillDirectories,
config.DisabledSkills,
@@ -1642,6 +1644,7 @@ internal record CreateSessionRequest(
IDictionary? McpServers,
string? EnvValueMode,
IList? CustomAgents,
+ DefaultAgentConfig? DefaultAgent,
string? Agent,
string? ConfigDir,
bool? EnableConfigDiscovery,
@@ -1698,6 +1701,7 @@ internal record ResumeSessionRequest(
IDictionary? McpServers,
string? EnvValueMode,
IList? CustomAgents,
+ DefaultAgentConfig? DefaultAgent,
string? Agent,
IList? SkillDirectories,
IList? DisabledSkills,
diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs
index fd42d0c27..131362055 100644
--- a/dotnet/src/Types.cs
+++ b/dotnet/src/Types.cs
@@ -1656,6 +1656,21 @@ public class CustomAgentConfig
public IList? Skills { get; set; }
}
+///
+/// Configuration for the default agent (the built-in agent that handles turns when no custom agent is selected).
+/// Use to hide specific tools from the default agent
+/// while keeping them available to custom sub-agents.
+///
+public class DefaultAgentConfig
+{
+ ///
+ /// List of tool names to exclude from the default agent.
+ /// These tools remain available to custom sub-agents that reference them
+ /// in their list.
+ ///
+ public IList? ExcludedTools { get; set; }
+}
+
///
/// Configuration for infinite sessions with automatic context compaction and workspace persistence.
/// When enabled, sessions automatically manage context window limits through background compaction
@@ -1709,6 +1724,7 @@ protected SessionConfig(SessionConfig? other)
Commands = other.Commands is not null ? [.. other.Commands] : null;
ConfigDir = other.ConfigDir;
CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null;
+ DefaultAgent = other.DefaultAgent;
Agent = other.Agent;
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
EnableConfigDiscovery = other.EnableConfigDiscovery;
@@ -1871,6 +1887,13 @@ protected SessionConfig(SessionConfig? other)
///
public IList? CustomAgents { get; set; }
+ ///
+ /// Configuration for the default agent (the built-in agent that handles turns when no custom agent is selected).
+ /// Use to hide specific tools from the default agent
+ /// while keeping them available to custom sub-agents.
+ ///
+ public DefaultAgentConfig? DefaultAgent { get; set; }
+
///
/// Name of the custom agent to activate when the session starts.
/// Must match the of one of the agents in .
@@ -1950,6 +1973,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
Commands = other.Commands is not null ? [.. other.Commands] : null;
ConfigDir = other.ConfigDir;
CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null;
+ DefaultAgent = other.DefaultAgent;
Agent = other.Agent;
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
DisableResume = other.DisableResume;
@@ -2117,6 +2141,13 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
///
public IList? CustomAgents { get; set; }
+ ///
+ /// Configuration for the default agent (the built-in agent that handles turns when no custom agent is selected).
+ /// Use to hide specific tools from the default agent
+ /// while keeping them available to custom sub-agents.
+ ///
+ public DefaultAgentConfig? DefaultAgent { get; set; }
+
///
/// Name of the custom agent to activate when the session starts.
/// Must match the of one of the agents in .
diff --git a/dotnet/test/CloneTests.cs b/dotnet/test/CloneTests.cs
index cc36162ff..5c326dcc4 100644
--- a/dotnet/test/CloneTests.cs
+++ b/dotnet/test/CloneTests.cs
@@ -90,6 +90,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
McpServers = new Dictionary { ["server1"] = new McpStdioServerConfig { Command = "echo" } },
CustomAgents = [new CustomAgentConfig { Name = "agent1" }],
Agent = "agent1",
+ DefaultAgent = new DefaultAgentConfig { ExcludedTools = ["hidden-tool"] },
SkillDirectories = ["/skills"],
DisabledSkills = ["skill1"],
};
@@ -109,6 +110,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
Assert.Equal(original.McpServers.Count, clone.McpServers!.Count);
Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count);
Assert.Equal(original.Agent, clone.Agent);
+ Assert.Equal(original.DefaultAgent!.ExcludedTools, clone.DefaultAgent!.ExcludedTools);
Assert.Equal(original.SkillDirectories, clone.SkillDirectories);
Assert.Equal(original.DisabledSkills, clone.DisabledSkills);
}
@@ -245,6 +247,7 @@ public void Clone_WithNullCollections_ReturnsNullCollections()
Assert.Null(clone.SkillDirectories);
Assert.Null(clone.DisabledSkills);
Assert.Null(clone.Tools);
+ Assert.Null(clone.DefaultAgent);
Assert.True(clone.IncludeSubAgentStreamingEvents);
}
diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs
index 59c11a84f..241698516 100644
--- a/dotnet/test/SessionTests.cs
+++ b/dotnet/test/SessionTests.cs
@@ -162,6 +162,35 @@ public async Task Should_Create_A_Session_With_ExcludedTools()
Assert.Contains("grep", toolNames);
}
+ [Fact]
+ public async Task Should_Create_A_Session_With_DefaultAgent_ExcludedTools()
+ {
+ var session = await CreateSessionAsync(new SessionConfig
+ {
+ Tools =
+ [
+ AIFunctionFactory.Create(
+ (string input) => "SECRET",
+ "secret_tool",
+ "A secret tool hidden from the default agent"),
+ ],
+ DefaultAgent = new DefaultAgentConfig
+ {
+ ExcludedTools = ["secret_tool"],
+ },
+ });
+
+ await session.SendAsync(new MessageOptions { Prompt = "What is 1+1?" });
+ await TestHelper.GetFinalAssistantMessageAsync(session);
+
+ // The real assertion: verify the runtime excluded the tool from the CAPI request
+ var traffic = await Ctx.GetExchangesAsync();
+ Assert.NotEmpty(traffic);
+
+ var toolNames = GetToolNames(traffic[0]);
+ Assert.DoesNotContain("secret_tool", toolNames);
+ }
+
[Fact]
public async Task Should_Create_Session_With_Custom_Tool()
{
diff --git a/go/client.go b/go/client.go
index 37e572dc8..74e4839be 100644
--- a/go/client.go
+++ b/go/client.go
@@ -592,6 +592,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
req.MCPServers = config.MCPServers
req.EnvValueMode = "direct"
req.CustomAgents = config.CustomAgents
+ req.DefaultAgent = config.DefaultAgent
req.Agent = config.Agent
req.SkillDirectories = config.SkillDirectories
req.DisabledSkills = config.DisabledSkills
@@ -776,6 +777,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
req.MCPServers = config.MCPServers
req.EnvValueMode = "direct"
req.CustomAgents = config.CustomAgents
+ req.DefaultAgent = config.DefaultAgent
req.Agent = config.Agent
req.SkillDirectories = config.SkillDirectories
req.DisabledSkills = config.DisabledSkills
diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go
index 1fed130d3..96ab7a908 100644
--- a/go/internal/e2e/session_test.go
+++ b/go/internal/e2e/session_test.go
@@ -313,6 +313,57 @@ func TestSession(t *testing.T) {
}
})
+ t.Run("should create a session with defaultAgent excludedTools", func(t *testing.T) {
+ ctx.ConfigureForTest(t)
+
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ Tools: []copilot.Tool{
+ {
+ Name: "secret_tool",
+ Description: "A secret tool hidden from the default agent",
+ Parameters: map[string]any{
+ "type": "object",
+ "properties": map[string]any{"input": map[string]any{"type": "string"}},
+ },
+ Handler: func(invocation copilot.ToolInvocation) (copilot.ToolResult, error) {
+ return copilot.ToolResult{TextResultForLLM: "SECRET", ResultType: "success"}, nil
+ },
+ },
+ },
+ DefaultAgent: &copilot.DefaultAgentConfig{
+ ExcludedTools: []string{"secret_tool"},
+ },
+ })
+ if err != nil {
+ t.Fatalf("Failed to create session: %v", err)
+ }
+
+ _, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "What is 1+1?"})
+ if err != nil {
+ t.Fatalf("Failed to send message: %v", err)
+ }
+
+ _, err = testharness.GetFinalAssistantMessage(t.Context(), session)
+ if err != nil {
+ t.Fatalf("Failed to get assistant message: %v", err)
+ }
+
+ // The real assertion: verify the runtime excluded the tool from the CAPI request
+ traffic, err := ctx.GetExchanges()
+ if err != nil {
+ t.Fatalf("Failed to get exchanges: %v", err)
+ }
+ if len(traffic) == 0 {
+ t.Fatal("Expected at least one exchange")
+ }
+
+ toolNames := getToolNames(traffic[0])
+ if contains(toolNames, "secret_tool") {
+ t.Errorf("Expected 'secret_tool' to be excluded from default agent, got %v", toolNames)
+ }
+ })
+
t.Run("should create session with custom tool", func(t *testing.T) {
ctx.ConfigureForTest(t)
diff --git a/go/types.go b/go/types.go
index aa4fafc94..e9f78e276 100644
--- a/go/types.go
+++ b/go/types.go
@@ -454,6 +454,15 @@ type CustomAgentConfig struct {
Skills []string `json:"skills,omitempty"`
}
+// DefaultAgentConfig configures the default agent (the built-in agent that handles turns when no custom agent is selected).
+// Use ExcludedTools to hide specific tools from the default agent while keeping
+// them available to custom sub-agents.
+type DefaultAgentConfig struct {
+ // ExcludedTools is a list of tool names to exclude from the default agent.
+ // These tools remain available to custom sub-agents that reference them in their Tools list.
+ ExcludedTools []string `json:"excludedTools,omitempty"`
+}
+
// InfiniteSessionConfig configures infinite sessions with automatic context compaction
// and workspace persistence. When enabled, sessions automatically manage context window
// limits through background compaction and persist state to a workspace directory.
@@ -543,6 +552,9 @@ type SessionConfig struct {
MCPServers map[string]MCPServerConfig
// CustomAgents configures custom agents for the session
CustomAgents []CustomAgentConfig
+ // DefaultAgent configures the default agent (the built-in agent that handles turns when no custom agent is selected).
+ // Use ExcludedTools to hide tools from the default agent while keeping them available to sub-agents.
+ DefaultAgent *DefaultAgentConfig
// Agent is the name of the custom agent to activate when the session starts.
// Must match the Name of one of the agents in CustomAgents.
Agent string
@@ -758,6 +770,8 @@ type ResumeSessionConfig struct {
MCPServers map[string]MCPServerConfig
// CustomAgents configures custom agents for the session
CustomAgents []CustomAgentConfig
+ // DefaultAgent configures the default agent (the built-in agent that handles turns when no custom agent is selected).
+ DefaultAgent *DefaultAgentConfig
// Agent is the name of the custom agent to activate when the session starts.
// Must match the Name of one of the agents in CustomAgents.
Agent string
@@ -970,6 +984,7 @@ type createSessionRequest struct {
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
EnvValueMode string `json:"envValueMode,omitempty"`
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
+ DefaultAgent *DefaultAgentConfig `json:"defaultAgent,omitempty"`
Agent string `json:"agent,omitempty"`
ConfigDir string `json:"configDir,omitempty"`
EnableConfigDiscovery *bool `json:"enableConfigDiscovery,omitempty"`
@@ -1019,6 +1034,7 @@ type resumeSessionRequest struct {
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
EnvValueMode string `json:"envValueMode,omitempty"`
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
+ DefaultAgent *DefaultAgentConfig `json:"defaultAgent,omitempty"`
Agent string `json:"agent,omitempty"`
SkillDirectories []string `json:"skillDirectories,omitempty"`
DisabledSkills []string `json:"disabledSkills,omitempty"`
diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts
index c8c137c3d..f4aa1e44f 100644
--- a/nodejs/src/client.ts
+++ b/nodejs/src/client.ts
@@ -752,6 +752,7 @@ export class CopilotClient {
mcpServers: config.mcpServers,
envValueMode: "direct",
customAgents: config.customAgents,
+ defaultAgent: config.defaultAgent,
agent: config.agent,
configDir: config.configDir,
enableConfigDiscovery: config.enableConfigDiscovery,
@@ -893,6 +894,7 @@ export class CopilotClient {
mcpServers: config.mcpServers,
envValueMode: "direct",
customAgents: config.customAgents,
+ defaultAgent: config.defaultAgent,
agent: config.agent,
skillDirectories: config.skillDirectories,
disabledSkills: config.disabledSkills,
diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts
index e2942998a..503d0942d 100644
--- a/nodejs/src/index.ts
+++ b/nodejs/src/index.ts
@@ -38,6 +38,7 @@ export type {
MCPStdioServerConfig,
MCPHTTPServerConfig,
MCPServerConfig,
+ DefaultAgentConfig,
MessageOptions,
ModelBilling,
ModelCapabilities,
diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts
index 9b2df4193..a8c644341 100644
--- a/nodejs/src/types.ts
+++ b/nodejs/src/types.ts
@@ -1114,6 +1114,21 @@ export interface CustomAgentConfig {
skills?: string[];
}
+/**
+ * Configuration for the default agent (the built-in agent that handles
+ * turns when no custom agent is selected).
+ * Use this to control tool visibility for the default agent independently of custom sub-agents.
+ */
+export interface DefaultAgentConfig {
+ /**
+ * List of tool names to exclude from the default agent.
+ * These tools remain available to custom sub-agents that reference them in their `tools` array.
+ * Use this to register tools that should only be accessed via delegation to sub-agents,
+ * keeping the default agent's context clean.
+ */
+ excludedTools?: string[];
+}
+
/**
* Configuration for infinite sessions with automatic context compaction and workspace persistence.
* When enabled, sessions automatically manage context window limits through background compaction
@@ -1292,6 +1307,14 @@ export interface SessionConfig {
*/
customAgents?: CustomAgentConfig[];
+ /**
+ * Configuration for the default agent (the built-in agent that handles
+ * turns when no custom agent is selected).
+ * Use `excludedTools` to hide specific tools from the default agent while keeping
+ * them available to custom sub-agents.
+ */
+ defaultAgent?: DefaultAgentConfig;
+
/**
* Name of the custom agent to activate when the session starts.
* Must match the `name` of one of the agents in `customAgents`.
@@ -1360,6 +1383,7 @@ export type ResumeSessionConfig = Pick<
| "enableConfigDiscovery"
| "mcpServers"
| "customAgents"
+ | "defaultAgent"
| "agent"
| "skillDirectories"
| "disabledSkills"
diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts
index 1c0eceb65..4ea74b576 100644
--- a/nodejs/test/client.test.ts
+++ b/nodejs/test/client.test.ts
@@ -227,6 +227,45 @@ describe("CopilotClient", () => {
spy.mockRestore();
});
+ it("forwards defaultAgent in session.create request", async () => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ const spy = vi.spyOn((client as any).connection!, "sendRequest");
+ await client.createSession({
+ defaultAgent: { excludedTools: ["heavy-tool"] },
+ onPermissionRequest: approveAll,
+ });
+
+ expect(spy).toHaveBeenCalledWith(
+ "session.create",
+ expect.objectContaining({
+ defaultAgent: { excludedTools: ["heavy-tool"] },
+ })
+ );
+ });
+
+ it("forwards defaultAgent in session.resume request", async () => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ const session = await client.createSession({ onPermissionRequest: approveAll });
+ const spy = vi.spyOn((client as any).connection!, "sendRequest");
+ await client.resumeSession(session.sessionId, {
+ defaultAgent: { excludedTools: ["heavy-tool"] },
+ onPermissionRequest: approveAll,
+ });
+
+ expect(spy).toHaveBeenCalledWith(
+ "session.resume",
+ expect.objectContaining({
+ defaultAgent: { excludedTools: ["heavy-tool"] },
+ })
+ );
+ });
+
it("does not request permissions on session.resume when using the default joinSession handler", async () => {
const client = new CopilotClient();
await client.start();
diff --git a/nodejs/test/e2e/mcp_and_agents.test.ts b/nodejs/test/e2e/mcp_and_agents.test.ts
index 59e6d498b..aa580cdee 100644
--- a/nodejs/test/e2e/mcp_and_agents.test.ts
+++ b/nodejs/test/e2e/mcp_and_agents.test.ts
@@ -5,8 +5,9 @@
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
import { describe, expect, it } from "vitest";
+import { z } from "zod";
import type { CustomAgentConfig, MCPStdioServerConfig, MCPServerConfig } from "../../src/index.js";
-import { approveAll } from "../../src/index.js";
+import { approveAll, defineTool } from "../../src/index.js";
import { createSdkTestContext } from "./harness/sdkTestContext.js";
const __filename = fileURLToPath(import.meta.url);
@@ -14,7 +15,7 @@ const __dirname = dirname(__filename);
const TEST_MCP_SERVER = resolve(__dirname, "../../../test/harness/test-mcp-server.mjs");
describe("MCP Servers and Custom Agents", async () => {
- const { copilotClient: client } = await createSdkTestContext();
+ const { copilotClient: client, openAiEndpoint } = await createSdkTestContext();
describe("MCP Servers", () => {
it("should accept MCP server configuration on session create", async () => {
@@ -296,4 +297,72 @@ describe("MCP Servers and Custom Agents", async () => {
await session.disconnect();
});
});
+
+ describe("Default Agent Tool Exclusion", () => {
+ it("should hide excluded tools from default agent", async () => {
+ const secretTool = defineTool("secret_tool", {
+ description: "A secret tool hidden from the default agent",
+ parameters: z.object({
+ input: z.string().describe("Input to process"),
+ }),
+ handler: ({ input }) => `SECRET:${input}`,
+ });
+
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ tools: [secretTool],
+ defaultAgent: {
+ excludedTools: ["secret_tool"],
+ },
+ });
+
+ // Ask about the tool — the default agent should not see it
+ const message = await session.sendAndWait({
+ prompt: "Do you have access to a tool called secret_tool? Answer yes or no.",
+ });
+
+ // Sanity-check the replayed response (not the actual exclusion assertion)
+ expect(message?.data.content?.toLowerCase()).toContain("no");
+
+ // The real assertion: verify the runtime excluded the tool from the CAPI request
+ const exchanges = await openAiEndpoint.getExchanges();
+ const toolNames = exchanges.flatMap((e) =>
+ (e.request.tools ?? []).map((t) => ("function" in t ? t.function.name : ""))
+ );
+ expect(toolNames).not.toContain("secret_tool");
+
+ await session.disconnect();
+ });
+
+ it("should accept defaultAgent configuration on session resume", async () => {
+ const session1 = await client.createSession({ onPermissionRequest: approveAll });
+ const sessionId = session1.sessionId;
+ await session1.sendAndWait({ prompt: "What is 3+3?" });
+
+ const secretTool = defineTool("secret_tool", {
+ description: "A secret tool hidden from the default agent",
+ parameters: z.object({
+ input: z.string().describe("Input to process"),
+ }),
+ handler: ({ input }) => `SECRET:${input}`,
+ });
+
+ const session2 = await client.resumeSession(sessionId, {
+ onPermissionRequest: approveAll,
+ tools: [secretTool],
+ defaultAgent: {
+ excludedTools: ["secret_tool"],
+ },
+ });
+
+ expect(session2.sessionId).toBe(sessionId);
+
+ const message = await session2.sendAndWait({
+ prompt: "What is 4+4?",
+ });
+ expect(message?.data.content).toContain("8");
+
+ await session2.disconnect();
+ });
+ });
});
diff --git a/python/copilot/client.py b/python/copilot/client.py
index 4c1186f23..09d970f4b 100644
--- a/python/copilot/client.py
+++ b/python/copilot/client.py
@@ -47,6 +47,7 @@
CopilotSession,
CreateSessionFsHandler,
CustomAgentConfig,
+ DefaultAgentConfig,
ElicitationHandler,
InfiniteSessionConfig,
MCPServerConfig,
@@ -1199,6 +1200,7 @@ async def create_session(
include_sub_agent_streaming_events: bool | None = None,
mcp_servers: dict[str, MCPServerConfig] | None = None,
custom_agents: list[CustomAgentConfig] | None = None,
+ default_agent: DefaultAgentConfig | dict[str, Any] | None = None,
agent: str | None = None,
config_dir: str | None = None,
enable_config_discovery: bool | None = None,
@@ -1241,6 +1243,8 @@ async def create_session(
``subagent.*`` lifecycle events are forwarded. Defaults to True.
mcp_servers: MCP server configurations.
custom_agents: Custom agent configurations.
+ default_agent: Configuration for the default agent,
+ including tool visibility controls.
agent: Agent to use for the session.
config_dir: Override for the configuration directory.
enable_config_discovery: When True, automatically discovers MCP server
@@ -1373,6 +1377,10 @@ async def create_session(
self._convert_custom_agent_to_wire_format(agent) for agent in custom_agents
]
+ # Add default agent configuration if provided
+ if default_agent:
+ payload["defaultAgent"] = self._convert_default_agent_to_wire_format(default_agent)
+
# Add agent selection if provided
if agent:
payload["agent"] = agent
@@ -1477,6 +1485,7 @@ async def resume_session(
include_sub_agent_streaming_events: bool | None = None,
mcp_servers: dict[str, MCPServerConfig] | None = None,
custom_agents: list[CustomAgentConfig] | None = None,
+ default_agent: DefaultAgentConfig | dict[str, Any] | None = None,
agent: str | None = None,
config_dir: str | None = None,
enable_config_discovery: bool | None = None,
@@ -1519,6 +1528,8 @@ async def resume_session(
``subagent.*`` lifecycle events are forwarded. Defaults to True.
mcp_servers: MCP server configurations.
custom_agents: Custom agent configurations.
+ default_agent: Configuration for the default agent,
+ including tool visibility controls.
agent: Agent to use for the session.
config_dir: Override for the configuration directory.
enable_config_discovery: When True, automatically discovers MCP server
@@ -1645,6 +1656,10 @@ async def resume_session(
self._convert_custom_agent_to_wire_format(a) for a in custom_agents
]
+ # Add default agent configuration if provided
+ if default_agent:
+ payload["defaultAgent"] = self._convert_default_agent_to_wire_format(default_agent)
+
if agent:
payload["agent"] = agent
if skill_directories:
@@ -2188,6 +2203,23 @@ def _convert_custom_agent_to_wire_format(
wire_agent["skills"] = agent["skills"]
return wire_agent
+ def _convert_default_agent_to_wire_format(
+ self, config: DefaultAgentConfig | dict[str, Any]
+ ) -> dict[str, Any]:
+ """
+ Convert default agent config from snake_case to camelCase wire format.
+
+ Args:
+ config: The default agent configuration in snake_case format.
+
+ Returns:
+ The default agent configuration in camelCase wire format.
+ """
+ wire: dict[str, Any] = {}
+ if "excluded_tools" in config:
+ wire["excludedTools"] = config["excluded_tools"]
+ return wire
+
async def _start_cli_server(self) -> None:
"""
Start the CLI server process.
diff --git a/python/copilot/session.py b/python/copilot/session.py
index ac771923a..148b1aa63 100644
--- a/python/copilot/session.py
+++ b/python/copilot/session.py
@@ -781,6 +781,18 @@ class CustomAgentConfig(TypedDict, total=False):
skills: NotRequired[list[str]]
+class DefaultAgentConfig(TypedDict, total=False):
+ """Configuration for the default agent.
+
+ The default agent is the built-in agent that handles turns
+ when no custom agent is selected.
+ """
+
+ # List of tool names to exclude from the default agent.
+ # These tools remain available to custom sub-agents that reference them.
+ excluded_tools: list[str]
+
+
class InfiniteSessionConfig(TypedDict, total=False):
"""
Configuration for infinite sessions with automatic context compaction
@@ -870,6 +882,10 @@ class SessionConfig(TypedDict, total=False):
mcp_servers: dict[str, MCPServerConfig]
# Custom agent configurations for the session
custom_agents: list[CustomAgentConfig]
+ # Configuration for the default agent.
+ # Use excluded_tools to hide tools from the default agent
+ # while keeping them available to sub-agents.
+ default_agent: DefaultAgentConfig
# Name of the custom agent to activate when the session starts.
# Must match the name of one of the agents in custom_agents.
agent: str
@@ -938,6 +954,8 @@ class ResumeSessionConfig(TypedDict, total=False):
mcp_servers: dict[str, MCPServerConfig]
# Custom agent configurations for the session
custom_agents: list[CustomAgentConfig]
+ # Configuration for the default agent.
+ default_agent: DefaultAgentConfig
# Name of the custom agent to activate when the session starts.
# Must match the name of one of the agents in custom_agents.
agent: str
diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py
index 621062e4e..9e8440b9d 100644
--- a/python/e2e/test_session.py
+++ b/python/e2e/test_session.py
@@ -146,6 +146,34 @@ async def test_should_create_a_session_with_excludedTools(self, ctx: E2ETestCont
assert "grep" in tool_names
assert "view" not in tool_names
+ async def test_should_create_a_session_with_defaultAgent_excludedTools(
+ self, ctx: E2ETestContext
+ ):
+ secret_tool = Tool(
+ name="secret_tool",
+ description="A secret tool hidden from the default agent",
+ handler=lambda args: "SECRET",
+ parameters={
+ "type": "object",
+ "properties": {"input": {"type": "string"}},
+ },
+ )
+
+ session = await ctx.client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ tools=[secret_tool],
+ default_agent={"excluded_tools": ["secret_tool"]},
+ )
+
+ await session.send("What is 1+1?")
+ await get_final_assistant_message(session)
+
+ # The real assertion: verify the runtime excluded the tool from the CAPI request
+ traffic = await ctx.get_exchanges()
+ tools = traffic[0]["request"]["tools"]
+ tool_names = [t["function"]["name"] for t in tools]
+ assert "secret_tool" not in tool_names
+
# TODO: This test shows there's a race condition inside client.ts. If createSession
# is called concurrently and autoStart is on, it may start multiple child processes.
# This needs to be fixed. Right now it manifests as being unable to delete the temp
diff --git a/test/scenarios/tools/custom-agents/README.md b/test/scenarios/tools/custom-agents/README.md
index 41bb78c9e..391345454 100644
--- a/test/scenarios/tools/custom-agents/README.md
+++ b/test/scenarios/tools/custom-agents/README.md
@@ -1,26 +1,30 @@
# Config Sample: Custom Agents
-Demonstrates configuring the Copilot SDK with **custom agent definitions** that restrict which tools an agent can use. This validates:
+Demonstrates configuring the Copilot SDK with **custom agent definitions** that restrict which tools an agent can use, and **agent-exclusive tools** that are hidden from the main agent. This validates:
1. **Agent definition** — The `customAgents` session config accepts agent definitions with name, description, tool lists, and custom prompts.
2. **Tool scoping** — Each custom agent can be restricted to a subset of available tools (e.g. read-only tools like `grep`, `glob`, `view`).
-3. **Agent awareness** — The model recognizes and can describe the configured custom agents.
+3. **Agent-exclusive tools** — The `defaultAgent.excludedTools` option hides tools from the main agent while keeping them available to sub-agents.
+4. **Agent awareness** — The model recognizes and can describe the configured custom agents.
## What Each Sample Does
-1. Creates a session with a `customAgents` array containing a "researcher" agent
-2. The researcher agent is scoped to read-only tools: `grep`, `glob`, `view`
-3. Sends: _"What custom agents are available? Describe the researcher agent and its capabilities."_
-4. Prints the response — which should describe the researcher agent and its tool restrictions
+1. Creates a session with a custom `analyze-codebase` tool and a `customAgents` array containing a "researcher" agent
+2. Uses `defaultAgent.excludedTools` to hide `analyze-codebase` from the main agent
+3. The researcher agent is scoped to read-only tools plus `analyze-codebase`: `grep`, `glob`, `view`, `analyze-codebase`
+4. Sends: _"What custom agents are available? Describe the researcher agent and its capabilities."_
+5. Prints the response — which should describe the researcher agent and its tool restrictions
## Configuration
| Option | Value | Effect |
|--------|-------|--------|
+| `tools` | `[analyze-codebase]` | Registers custom tool at session level |
+| `defaultAgent.excludedTools` | `["analyze-codebase"]` | Hides tool from main agent |
| `customAgents[0].name` | `"researcher"` | Internal identifier for the agent |
| `customAgents[0].displayName` | `"Research Agent"` | Human-readable name |
| `customAgents[0].description` | Custom text | Describes agent purpose |
-| `customAgents[0].tools` | `["grep", "glob", "view"]` | Restricts agent to read-only tools |
+| `customAgents[0].tools` | `["grep", "glob", "view", "analyze-codebase"]` | Restricts agent to read-only tools + analysis |
| `customAgents[0].prompt` | Custom text | Sets agent behavior instructions |
## Run
diff --git a/test/scenarios/tools/custom-agents/csharp/Program.cs b/test/scenarios/tools/custom-agents/csharp/Program.cs
index c5c6525f1..d3c068ade 100644
--- a/test/scenarios/tools/custom-agents/csharp/Program.cs
+++ b/test/scenarios/tools/custom-agents/csharp/Program.cs
@@ -1,4 +1,5 @@
using GitHub.Copilot.SDK;
+using Microsoft.Extensions.AI;
var cliPath = Environment.GetEnvironmentVariable("COPILOT_CLI_PATH");
@@ -12,9 +13,22 @@
try
{
+ var analyzeCodebase = AIFunctionFactory.Create(
+ (string query) => $"Analysis result for: {query}",
+ new AIFunctionFactoryOptions
+ {
+ Name = "analyze-codebase",
+ Description = "Performs deep analysis of the codebase",
+ });
+
await using var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "claude-haiku-4.5",
+ Tools = [analyzeCodebase],
+ DefaultAgent = new DefaultAgentConfig
+ {
+ ExcludedTools = ["analyze-codebase"],
+ },
CustomAgents =
[
new CustomAgentConfig
@@ -22,7 +36,7 @@
Name = "researcher",
DisplayName = "Research Agent",
Description = "A research agent that can only read and search files, not modify them",
- Tools = ["grep", "glob", "view"],
+ Tools = ["grep", "glob", "view", "analyze-codebase"],
Prompt = "You are a research assistant. You can search and read files but cannot modify anything. When asked about your capabilities, list the tools you have access to.",
},
],
diff --git a/test/scenarios/tools/custom-agents/go/main.go b/test/scenarios/tools/custom-agents/go/main.go
index d1769ff2b..1e6ada739 100644
--- a/test/scenarios/tools/custom-agents/go/main.go
+++ b/test/scenarios/tools/custom-agents/go/main.go
@@ -20,14 +20,29 @@ func main() {
}
defer client.Stop()
+ type AnalyzeParams struct {
+ Query string `json:"query" jsonschema:"the analysis query"`
+ }
+
+ analyzeCodebase := copilot.DefineTool("analyze-codebase",
+ "Performs deep analysis of the codebase",
+ func(params AnalyzeParams, inv copilot.ToolInvocation) (string, error) {
+ return fmt.Sprintf("Analysis result for: %s", params.Query), nil
+ },
+ )
+
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
Model: "claude-haiku-4.5",
+ Tools: []copilot.Tool{analyzeCodebase},
+ DefaultAgent: &copilot.DefaultAgentConfig{
+ ExcludedTools: []string{"analyze-codebase"},
+ },
CustomAgents: []copilot.CustomAgentConfig{
{
Name: "researcher",
DisplayName: "Research Agent",
Description: "A research agent that can only read and search files, not modify them",
- Tools: []string{"grep", "glob", "view"},
+ Tools: []string{"grep", "glob", "view", "analyze-codebase"},
Prompt: "You are a research assistant. You can search and read files but cannot modify anything. When asked about your capabilities, list the tools you have access to.",
},
},
diff --git a/test/scenarios/tools/custom-agents/python/main.py b/test/scenarios/tools/custom-agents/python/main.py
index d4c45950f..bf6e3978c 100644
--- a/test/scenarios/tools/custom-agents/python/main.py
+++ b/test/scenarios/tools/custom-agents/python/main.py
@@ -2,6 +2,11 @@
import os
from copilot import CopilotClient
from copilot.client import SubprocessConfig
+from copilot.tools import Tool
+
+
+async def analyze_handler(args):
+ return f"Analysis result for: {args.get('query', '')}"
async def main():
@@ -12,18 +17,29 @@ async def main():
try:
session = await client.create_session(
- {
- "model": "claude-haiku-4.5",
- "custom_agents": [
- {
- "name": "researcher",
- "display_name": "Research Agent",
- "description": "A research agent that can only read and search files, not modify them",
- "tools": ["grep", "glob", "view"],
- "prompt": "You are a research assistant. You can search and read files but cannot modify anything. When asked about your capabilities, list the tools you have access to.",
+ model="claude-haiku-4.5",
+ tools=[
+ Tool(
+ name="analyze-codebase",
+ description="Performs deep analysis of the codebase",
+ handler=analyze_handler,
+ parameters={
+ "type": "object",
+ "properties": {"query": {"type": "string"}},
},
- ],
- }
+ ),
+ ],
+ default_agent={"excluded_tools": ["analyze-codebase"]},
+ custom_agents=[
+ {
+ "name": "researcher",
+ "display_name": "Research Agent",
+ "description": "A research agent that can only read and search files, not modify them",
+ "tools": ["grep", "glob", "view", "analyze-codebase"],
+ "prompt": "You are a research assistant. You can search and read files but cannot modify anything. When asked about your capabilities, list the tools you have access to.",
+ },
+ ],
+ on_permission_request=lambda _: {"action": "allow"},
)
response = await session.send_and_wait(
diff --git a/test/scenarios/tools/custom-agents/typescript/src/index.ts b/test/scenarios/tools/custom-agents/typescript/src/index.ts
index f6e163256..ffb0bd827 100644
--- a/test/scenarios/tools/custom-agents/typescript/src/index.ts
+++ b/test/scenarios/tools/custom-agents/typescript/src/index.ts
@@ -1,4 +1,13 @@
-import { CopilotClient } from "@github/copilot-sdk";
+import { CopilotClient, defineTool } from "@github/copilot-sdk";
+import { z } from "zod";
+
+const analyzeCodebase = defineTool("analyze-codebase", {
+ description: "Performs deep analysis of the codebase, generating extensive context",
+ parameters: z.object({ query: z.string().describe("The analysis query") }),
+ handler: async ({ query }) => {
+ return `Analysis result for: ${query}`;
+ },
+});
async function main() {
const client = new CopilotClient({
@@ -9,12 +18,16 @@ async function main() {
try {
const session = await client.createSession({
model: "claude-haiku-4.5",
+ tools: [analyzeCodebase],
+ defaultAgent: {
+ excludedTools: ["analyze-codebase"],
+ },
customAgents: [
{
name: "researcher",
displayName: "Research Agent",
description: "A research agent that can only read and search files, not modify them",
- tools: ["grep", "glob", "view"],
+ tools: ["grep", "glob", "view", "analyze-codebase"],
prompt: "You are a research assistant. You can search and read files but cannot modify anything. When asked about your capabilities, list the tools you have access to.",
},
],
diff --git a/test/snapshots/mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume.yaml b/test/snapshots/mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume.yaml
new file mode 100644
index 000000000..65fe6664e
--- /dev/null
+++ b/test/snapshots/mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume.yaml
@@ -0,0 +1,14 @@
+models:
+ - claude-sonnet-4.5
+conversations:
+ - messages:
+ - role: system
+ content: ${system}
+ - role: user
+ content: What is 3+3?
+ - role: assistant
+ content: 3 + 3 = 6
+ - role: user
+ content: What is 4+4?
+ - role: assistant
+ content: 4 + 4 = 8
diff --git a/test/snapshots/mcp_and_agents/should_hide_excluded_tools_from_default_agent.yaml b/test/snapshots/mcp_and_agents/should_hide_excluded_tools_from_default_agent.yaml
new file mode 100644
index 000000000..f5506bb18
--- /dev/null
+++ b/test/snapshots/mcp_and_agents/should_hide_excluded_tools_from_default_agent.yaml
@@ -0,0 +1,10 @@
+models:
+ - claude-sonnet-4.5
+conversations:
+ - messages:
+ - role: system
+ content: ${system}
+ - role: user
+ content: Do you have access to a tool called secret_tool? Answer yes or no.
+ - role: assistant
+ content: No, I don't have access to a tool called secret_tool.
diff --git a/test/snapshots/session/should_create_a_session_with_defaultagent_excludedtools.yaml b/test/snapshots/session/should_create_a_session_with_defaultagent_excludedtools.yaml
new file mode 100644
index 000000000..250402101
--- /dev/null
+++ b/test/snapshots/session/should_create_a_session_with_defaultagent_excludedtools.yaml
@@ -0,0 +1,10 @@
+models:
+ - claude-sonnet-4.5
+conversations:
+ - messages:
+ - role: system
+ content: ${system}
+ - role: user
+ content: What is 1+1?
+ - role: assistant
+ content: 1 + 1 = 2