diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md
index de642e19..f9c1a373 100644
--- a/docs/features/custom-agents.md
+++ b/docs/features/custom-agents.md
@@ -219,6 +219,102 @@ await using var session = await client.CreateSessionAsync(new SessionConfig
> **Tip:** A good `description` helps the runtime match user intent to the right agent. Be specific about the agent's expertise and capabilities.
+In addition to per-agent configuration above, you can set `agent` on the **session config** itself to pre-select which custom agent is active when the session starts. See [Selecting an Agent at Session Creation](#selecting-an-agent-at-session-creation) below.
+
+| Session Config Property | Type | Description |
+|-------------------------|------|-------------|
+| `agent` | `string` | Name of the custom agent to pre-select at session creation. Must match a `name` in `customAgents`. |
+
+## Selecting an Agent at Session Creation
+
+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`.
+
+This is equivalent to calling `session.rpc.agent.select()` after creation, but avoids the extra API call and ensures the agent is active from the very first prompt.
+
+
+Node.js / TypeScript
+
+
+```typescript
+const session = await client.createSession({
+ customAgents: [
+ {
+ name: "researcher",
+ prompt: "You are a research assistant. Analyze code and answer questions.",
+ },
+ {
+ name: "editor",
+ prompt: "You are a code editor. Make minimal, surgical changes.",
+ },
+ ],
+ agent: "researcher", // Pre-select the researcher agent
+});
+```
+
+
+
+
+Python
+
+
+```python
+session = await client.create_session({
+ "custom_agents": [
+ {
+ "name": "researcher",
+ "prompt": "You are a research assistant. Analyze code and answer questions.",
+ },
+ {
+ "name": "editor",
+ "prompt": "You are a code editor. Make minimal, surgical changes.",
+ },
+ ],
+ "agent": "researcher", # Pre-select the researcher agent
+})
+```
+
+
+
+
+Go
+
+
+```go
+session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
+ CustomAgents: []copilot.CustomAgentConfig{
+ {
+ Name: "researcher",
+ Prompt: "You are a research assistant. Analyze code and answer questions.",
+ },
+ {
+ Name: "editor",
+ Prompt: "You are a code editor. Make minimal, surgical changes.",
+ },
+ },
+ Agent: "researcher", // Pre-select the researcher agent
+})
+```
+
+
+
+
+.NET
+
+
+```csharp
+var session = await client.CreateSessionAsync(new SessionConfig
+{
+ CustomAgents = new List
+ {
+ new() { Name = "researcher", Prompt = "You are a research assistant. Analyze code and answer questions." },
+ new() { Name = "editor", Prompt = "You are a code editor. Make minimal, surgical changes." },
+ },
+ Agent = "researcher", // Pre-select the researcher agent
+});
+```
+
+
+
## How Sub-Agent Delegation Works
When you send a prompt to a session with custom agents, the runtime evaluates whether to delegate to a sub-agent:
diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md
index 7f3759df..59a5d9d5 100644
--- a/docs/features/session-persistence.md
+++ b/docs/features/session-persistence.md
@@ -248,6 +248,7 @@ When resuming a session, you can optionally reconfigure many settings. This is u
| `configDir` | Override configuration directory |
| `mcpServers` | Configure MCP servers |
| `customAgents` | Configure custom agents |
+| `agent` | Pre-select a custom agent by name |
| `skillDirectories` | Directories to load skills from |
| `disabledSkills` | Skills to disable |
| `infiniteSessions` | Configure infinite session behavior |
diff --git a/docs/getting-started.md b/docs/getting-started.md
index de95a027..fe952182 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -1241,6 +1241,8 @@ const session = await client.createSession({
});
```
+> **Tip:** You can also set `agent: "pr-reviewer"` in the session config to pre-select this agent from the start. See the [Custom Agents guide](./guides/custom-agents.md#selecting-an-agent-at-session-creation) for details.
+
### Customize the System Message
Control the AI's behavior and personality:
diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs
index 8cad6b04..91b6353f 100644
--- a/dotnet/src/Client.cs
+++ b/dotnet/src/Client.cs
@@ -419,6 +419,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance
config.McpServers,
"direct",
config.CustomAgents,
+ config.Agent,
config.ConfigDir,
config.SkillDirectories,
config.DisabledSkills,
@@ -512,6 +513,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes
config.McpServers,
"direct",
config.CustomAgents,
+ config.Agent,
config.SkillDirectories,
config.DisabledSkills,
config.InfiniteSessions);
@@ -1407,6 +1409,7 @@ internal record CreateSessionRequest(
Dictionary? McpServers,
string? EnvValueMode,
List? CustomAgents,
+ string? Agent,
string? ConfigDir,
List? SkillDirectories,
List? DisabledSkills,
@@ -1450,6 +1453,7 @@ internal record ResumeSessionRequest(
Dictionary? McpServers,
string? EnvValueMode,
List? CustomAgents,
+ string? Agent,
List? SkillDirectories,
List? DisabledSkills,
InfiniteSessionConfig? InfiniteSessions);
diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs
index dbee05cf..52d870b8 100644
--- a/dotnet/src/Types.cs
+++ b/dotnet/src/Types.cs
@@ -1197,6 +1197,7 @@ protected SessionConfig(SessionConfig? other)
ClientName = other.ClientName;
ConfigDir = other.ConfigDir;
CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null;
+ Agent = other.Agent;
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null;
Hooks = other.Hooks;
@@ -1307,6 +1308,12 @@ protected SessionConfig(SessionConfig? other)
///
public List? CustomAgents { get; set; }
+ ///
+ /// Name of the custom agent to activate when the session starts.
+ /// Must match the of one of the agents in .
+ ///
+ public string? Agent { get; set; }
+
///
/// Directories to load skills from.
///
@@ -1361,6 +1368,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
ClientName = other.ClientName;
ConfigDir = other.ConfigDir;
CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null;
+ Agent = other.Agent;
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
DisableResume = other.DisableResume;
ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null;
@@ -1476,6 +1484,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
///
public List? CustomAgents { get; set; }
+ ///
+ /// Name of the custom agent to activate when the session starts.
+ /// Must match the of one of the agents in .
+ ///
+ public string? Agent { get; set; }
+
///
/// Directories to load skills from.
///
diff --git a/dotnet/test/CloneTests.cs b/dotnet/test/CloneTests.cs
index 8982c5d6..cc6e5ad5 100644
--- a/dotnet/test/CloneTests.cs
+++ b/dotnet/test/CloneTests.cs
@@ -88,6 +88,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
Streaming = true,
McpServers = new Dictionary { ["server1"] = new object() },
CustomAgents = [new CustomAgentConfig { Name = "agent1" }],
+ Agent = "agent1",
SkillDirectories = ["/skills"],
DisabledSkills = ["skill1"],
};
@@ -105,6 +106,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
Assert.Equal(original.Streaming, clone.Streaming);
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.SkillDirectories, clone.SkillDirectories);
Assert.Equal(original.DisabledSkills, clone.DisabledSkills);
}
@@ -242,4 +244,32 @@ public void Clone_WithNullCollections_ReturnsNullCollections()
Assert.Null(clone.DisabledSkills);
Assert.Null(clone.Tools);
}
+
+ [Fact]
+ public void SessionConfig_Clone_CopiesAgentProperty()
+ {
+ var original = new SessionConfig
+ {
+ Agent = "test-agent",
+ CustomAgents = [new CustomAgentConfig { Name = "test-agent", Prompt = "You are a test agent." }],
+ };
+
+ var clone = original.Clone();
+
+ Assert.Equal("test-agent", clone.Agent);
+ }
+
+ [Fact]
+ public void ResumeSessionConfig_Clone_CopiesAgentProperty()
+ {
+ var original = new ResumeSessionConfig
+ {
+ Agent = "test-agent",
+ CustomAgents = [new CustomAgentConfig { Name = "test-agent", Prompt = "You are a test agent." }],
+ };
+
+ var clone = original.Clone();
+
+ Assert.Equal("test-agent", clone.Agent);
+ }
}
diff --git a/go/client.go b/go/client.go
index a43530ad..3c1fb28c 100644
--- a/go/client.go
+++ b/go/client.go
@@ -502,6 +502,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
req.MCPServers = config.MCPServers
req.EnvValueMode = "direct"
req.CustomAgents = config.CustomAgents
+ req.Agent = config.Agent
req.SkillDirectories = config.SkillDirectories
req.DisabledSkills = config.DisabledSkills
req.InfiniteSessions = config.InfiniteSessions
@@ -616,6 +617,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
req.MCPServers = config.MCPServers
req.EnvValueMode = "direct"
req.CustomAgents = config.CustomAgents
+ req.Agent = config.Agent
req.SkillDirectories = config.SkillDirectories
req.DisabledSkills = config.DisabledSkills
req.InfiniteSessions = config.InfiniteSessions
diff --git a/go/client_test.go b/go/client_test.go
index d740fd79..76efe98b 100644
--- a/go/client_test.go
+++ b/go/client_test.go
@@ -413,6 +413,60 @@ func TestResumeSessionRequest_ClientName(t *testing.T) {
})
}
+func TestCreateSessionRequest_Agent(t *testing.T) {
+ t.Run("includes agent in JSON when set", func(t *testing.T) {
+ req := createSessionRequest{Agent: "test-agent"}
+ data, err := json.Marshal(req)
+ if err != nil {
+ t.Fatalf("Failed to marshal: %v", err)
+ }
+ var m map[string]any
+ if err := json.Unmarshal(data, &m); err != nil {
+ t.Fatalf("Failed to unmarshal: %v", err)
+ }
+ if m["agent"] != "test-agent" {
+ t.Errorf("Expected agent to be 'test-agent', got %v", m["agent"])
+ }
+ })
+
+ t.Run("omits agent from JSON when empty", func(t *testing.T) {
+ req := createSessionRequest{}
+ data, _ := json.Marshal(req)
+ var m map[string]any
+ json.Unmarshal(data, &m)
+ if _, ok := m["agent"]; ok {
+ t.Error("Expected agent to be omitted when empty")
+ }
+ })
+}
+
+func TestResumeSessionRequest_Agent(t *testing.T) {
+ t.Run("includes agent in JSON when set", func(t *testing.T) {
+ req := resumeSessionRequest{SessionID: "s1", Agent: "test-agent"}
+ data, err := json.Marshal(req)
+ if err != nil {
+ t.Fatalf("Failed to marshal: %v", err)
+ }
+ var m map[string]any
+ if err := json.Unmarshal(data, &m); err != nil {
+ t.Fatalf("Failed to unmarshal: %v", err)
+ }
+ if m["agent"] != "test-agent" {
+ t.Errorf("Expected agent to be 'test-agent', got %v", m["agent"])
+ }
+ })
+
+ t.Run("omits agent from JSON when empty", func(t *testing.T) {
+ req := resumeSessionRequest{SessionID: "s1"}
+ data, _ := json.Marshal(req)
+ var m map[string]any
+ json.Unmarshal(data, &m)
+ if _, ok := m["agent"]; ok {
+ t.Error("Expected agent to be omitted when empty")
+ }
+ })
+}
+
func TestOverridesBuiltInTool(t *testing.T) {
t.Run("OverridesBuiltInTool is serialized in tool definition", func(t *testing.T) {
tool := Tool{
diff --git a/go/types.go b/go/types.go
index d749de74..7970b2fe 100644
--- a/go/types.go
+++ b/go/types.go
@@ -384,6 +384,9 @@ type SessionConfig struct {
MCPServers map[string]MCPServerConfig
// CustomAgents configures custom agents for the session
CustomAgents []CustomAgentConfig
+ // 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
// SkillDirectories is a list of directories to load skills from
SkillDirectories []string
// DisabledSkills is a list of skill names to disable
@@ -467,6 +470,9 @@ type ResumeSessionConfig struct {
MCPServers map[string]MCPServerConfig
// CustomAgents configures custom agents for the session
CustomAgents []CustomAgentConfig
+ // 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
// SkillDirectories is a list of directories to load skills from
SkillDirectories []string
// DisabledSkills is a list of skill names to disable
@@ -652,6 +658,7 @@ type createSessionRequest struct {
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
EnvValueMode string `json:"envValueMode,omitempty"`
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
+ Agent string `json:"agent,omitempty"`
ConfigDir string `json:"configDir,omitempty"`
SkillDirectories []string `json:"skillDirectories,omitempty"`
DisabledSkills []string `json:"disabledSkills,omitempty"`
@@ -685,6 +692,7 @@ type resumeSessionRequest struct {
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
EnvValueMode string `json:"envValueMode,omitempty"`
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
+ Agent string `json:"agent,omitempty"`
SkillDirectories []string `json:"skillDirectories,omitempty"`
DisabledSkills []string `json:"disabledSkills,omitempty"`
InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"`
diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts
index de5f1856..1108edae 100644
--- a/nodejs/src/client.ts
+++ b/nodejs/src/client.ts
@@ -567,6 +567,7 @@ export class CopilotClient {
mcpServers: config.mcpServers,
envValueMode: "direct",
customAgents: config.customAgents,
+ agent: config.agent,
configDir: config.configDir,
skillDirectories: config.skillDirectories,
disabledSkills: config.disabledSkills,
@@ -654,6 +655,7 @@ export class CopilotClient {
mcpServers: config.mcpServers,
envValueMode: "direct",
customAgents: config.customAgents,
+ agent: config.agent,
skillDirectories: config.skillDirectories,
disabledSkills: config.disabledSkills,
infiniteSessions: config.infiniteSessions,
diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts
index 7eef9409..acda50fe 100644
--- a/nodejs/src/types.ts
+++ b/nodejs/src/types.ts
@@ -725,6 +725,13 @@ export interface SessionConfig {
*/
customAgents?: CustomAgentConfig[];
+ /**
+ * Name of the custom agent to activate when the session starts.
+ * Must match the `name` of one of the agents in `customAgents`.
+ * Equivalent to calling `session.rpc.agent.select({ name })` after creation.
+ */
+ agent?: string;
+
/**
* Directories to load skills from.
*/
@@ -764,6 +771,7 @@ export type ResumeSessionConfig = Pick<
| "configDir"
| "mcpServers"
| "customAgents"
+ | "agent"
| "skillDirectories"
| "disabledSkills"
| "infiniteSessions"
diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts
index b7dd3439..22f96999 100644
--- a/nodejs/test/client.test.ts
+++ b/nodejs/test/client.test.ts
@@ -336,4 +336,56 @@ describe("CopilotClient", () => {
spy.mockRestore();
});
});
+
+ describe("agent parameter in session creation", () => {
+ it("forwards agent 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({
+ onPermissionRequest: approveAll,
+ customAgents: [
+ {
+ name: "test-agent",
+ prompt: "You are a test agent.",
+ },
+ ],
+ agent: "test-agent",
+ });
+
+ const payload = spy.mock.calls.find((c) => c[0] === "session.create")![1] as any;
+ expect(payload.agent).toBe("test-agent");
+ expect(payload.customAgents).toEqual([expect.objectContaining({ name: "test-agent" })]);
+ });
+
+ it("forwards agent 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")
+ .mockImplementation(async (method: string, params: any) => {
+ if (method === "session.resume") return { sessionId: params.sessionId };
+ throw new Error(`Unexpected method: ${method}`);
+ });
+ await client.resumeSession(session.sessionId, {
+ onPermissionRequest: approveAll,
+ customAgents: [
+ {
+ name: "test-agent",
+ prompt: "You are a test agent.",
+ },
+ ],
+ agent: "test-agent",
+ });
+
+ const payload = spy.mock.calls.find((c) => c[0] === "session.resume")![1] as any;
+ expect(payload.agent).toBe("test-agent");
+ spy.mockRestore();
+ });
+ });
});
diff --git a/python/copilot/client.py b/python/copilot/client.py
index 7ea4e97a..c29f35d1 100644
--- a/python/copilot/client.py
+++ b/python/copilot/client.py
@@ -569,6 +569,11 @@ async def create_session(self, config: SessionConfig) -> CopilotSession:
self._convert_custom_agent_to_wire_format(agent) for agent in custom_agents
]
+ # Add agent selection if provided
+ agent = cfg.get("agent")
+ if agent:
+ payload["agent"] = agent
+
# Add config directory override if provided
config_dir = cfg.get("config_dir")
if config_dir:
@@ -758,6 +763,11 @@ async def resume_session(self, session_id: str, config: ResumeSessionConfig) ->
self._convert_custom_agent_to_wire_format(agent) for agent in custom_agents
]
+ # Add agent selection if provided
+ agent = cfg.get("agent")
+ if agent:
+ payload["agent"] = agent
+
# Add skill directories configuration if provided
skill_directories = cfg.get("skill_directories")
if skill_directories:
diff --git a/python/copilot/types.py b/python/copilot/types.py
index 6c484ce4..f094666c 100644
--- a/python/copilot/types.py
+++ b/python/copilot/types.py
@@ -507,6 +507,9 @@ class SessionConfig(TypedDict, total=False):
mcp_servers: dict[str, MCPServerConfig]
# Custom agent configurations for the session
custom_agents: list[CustomAgentConfig]
+ # 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
# Override the default configuration directory location.
# When specified, the session will use this directory for storing config and state.
config_dir: str
@@ -575,6 +578,9 @@ class ResumeSessionConfig(TypedDict, total=False):
mcp_servers: dict[str, MCPServerConfig]
# Custom agent configurations for the session
custom_agents: list[CustomAgentConfig]
+ # 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
# Directories to load skills from
skill_directories: list[str]
# List of skill names to disable
diff --git a/python/test_client.py b/python/test_client.py
index bcc249f3..ef068b7a 100644
--- a/python/test_client.py
+++ b/python/test_client.py
@@ -265,6 +265,63 @@ async def mock_request(method, params):
finally:
await client.force_stop()
+ @pytest.mark.asyncio
+ async def test_create_session_forwards_agent(self):
+ client = CopilotClient({"cli_path": CLI_PATH})
+ await client.start()
+
+ try:
+ captured = {}
+ original_request = client._client.request
+
+ async def mock_request(method, params):
+ captured[method] = params
+ return await original_request(method, params)
+
+ client._client.request = mock_request
+ await client.create_session(
+ {
+ "agent": "test-agent",
+ "custom_agents": [{"name": "test-agent", "prompt": "You are a test agent."}],
+ "on_permission_request": PermissionHandler.approve_all,
+ }
+ )
+ assert captured["session.create"]["agent"] == "test-agent"
+ finally:
+ await client.force_stop()
+
+ @pytest.mark.asyncio
+ async def test_resume_session_forwards_agent(self):
+ client = CopilotClient({"cli_path": CLI_PATH})
+ await client.start()
+
+ try:
+ session = await client.create_session(
+ {"on_permission_request": PermissionHandler.approve_all}
+ )
+
+ captured = {}
+ original_request = client._client.request
+
+ async def mock_request(method, params):
+ captured[method] = params
+ if method == "session.resume":
+ return {"sessionId": session.session_id}
+ return await original_request(method, params)
+
+ client._client.request = mock_request
+ await client.resume_session(
+ session.session_id,
+ {
+ "agent": "test-agent",
+ "custom_agents": [{"name": "test-agent", "prompt": "You are a test agent."}],
+ "on_permission_request": PermissionHandler.approve_all,
+ },
+ )
+ assert captured["session.resume"]["agent"] == "test-agent"
+ finally:
+ await client.force_stop()
+
@pytest.mark.asyncio
async def test_set_model_sends_correct_rpc(self):
client = CopilotClient({"cli_path": CLI_PATH})