Skip to content

Commit b1b0df5

Browse files
feat: add per-agent tool visibility via defaultAgent.excludedTools (#1098)
* feat: add per-agent tool visibility via defaultAgent.excludedTools Add a new DefaultAgentConfig type and defaultAgent property to SessionConfig across all four SDKs (Node.js, Python, Go, .NET). This allows tools to be hidden from the default agent while remaining available to custom sub-agents, enabling the orchestrator pattern where the default agent delegates heavy-context work to specialized sub-agents. The default agent is the built-in agent that handles turns when no custom agent is selected. Tools listed in defaultAgent.excludedTools are excluded from the default agent but remain available to sub-agents that reference them in their tools array. Changes: - Node.js: DefaultAgentConfig interface, defaultAgent on SessionConfig/ ResumeSessionConfig, RPC pass-through, 2 unit tests - Python: DefaultAgentConfig TypedDict, default_agent parameter on create_session()/resume_session(), wire format conversion - Go: DefaultAgentConfig struct, DefaultAgent on config and request structs - .NET: DefaultAgentConfig class, DefaultAgent property on configs, copy constructors, and RPC request records - Docs: Agent-Exclusive Tools section in custom-agents.md - Test scenario: custom-agents scenario updated with defaultAgent usage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address CI validation failures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address PR review feedback - Fix defineTool signature in TS scenario and docs (name, config) - Remove unused System.Text.Json import from C# scenario Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address remaining CI validation failures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 922959f commit b1b0df5

24 files changed

Lines changed: 617 additions & 24 deletions

docs/features/custom-agents.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,154 @@ const session = await client.createSession({
759759

760760
> **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.
761761
762+
## Agent-Exclusive Tools
763+
764+
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.
765+
766+
This is useful when:
767+
- Certain tools generate large amounts of context that would overwhelm the main agent
768+
- You want the main agent to act as an orchestrator, delegating heavy work to specialized sub-agents
769+
- You need strict separation between orchestration and execution
770+
771+
<details open>
772+
<summary><strong>Node.js / TypeScript</strong></summary>
773+
774+
```typescript
775+
import { CopilotClient, defineTool, approveAll } from "@github/copilot-sdk";
776+
import { z } from "zod";
777+
778+
const heavyContextTool = defineTool("analyze-codebase", {
779+
description: "Performs deep analysis of the codebase, generating extensive context",
780+
parameters: z.object({ query: z.string() }),
781+
handler: async ({ query }) => {
782+
// ... expensive analysis that returns lots of data
783+
return { analysis: "..." };
784+
},
785+
});
786+
787+
const session = await client.createSession({
788+
tools: [heavyContextTool],
789+
defaultAgent: {
790+
excludedTools: ["analyze-codebase"],
791+
},
792+
customAgents: [
793+
{
794+
name: "researcher",
795+
description: "Deep codebase analysis agent with access to heavy-context tools",
796+
tools: ["analyze-codebase"],
797+
prompt: "You perform thorough codebase analysis using the analyze-codebase tool.",
798+
},
799+
],
800+
});
801+
```
802+
803+
</details>
804+
805+
<details>
806+
<summary><strong>Python</strong></summary>
807+
808+
```python
809+
from copilot import CopilotClient
810+
from copilot.tools import Tool
811+
812+
heavy_tool = Tool(
813+
name="analyze-codebase",
814+
description="Performs deep analysis of the codebase",
815+
handler=analyze_handler,
816+
parameters={"type": "object", "properties": {"query": {"type": "string"}}},
817+
)
818+
819+
session = await client.create_session(
820+
tools=[heavy_tool],
821+
default_agent={"excluded_tools": ["analyze-codebase"]},
822+
custom_agents=[
823+
{
824+
"name": "researcher",
825+
"description": "Deep codebase analysis agent",
826+
"tools": ["analyze-codebase"],
827+
"prompt": "You perform thorough codebase analysis.",
828+
},
829+
],
830+
on_permission_request=approve_all,
831+
)
832+
```
833+
834+
</details>
835+
836+
<details>
837+
<summary><strong>Go</strong></summary>
838+
839+
<!-- docs-validate: skip -->
840+
```go
841+
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
842+
Tools: []copilot.Tool{heavyTool},
843+
DefaultAgent: &copilot.DefaultAgentConfig{
844+
ExcludedTools: []string{"analyze-codebase"},
845+
},
846+
CustomAgents: []copilot.CustomAgentConfig{
847+
{
848+
Name: "researcher",
849+
Description: "Deep codebase analysis agent",
850+
Tools: []string{"analyze-codebase"},
851+
Prompt: "You perform thorough codebase analysis.",
852+
},
853+
},
854+
})
855+
```
856+
857+
</details>
858+
859+
<details>
860+
<summary><strong>C# / .NET</strong></summary>
861+
862+
<!-- docs-validate: skip -->
863+
```csharp
864+
var session = await client.CreateSessionAsync(new SessionConfig
865+
{
866+
Tools = [analyzeCodebaseTool],
867+
DefaultAgent = new DefaultAgentConfig
868+
{
869+
ExcludedTools = ["analyze-codebase"],
870+
},
871+
CustomAgents =
872+
[
873+
new CustomAgentConfig
874+
{
875+
Name = "researcher",
876+
Description = "Deep codebase analysis agent",
877+
Tools = ["analyze-codebase"],
878+
Prompt = "You perform thorough codebase analysis.",
879+
},
880+
],
881+
});
882+
```
883+
884+
</details>
885+
886+
### How It Works
887+
888+
Tools listed in `defaultAgent.excludedTools`:
889+
890+
1. **Are registered** — their handlers are available for execution
891+
2. **Are hidden** from the main agent's tool list — the LLM won't see or call them directly
892+
3. **Remain available** to any custom sub-agent that includes them in its `tools` array
893+
894+
### Interaction with Other Tool Filters
895+
896+
`defaultAgent.excludedTools` is orthogonal to the session-level `availableTools` and `excludedTools`:
897+
898+
| Filter | Scope | Effect |
899+
|--------|-------|--------|
900+
| `availableTools` | Session-wide | Allowlist — only these tools exist for anyone |
901+
| `excludedTools` | Session-wide | Blocklist — these tools are blocked for everyone |
902+
| `defaultAgent.excludedTools` | Main agent only | These tools are hidden from the main agent but available to sub-agents |
903+
904+
Precedence:
905+
1. Session-level `availableTools`/`excludedTools` are applied first (globally)
906+
2. `defaultAgent.excludedTools` is applied on top, further restricting the main agent only
907+
908+
> **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.
909+
762910
## Attaching MCP Servers to Agents
763911

764912
Each custom agent can have its own MCP (Model Context Protocol) servers, giving it access to specialized data sources:

dotnet/src/Client.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
501501
config.McpServers,
502502
"direct",
503503
config.CustomAgents,
504+
config.DefaultAgent,
504505
config.Agent,
505506
config.ConfigDir,
506507
config.EnableConfigDiscovery,
@@ -627,6 +628,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
627628
config.McpServers,
628629
"direct",
629630
config.CustomAgents,
631+
config.DefaultAgent,
630632
config.Agent,
631633
config.SkillDirectories,
632634
config.DisabledSkills,
@@ -1642,6 +1644,7 @@ internal record CreateSessionRequest(
16421644
IDictionary<string, McpServerConfig>? McpServers,
16431645
string? EnvValueMode,
16441646
IList<CustomAgentConfig>? CustomAgents,
1647+
DefaultAgentConfig? DefaultAgent,
16451648
string? Agent,
16461649
string? ConfigDir,
16471650
bool? EnableConfigDiscovery,
@@ -1698,6 +1701,7 @@ internal record ResumeSessionRequest(
16981701
IDictionary<string, McpServerConfig>? McpServers,
16991702
string? EnvValueMode,
17001703
IList<CustomAgentConfig>? CustomAgents,
1704+
DefaultAgentConfig? DefaultAgent,
17011705
string? Agent,
17021706
IList<string>? SkillDirectories,
17031707
IList<string>? DisabledSkills,

dotnet/src/Types.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1656,6 +1656,21 @@ public class CustomAgentConfig
16561656
public IList<string>? Skills { get; set; }
16571657
}
16581658

1659+
/// <summary>
1660+
/// Configuration for the default agent (the built-in agent that handles turns when no custom agent is selected).
1661+
/// Use <see cref="ExcludedTools"/> to hide specific tools from the default agent
1662+
/// while keeping them available to custom sub-agents.
1663+
/// </summary>
1664+
public class DefaultAgentConfig
1665+
{
1666+
/// <summary>
1667+
/// List of tool names to exclude from the default agent.
1668+
/// These tools remain available to custom sub-agents that reference them
1669+
/// in their <see cref="CustomAgentConfig.Tools"/> list.
1670+
/// </summary>
1671+
public IList<string>? ExcludedTools { get; set; }
1672+
}
1673+
16591674
/// <summary>
16601675
/// Configuration for infinite sessions with automatic context compaction and workspace persistence.
16611676
/// When enabled, sessions automatically manage context window limits through background compaction
@@ -1709,6 +1724,7 @@ protected SessionConfig(SessionConfig? other)
17091724
Commands = other.Commands is not null ? [.. other.Commands] : null;
17101725
ConfigDir = other.ConfigDir;
17111726
CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null;
1727+
DefaultAgent = other.DefaultAgent;
17121728
Agent = other.Agent;
17131729
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
17141730
EnableConfigDiscovery = other.EnableConfigDiscovery;
@@ -1871,6 +1887,13 @@ protected SessionConfig(SessionConfig? other)
18711887
/// </summary>
18721888
public IList<CustomAgentConfig>? CustomAgents { get; set; }
18731889

1890+
/// <summary>
1891+
/// Configuration for the default agent (the built-in agent that handles turns when no custom agent is selected).
1892+
/// Use <see cref="DefaultAgentConfig.ExcludedTools"/> to hide specific tools from the default agent
1893+
/// while keeping them available to custom sub-agents.
1894+
/// </summary>
1895+
public DefaultAgentConfig? DefaultAgent { get; set; }
1896+
18741897
/// <summary>
18751898
/// Name of the custom agent to activate when the session starts.
18761899
/// Must match the <see cref="CustomAgentConfig.Name"/> of one of the agents in <see cref="CustomAgents"/>.
@@ -1950,6 +1973,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
19501973
Commands = other.Commands is not null ? [.. other.Commands] : null;
19511974
ConfigDir = other.ConfigDir;
19521975
CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null;
1976+
DefaultAgent = other.DefaultAgent;
19531977
Agent = other.Agent;
19541978
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
19551979
DisableResume = other.DisableResume;
@@ -2117,6 +2141,13 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
21172141
/// </summary>
21182142
public IList<CustomAgentConfig>? CustomAgents { get; set; }
21192143

2144+
/// <summary>
2145+
/// Configuration for the default agent (the built-in agent that handles turns when no custom agent is selected).
2146+
/// Use <see cref="DefaultAgentConfig.ExcludedTools"/> to hide specific tools from the default agent
2147+
/// while keeping them available to custom sub-agents.
2148+
/// </summary>
2149+
public DefaultAgentConfig? DefaultAgent { get; set; }
2150+
21202151
/// <summary>
21212152
/// Name of the custom agent to activate when the session starts.
21222153
/// Must match the <see cref="CustomAgentConfig.Name"/> of one of the agents in <see cref="CustomAgents"/>.

dotnet/test/CloneTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
9090
McpServers = new Dictionary<string, McpServerConfig> { ["server1"] = new McpStdioServerConfig { Command = "echo" } },
9191
CustomAgents = [new CustomAgentConfig { Name = "agent1" }],
9292
Agent = "agent1",
93+
DefaultAgent = new DefaultAgentConfig { ExcludedTools = ["hidden-tool"] },
9394
SkillDirectories = ["/skills"],
9495
DisabledSkills = ["skill1"],
9596
};
@@ -109,6 +110,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
109110
Assert.Equal(original.McpServers.Count, clone.McpServers!.Count);
110111
Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count);
111112
Assert.Equal(original.Agent, clone.Agent);
113+
Assert.Equal(original.DefaultAgent!.ExcludedTools, clone.DefaultAgent!.ExcludedTools);
112114
Assert.Equal(original.SkillDirectories, clone.SkillDirectories);
113115
Assert.Equal(original.DisabledSkills, clone.DisabledSkills);
114116
}
@@ -245,6 +247,7 @@ public void Clone_WithNullCollections_ReturnsNullCollections()
245247
Assert.Null(clone.SkillDirectories);
246248
Assert.Null(clone.DisabledSkills);
247249
Assert.Null(clone.Tools);
250+
Assert.Null(clone.DefaultAgent);
248251
Assert.True(clone.IncludeSubAgentStreamingEvents);
249252
}
250253

