Skip to content

Commit 7766b1a

Browse files
patnikoCopilot
andauthored
feat: add agent parameter to session creation for pre-selecting custom agents (#722)
Add an optional `agent` field to SessionConfig and ResumeSessionConfig across all four SDKs (Node.js, Python, Go, .NET) that allows specifying which custom agent should be active when the session starts. Previously, users had to create a session and then make a separate `session.rpc.agent.select()` call to activate a specific custom agent. This change allows setting the agent directly in the session config, equivalent to passing `--agent <name>` in the Copilot CLI. The `agent` value must match the `name` of one of the agents defined in `customAgents`. Changes: - Node.js: Added `agent?: string` to SessionConfig and ResumeSessionConfig, wired in client.ts for both session.create and session.resume RPC calls - Python: Added `agent: str` to SessionConfig and ResumeSessionConfig, wired in client.py for both create and resume payloads - Go: Added `Agent string` to SessionConfig and ResumeSessionConfig, wired in client.go for both request types - .NET: Added `Agent` property to SessionConfig and ResumeSessionConfig, updated copy constructors, CreateSessionRequest/ResumeSessionRequest records, and CreateSessionAsync/ResumeSessionAsync call sites - Docs: Added "Selecting an Agent at Session Creation" section with examples in all 4 languages to custom-agents.md, updated session-persistence.md and getting-started.md - Tests: Added unit tests verifying agent parameter is forwarded in both session.create and session.resume RPC calls Closes #317, closes #410, closes #547 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f4b0956 commit 7766b1a

File tree

15 files changed

+346
-0
lines changed

15 files changed

+346
-0
lines changed

docs/features/custom-agents.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,102 @@ await using var session = await client.CreateSessionAsync(new SessionConfig
219219

220220
> **Tip:** A good `description` helps the runtime match user intent to the right agent. Be specific about the agent's expertise and capabilities.
221221
222+
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.
223+
224+
| Session Config Property | Type | Description |
225+
|-------------------------|------|-------------|
226+
| `agent` | `string` | Name of the custom agent to pre-select at session creation. Must match a `name` in `customAgents`. |
227+
228+
## Selecting an Agent at Session Creation
229+
230+
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`.
231+
232+
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.
233+
234+
<details open>
235+
<summary><strong>Node.js / TypeScript</strong></summary>
236+
237+
<!-- docs-validate: skip -->
238+
```typescript
239+
const session = await client.createSession({
240+
customAgents: [
241+
{
242+
name: "researcher",
243+
prompt: "You are a research assistant. Analyze code and answer questions.",
244+
},
245+
{
246+
name: "editor",
247+
prompt: "You are a code editor. Make minimal, surgical changes.",
248+
},
249+
],
250+
agent: "researcher", // Pre-select the researcher agent
251+
});
252+
```
253+
254+
</details>
255+
256+
<details>
257+
<summary><strong>Python</strong></summary>
258+
259+
<!-- docs-validate: skip -->
260+
```python
261+
session = await client.create_session({
262+
"custom_agents": [
263+
{
264+
"name": "researcher",
265+
"prompt": "You are a research assistant. Analyze code and answer questions.",
266+
},
267+
{
268+
"name": "editor",
269+
"prompt": "You are a code editor. Make minimal, surgical changes.",
270+
},
271+
],
272+
"agent": "researcher", # Pre-select the researcher agent
273+
})
274+
```
275+
276+
</details>
277+
278+
<details>
279+
<summary><strong>Go</strong></summary>
280+
281+
<!-- docs-validate: skip -->
282+
```go
283+
session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
284+
CustomAgents: []copilot.CustomAgentConfig{
285+
{
286+
Name: "researcher",
287+
Prompt: "You are a research assistant. Analyze code and answer questions.",
288+
},
289+
{
290+
Name: "editor",
291+
Prompt: "You are a code editor. Make minimal, surgical changes.",
292+
},
293+
},
294+
Agent: "researcher", // Pre-select the researcher agent
295+
})
296+
```
297+
298+
</details>
299+
300+
<details>
301+
<summary><strong>.NET</strong></summary>
302+
303+
<!-- docs-validate: skip -->
304+
```csharp
305+
var session = await client.CreateSessionAsync(new SessionConfig
306+
{
307+
CustomAgents = new List<CustomAgentConfig>
308+
{
309+
new() { Name = "researcher", Prompt = "You are a research assistant. Analyze code and answer questions." },
310+
new() { Name = "editor", Prompt = "You are a code editor. Make minimal, surgical changes." },
311+
},
312+
Agent = "researcher", // Pre-select the researcher agent
313+
});
314+
```
315+
316+
</details>
317+
222318
## How Sub-Agent Delegation Works
223319

224320
When you send a prompt to a session with custom agents, the runtime evaluates whether to delegate to a sub-agent:

docs/features/session-persistence.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ When resuming a session, you can optionally reconfigure many settings. This is u
248248
| `configDir` | Override configuration directory |
249249
| `mcpServers` | Configure MCP servers |
250250
| `customAgents` | Configure custom agents |
251+
| `agent` | Pre-select a custom agent by name |
251252
| `skillDirectories` | Directories to load skills from |
252253
| `disabledSkills` | Skills to disable |
253254
| `infiniteSessions` | Configure infinite session behavior |

docs/getting-started.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,6 +1241,8 @@ const session = await client.createSession({
12411241
});
12421242
```
12431243

