Skip to content

Commit f02ae8a

Browse files
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>
1 parent 719beb0 commit f02ae8a

File tree

24 files changed

+626
-25
lines changed

24 files changed

+626
-25
lines changed

docs/features/custom-agents.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,153 @@ 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({
779+
name: "analyze-codebase",
780+
description: "Performs deep analysis of the codebase, generating extensive context",
781+
parameters: z.object({ query: z.string() }),
782+
handler: async ({ query }) => {
783+
// ... expensive analysis that returns lots of data
784+
return { analysis: "..." };
785+
},
786+
});
787+
788+
const session = await client.createSession({
789+
tools: [heavyContextTool],
790+
defaultAgent: {
791+
excludedTools: ["analyze-codebase"],
792+
},
793+
customAgents: [
794+
{
795+
name: "researcher",
796+
description: "Deep codebase analysis agent with access to heavy-context tools",
797+
tools: ["analyze-codebase"],
798+
prompt: "You perform thorough codebase analysis using the analyze-codebase tool.",
799+
},
800+
],
801+
});
802+
```
803+
804+
</details>
805+
806+
<details>
807+
<summary><strong>Python</strong></summary>
808+
809+
```python
810+
from copilot import CopilotClient
811+
from copilot.tools import Tool
812+
813+
heavy_tool = Tool(
814+
name="analyze-codebase",
815+
description="Performs deep analysis of the codebase",
816+
handler=analyze_handler,
817+
parameters={"type": "object", "properties": {"query": {"type": "string"}}},
818+
)
819+
820+
session = await client.create_session(
821+
tools=[heavy_tool],
822+
default_agent={"excluded_tools": ["analyze-codebase"]},
823+
custom_agents=[
824+
{
825+
"name": "researcher",
826+
"description": "Deep codebase analysis agent",
827+
"tools": ["analyze-codebase"],
828+
"prompt": "You perform thorough codebase analysis.",
829+
},
830+
],
831+
on_permission_request=approve_all,
832+
)
833+
```
834+
835+
</details>
836+
837+
<details>
838+
<summary><strong>Go</strong></summary>
839+
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+
```csharp
863+
var session = await client.CreateSessionAsync(new SessionConfig
864+
{
865+
Tools = [analyzeCodebaseTool],
866+
defaultAgent = new DefaultAgentConfig
867+
{
868+
ExcludedTools = ["analyze-codebase"],
869+
},
870+
CustomAgents =
871+
[
872+
new CustomAgentConfig
873+
{
874+
Name = "researcher",
875+
Description = "Deep codebase analysis agent",
876+
Tools = ["analyze-codebase"],
877+
Prompt = "You perform thorough codebase analysis.",
878+
},
879+
],
880+
});
881+
```
882+
883+
</details>
884+
885+
### How It Works
886+
887+
Tools listed in `defaultAgent.excludedTools`:
888+
889+
1. **Are registered** — their handlers are available for execution
890+
2. **Are hidden** from the main agent's tool list — the LLM won't see or call them directly
891+
3. **Remain available** to any custom sub-agent that includes them in its `tools` array
892+
893+
### Interaction with Other Tool Filters
894+
895+
`defaultAgent.excludedTools` is orthogonal to the session-level `availableTools` and `excludedTools`:
896+
897+
| Filter | Scope | Effect |
898+
|--------|-------|--------|
899+
| `availableTools` | Session-wide | Allowlist — only these tools exist for anyone |
900+
| `excludedTools` | Session-wide | Blocklist — these tools are blocked for everyone |
901+
| `defaultAgent.excludedTools` | Main agent only | These tools are hidden from the main agent but available to sub-agents |
902+
903+
Precedence:
904+
1. Session-level `availableTools`/`excludedTools` are applied first (globally)
905+
2. `defaultAgent.excludedTools` is applied on top, further restricting the main agent only
906+
907+
> **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.
908+
762909
## Attaching MCP Servers to Agents
763910

764911
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
@@ -500,6 +500,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
500500
config.McpServers,
501501
"direct",
502502
config.CustomAgents,
503+
config.DefaultAgent,
503504
config.Agent,
504505
config.ConfigDir,
505506
config.EnableConfigDiscovery,
@@ -625,6 +626,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
625626
config.McpServers,
626627
"direct",
627628
config.CustomAgents,
629+
config.DefaultAgent,
628630
config.Agent,
629631
config.SkillDirectories,
630632
config.DisabledSkills,
@@ -1639,6 +1641,7 @@ internal record CreateSessionRequest(
16391641
IDictionary<string, McpServerConfig>? McpServers,
16401642
string? EnvValueMode,
16411643
IList<CustomAgentConfig>? CustomAgents,
1644+
DefaultAgentConfig? DefaultAgent,
16421645
string? Agent,
16431646
string? ConfigDir,
16441647
bool? EnableConfigDiscovery,
@@ -1694,6 +1697,7 @@ internal record ResumeSessionRequest(
16941697
IDictionary<string, McpServerConfig>? McpServers,
16951698
string? EnvValueMode,
16961699
IList<CustomAgentConfig>? CustomAgents,
1700+
DefaultAgentConfig? DefaultAgent,
16971701
string? Agent,
16981702
IList<string>? SkillDirectories,
16991703
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;
@@ -1859,6 +1875,13 @@ protected SessionConfig(SessionConfig? other)
18591875
/// </summary>
18601876
public IList<CustomAgentConfig>? CustomAgents { get; set; }
18611877

1878+
/// <summary>
1879+
/// Configuration for the default agent (the built-in agent that handles turns when no custom agent is selected).
1880+
/// Use <see cref="DefaultAgentConfig.ExcludedTools"/> to hide specific tools from the default agent
1881+
/// while keeping them available to custom sub-agents.
1882+
/// </summary>
1883+
public DefaultAgentConfig? DefaultAgent { get; set; }
1884+
18621885
/// <summary>
18631886
/// Name of the custom agent to activate when the session starts.
18641887
/// Must match the <see cref="CustomAgentConfig.Name"/> of one of the agents in <see cref="CustomAgents"/>.
@@ -1938,6 +1961,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
19381961
Commands = other.Commands is not null ? [.. other.Commands] : null;
19391962
ConfigDir = other.ConfigDir;
19401963
CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null;
1964+
DefaultAgent = other.DefaultAgent;
19411965
Agent = other.Agent;
19421966
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
19431967
DisableResume = other.DisableResume;
@@ -2093,6 +2117,13 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
20932117
/// </summary>
20942118
public IList<CustomAgentConfig>? CustomAgents { get; set; }
20952119

2120+
/// <summary>
2121+
/// Configuration for the default agent (the built-in agent that handles turns when no custom agent is selected).
2122+
/// Use <see cref="DefaultAgentConfig.ExcludedTools"/> to hide specific tools from the default agent
2123+
/// while keeping them available to custom sub-agents.
2124+
/// </summary>
2125+
public DefaultAgentConfig? DefaultAgent { get; set; }
2126+
20962127
/// <summary>
20972128
/// Name of the custom agent to activate when the session starts.
20982129
/// 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
@@ -89,6 +89,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
8989
McpServers = new Dictionary<string, McpServerConfig> { ["server1"] = new McpStdioServerConfig { Command = "echo" } },
9090
CustomAgents = [new CustomAgentConfig { Name = "agent1" }],
9191
Agent = "agent1",
92+
DefaultAgent = new DefaultAgentConfig { ExcludedTools = ["hidden-tool"] },
9293
SkillDirectories = ["/skills"],
9394
DisabledSkills = ["skill1"],
9495
};
@@ -107,6 +108,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
107108
Assert.Equal(original.McpServers.Count, clone.McpServers!.Count);
108109
Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count);
109110
Assert.Equal(original.Agent, clone.Agent);
111+
Assert.Equal(original.DefaultAgent!.ExcludedTools, clone.DefaultAgent!.ExcludedTools);
110112
Assert.Equal(original.SkillDirectories, clone.SkillDirectories);
111113
Assert.Equal(original.DisabledSkills, clone.DisabledSkills);
112114
}
@@ -243,6 +245,7 @@ public void Clone_WithNullCollections_ReturnsNullCollections()
243245
Assert.Null(clone.SkillDirectories);
244246
Assert.Null(clone.DisabledSkills);
245247
Assert.Null(clone.Tools);
248+
Assert.Null(clone.DefaultAgent);
246249
}
247250

248251
[Fact]

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
@@ -766,6 +767,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
766767
req.MCPServers = config.MCPServers
767768
req.EnvValueMode = "direct"
768769
req.CustomAgents = config.CustomAgents
770+
req.DefaultAgent = config.DefaultAgent
769771
req.Agent = config.Agent
770772
req.SkillDirectories = config.SkillDirectories
771773
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)