dotnet/test/SessionTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,35 @@ public async Task Should_Create_A_Session_With_ExcludedTools()
162162
Assert.Contains("grep", toolNames);
163163
}
164164

165+
[Fact]
166+
public async Task Should_Create_A_Session_With_DefaultAgent_ExcludedTools()
167+
{
168+
var session = await CreateSessionAsync(new SessionConfig
169+
{
170+
Tools =
171+
[
172+
AIFunctionFactory.Create(
173+
(string input) => "SECRET",
174+
"secret_tool",
175+
"A secret tool hidden from the default agent"),
176+
],
177+
DefaultAgent = new DefaultAgentConfig
178+
{
179+
ExcludedTools = ["secret_tool"],
180+
},
181+
});
182+
183+
await session.SendAsync(new MessageOptions { Prompt = "What is 1+1?" });
184+
await TestHelper.GetFinalAssistantMessageAsync(session);
185+
186+
// The real assertion: verify the runtime excluded the tool from the CAPI request
187+
var traffic = await Ctx.GetExchangesAsync();
188+
Assert.NotEmpty(traffic);
189+
190+
var toolNames = GetToolNames(traffic[0]);
191+
Assert.DoesNotContain("secret_tool", toolNames);
192+
}
193+
165194
[Fact]
166195
public async Task Should_Create_Session_With_Custom_Tool()
167196
{

go/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
592592
req.MCPServers = config.MCPServers
593593
req.EnvValueMode = "direct"
594594
req.CustomAgents = config.CustomAgents
595+
req.DefaultAgent = config.DefaultAgent
595596
req.Agent = config.Agent
596597
req.SkillDirectories = config.SkillDirectories
597598
req.DisabledSkills = config.DisabledSkills
@@ -776,6 +777,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
776777
req.MCPServers = config.MCPServers
777778
req.EnvValueMode = "direct"
778779
req.CustomAgents = config.CustomAgents
780+
req.DefaultAgent = config.DefaultAgent
779781
req.Agent = config.Agent
780782
req.SkillDirectories = config.SkillDirectories
781783
req.DisabledSkills = config.DisabledSkills

go/internal/e2e/session_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,57 @@ func TestSession(t *testing.T) {
313313
}
314314
})
315315