1244+
> **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.
1245+
12441246
### Customize the System Message
12451247

12461248
Control the AI's behavior and personality:

dotnet/src/Client.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
419419
config.McpServers,
420420
"direct",
421421
config.CustomAgents,
422+
config.Agent,
422423
config.ConfigDir,
423424
config.SkillDirectories,
424425
config.DisabledSkills,
@@ -512,6 +513,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
512513
config.McpServers,
513514
"direct",
514515
config.CustomAgents,
516+
config.Agent,
515517
config.SkillDirectories,
516518
config.DisabledSkills,
517519
config.InfiniteSessions);
@@ -1407,6 +1409,7 @@ internal record CreateSessionRequest(
14071409
Dictionary<string, object>? McpServers,
14081410
string? EnvValueMode,
14091411
List<CustomAgentConfig>? CustomAgents,
1412+
string? Agent,
14101413
string? ConfigDir,
14111414
List<string>? SkillDirectories,
14121415
List<string>? DisabledSkills,
@@ -1450,6 +1453,7 @@ internal record ResumeSessionRequest(
14501453
Dictionary<string, object>? McpServers,
14511454
string? EnvValueMode,
14521455
List<CustomAgentConfig>? CustomAgents,
1456+
string? Agent,
14531457
List<string>? SkillDirectories,
14541458
List<string>? DisabledSkills,
14551459
InfiniteSessionConfig? InfiniteSessions);

dotnet/src/Types.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,7 @@ protected SessionConfig(SessionConfig? other)
11971197
ClientName = other.ClientName;
11981198
ConfigDir = other.ConfigDir;
11991199
CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null;
1200+
Agent = other.Agent;
12001201
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
12011202
ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null;
12021203
Hooks = other.Hooks;
@@ -1307,6 +1308,12 @@ protected SessionConfig(SessionConfig? other)
13071308
/// </summary>
13081309
public List<CustomAgentConfig>? CustomAgents { get; set; }
13091310

1311+
/// <summary>
1312+
/// Name of the custom agent to activate when the session starts.
1313+
/// Must match the <see cref="CustomAgentConfig.Name"/> of one of the agents in <see cref="CustomAgents"/>.
1314+
/// </summary>
1315+
public string? Agent { get; set; }
1316+
13101317
/// <summary>
13111318
/// Directories to load skills from.
13121319
/// </summary>
@@ -1361,6 +1368,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
13611368
ClientName = other.ClientName;
13621369
ConfigDir = other.ConfigDir;
13631370
CustomAgents = other.CustomAgents is not null ? [.. other.CustomAgents] : null;
1371+
Agent = other.Agent;
13641372
DisabledSkills = other.DisabledSkills is not null ? [.. other.DisabledSkills] : null;
13651373
DisableResume = other.DisableResume;
13661374
ExcludedTools = other.ExcludedTools is not null ? [.. other.ExcludedTools] : null;
@@ -1476,6 +1484,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
14761484
/// </summary>
14771485
public List<CustomAgentConfig>? CustomAgents { get; set; }
14781486

1487+
/// <summary>
1488+
/// Name of the custom agent to activate when the session starts.
1489+
/// Must match the <see cref="CustomAgentConfig.Name"/> of one of the agents in <see cref="CustomAgents"/>.
1490+
/// </summary>
1491+
public string? Agent { get; set; }
1492+
14791493
/// <summary>
14801494
/// Directories to load skills from.
14811495
/// </summary>

dotnet/test/CloneTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
8888
Streaming = true,
8989
McpServers = new Dictionary<string, object> { ["server1"] = new object() },
9090
CustomAgents = [new CustomAgentConfig { Name = "agent1" }],
91+
Agent = "agent1",
9192
SkillDirectories = ["/skills"],
9293
DisabledSkills = ["skill1"],
9394
};
@@ -105,6 +106,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
105106
Assert.Equal(original.Streaming, clone.Streaming);
106107
Assert.Equal(original.McpServers.Count, clone.McpServers!.Count);
107108
Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count);
109+
Assert.Equal(original.Agent, clone.Agent);
108110
Assert.Equal(original.SkillDirectories, clone.SkillDirectories);
109111
Assert.Equal(original.DisabledSkills, clone.DisabledSkills);
110112
}
@@ -242,4 +244,32 @@ public void Clone_WithNullCollections_ReturnsNullCollections()
242244
Assert.Null(clone.DisabledSkills);
243245
Assert.Null(clone.Tools);
244246
}
247+
248+
[Fact]
249+
public void SessionConfig_Clone_CopiesAgentProperty()
250+
{
251+
var original = new SessionConfig
252+
{
253+
Agent = "test-agent",
254+
CustomAgents = [new CustomAgentConfig { Name = "test-agent", Prompt = "You are a test agent." }],
255+
};
256+
257+
var clone = original.Clone();
258+
259+
Assert.Equal("test-agent", clone.Agent);
260+
}
261+
262+
[Fact]
263+
public void ResumeSessionConfig_Clone_CopiesAgentProperty()
264+
{
265+
var original = new ResumeSessionConfig
266+
{
267+
Agent = "test-agent",
268+
CustomAgents = [new CustomAgentConfig { Name = "test-agent", Prompt = "You are a test agent." }],
269+
};
270+
271+
var clone = original.Clone();
272+
273+
Assert.Equal("test-agent", clone.Agent);
274+
}
245275
}