316+
t.Run("should create a session with defaultAgent excludedTools", func(t *testing.T) {
317+
ctx.ConfigureForTest(t)
318+
319+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
320+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
321+
Tools: []copilot.Tool{
322+
{
323+
Name: "secret_tool",
324+
Description: "A secret tool hidden from the default agent",
325+
Parameters: map[string]any{
326+
"type": "object",
327+
"properties": map[string]any{"input": map[string]any{"type": "string"}},
328+
},
329+
Handler: func(invocation copilot.ToolInvocation) (copilot.ToolResult, error) {
330+
return copilot.ToolResult{TextResultForLLM: "SECRET", ResultType: "success"}, nil
331+
},
332+
},
333+
},
334+
DefaultAgent: &copilot.DefaultAgentConfig{
335+
ExcludedTools: []string{"secret_tool"},
336+
},
337+
})
338+
if err != nil {
339+
t.Fatalf("Failed to create session: %v", err)
340+
}
341+
342+
_, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "What is 1+1?"})
343+
if err != nil {
344+
t.Fatalf("Failed to send message: %v", err)
345+
}
346+
347+
_, err = testharness.GetFinalAssistantMessage(t.Context(), session)
348+
if err != nil {
349+
t.Fatalf("Failed to get assistant message: %v", err)
350+
}
351+
352+
// The real assertion: verify the runtime excluded the tool from the CAPI request
353+
traffic, err := ctx.GetExchanges()
354+
if err != nil {
355+
t.Fatalf("Failed to get exchanges: %v", err)
356+
}
357+
if len(traffic) == 0 {
358+
t.Fatal("Expected at least one exchange")
359+
}
360+
361+
toolNames := getToolNames(traffic[0])
362+
if contains(toolNames, "secret_tool") {
363+
t.Errorf("Expected 'secret_tool' to be excluded from default agent, got %v", toolNames)
364+
}
365+
})
366+
316367
t.Run("should create session with custom tool", func(t *testing.T) {
317368
ctx.ConfigureForTest(t)
318369

0 commit comments

Comments
 (0)