go/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
502502
req.MCPServers = config.MCPServers
503503
req.EnvValueMode = "direct"
504504
req.CustomAgents = config.CustomAgents
505+
req.Agent = config.Agent
505506
req.SkillDirectories = config.SkillDirectories
506507
req.DisabledSkills = config.DisabledSkills
507508
req.InfiniteSessions = config.InfiniteSessions
@@ -616,6 +617,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
616617
req.MCPServers = config.MCPServers
617618
req.EnvValueMode = "direct"
618619
req.CustomAgents = config.CustomAgents
620+
req.Agent = config.Agent
619621
req.SkillDirectories = config.SkillDirectories
620622
req.DisabledSkills = config.DisabledSkills
621623
req.InfiniteSessions = config.InfiniteSessions

go/client_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,60 @@ func TestResumeSessionRequest_ClientName(t *testing.T) {
413413
})
414414
}
415415

416+
func TestCreateSessionRequest_Agent(t *testing.T) {
417+
t.Run("includes agent in JSON when set", func(t *testing.T) {
418+
req := createSessionRequest{Agent: "test-agent"}
419+
data, err := json.Marshal(req)
420+
if err != nil {
421+
t.Fatalf("Failed to marshal: %v", err)
422+
}
423+
var m map[string]any
424+
if err := json.Unmarshal(data, &m); err != nil {
425+
t.Fatalf("Failed to unmarshal: %v", err)
426+
}
427+
if m["agent"] != "test-agent" {
428+
t.Errorf("Expected agent to be 'test-agent', got %v", m["agent"])
429+
}
430+
})
431+
432+
t.Run("omits agent from JSON when empty", func(t *testing.T) {
433+
req := createSessionRequest{}
434+
data, _ := json.Marshal(req)
435+
var m map[string]any
436+
json.Unmarshal(data, &m)
437+
if _, ok := m["agent"]; ok {
438+
t.Error("Expected agent to be omitted when empty")
439+
}
440+
})
441+
}
442+
443+
func TestResumeSessionRequest_Agent(t *testing.T) {
444+
t.Run("includes agent in JSON when set", func(t *testing.T) {
445+
req := resumeSessionRequest{SessionID: "s1", Agent: "test-agent"}
446+
data, err := json.Marshal(req)
447+
if err != nil {
448+
t.Fatalf("Failed to marshal: %v", err)
449+
}
450+
var m map[string]any
451+
if err := json.Unmarshal(data, &m); err != nil {
452+
t.Fatalf("Failed to unmarshal: %v", err)
453+
}
454+
if m["agent"] != "test-agent" {
455+
t.Errorf("Expected agent to be 'test-agent', got %v", m["agent"])
456+
}
457+
})
458+
459+
t.Run("omits agent from JSON when empty", func(t *testing.T) {
460+
req := resumeSessionRequest{SessionID: "s1"}
461+
data, _ := json.Marshal(req)
462+
var m map[string]any
463+
json.Unmarshal(data, &m)
464+
if _, ok := m["agent"]; ok {
465+
t.Error("Expected agent to be omitted when empty")
466+
}
467+
})
468+
}
469+
416470
func TestOverridesBuiltInTool(t *testing.T) {
417471
t.Run("OverridesBuiltInTool is serialized in tool definition", func(t *testing.T) {
418472
tool := Tool{

go/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,9 @@ type SessionConfig struct {
384384
MCPServers map[string]MCPServerConfig
385385
// CustomAgents configures custom agents for the session
386386
CustomAgents []CustomAgentConfig
387+
// Agent is the name of the custom agent to activate when the session starts.
388+
// Must match the Name of one of the agents in CustomAgents.
389+
Agent string
387390
// SkillDirectories is a list of directories to load skills from
388391
SkillDirectories []string
389392
// DisabledSkills is a list of skill names to disable
@@ -467,6 +470,9 @@ type ResumeSessionConfig struct {
467470
MCPServers map[string]MCPServerConfig
468471
// CustomAgents configures custom agents for the session
469472
CustomAgents []CustomAgentConfig
473+
// Agent is the name of the custom agent to activate when the session starts.
474+
// Must match the Name of one of the agents in CustomAgents.
475+
Agent string
470476
// SkillDirectories is a list of directories to load skills from
471477
SkillDirectories []string
472478
// DisabledSkills is a list of skill names to disable
@@ -652,6 +658,7 @@ type createSessionRequest struct {
652658
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
653659
EnvValueMode string `json:"envValueMode,omitempty"`
654660
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
661+
Agent string `json:"agent,omitempty"`
655662
ConfigDir string `json:"configDir,omitempty"`
656663
SkillDirectories []string `json:"skillDirectories,omitempty"`
657664
DisabledSkills []string `json:"disabledSkills,omitempty"`
@@ -685,6 +692,7 @@ type resumeSessionRequest struct {
685692
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
686693
EnvValueMode string `json:"envValueMode,omitempty"`
687694
CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"`
695+
Agent string `json:"agent,omitempty"`
688696
SkillDirectories []string `json:"skillDirectories,omitempty"`
689697
DisabledSkills []string `json:"disabledSkills,omitempty"`
690698
InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"`

nodejs/src/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ export class CopilotClient {
567567
mcpServers: config.mcpServers,
568568
envValueMode: "direct",
569569
customAgents: config.customAgents,
570+
agent: config.agent,
570571
configDir: config.configDir,
571572
skillDirectories: config.skillDirectories,
572573
disabledSkills: config.disabledSkills,
@@ -654,6 +655,7 @@ export class CopilotClient {
654655
mcpServers: config.mcpServers,
655656
envValueMode: "direct",
656657
customAgents: config.customAgents,
658+
agent: config.agent,
657659
skillDirectories: config.skillDirectories,
658660
disabledSkills: config.disabledSkills,
659661
infiniteSessions: config.infiniteSessions,

0 commit comments

Comments
 (0)