Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions docs/features/custom-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<details open>
<summary><strong>Node.js / TypeScript</strong></summary>

<!-- docs-validate: skip -->
```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
});
```

</details>

<details>
<summary><strong>Python</strong></summary>

<!-- docs-validate: skip -->
```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
})
```

</details>

<details>
<summary><strong>Go</strong></summary>

<!-- docs-validate: skip -->
```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
})
```

</details>

<details>
<summary><strong>.NET</strong></summary>

<!-- docs-validate: skip -->
```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
CustomAgents = new List<CustomAgentConfig>
{
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
});
```

</details>

## 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:
Expand Down
1 change: 1 addition & 0 deletions docs/features/session-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 2 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
config.McpServers,
"direct",
config.CustomAgents,
config.Agent,
config.ConfigDir,
config.SkillDirectories,
config.DisabledSkills,
Expand Down Expand Up @@ -512,6 +513,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
config.McpServers,
"direct",
config.CustomAgents,
config.Agent,
config.SkillDirectories,
config.DisabledSkills,
config.InfiniteSessions);
Expand Down Expand Up @@ -1407,6 +1409,7 @@ internal record CreateSessionRequest(
Dictionary<string, object>? McpServers,
string? EnvValueMode,
List<CustomAgentConfig>? CustomAgents,
string? Agent,
string? ConfigDir,
List<string>? SkillDirectories,
List<string>? DisabledSkills,
Expand Down Expand Up @@ -1450,6 +1453,7 @@ internal record ResumeSessionRequest(
Dictionary<string, object>? McpServers,
string? EnvValueMode,
List<CustomAgentConfig>? CustomAgents,
string? Agent,
List<string>? SkillDirectories,
List<string>? DisabledSkills,
InfiniteSessionConfig? InfiniteSessions);
Expand Down
14 changes: 14 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1307,6 +1308,12 @@ protected SessionConfig(SessionConfig? other)
/// </summary>
public List<CustomAgentConfig>? CustomAgents { get; set; }

/// <summary>
/// Name of the custom agent to activate when the session starts.
/// Must match the <see cref="CustomAgentConfig.Name"/> of one of the agents in <see cref="CustomAgents"/>.
/// </summary>
public string? Agent { get; set; }

/// <summary>
/// Directories to load skills from.
/// </summary>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1476,6 +1484,12 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
/// </summary>
public List<CustomAgentConfig>? CustomAgents { get; set; }

/// <summary>
/// Name of the custom agent to activate when the session starts.
/// Must match the <see cref="CustomAgentConfig.Name"/> of one of the agents in <see cref="CustomAgents"/>.
/// </summary>
public string? Agent { get; set; }

/// <summary>
/// Directories to load skills from.
/// </summary>
Expand Down
30 changes: 30 additions & 0 deletions dotnet/test/CloneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
Streaming = true,
McpServers = new Dictionary<string, object> { ["server1"] = new object() },
CustomAgents = [new CustomAgentConfig { Name = "agent1" }],
Agent = "agent1",
SkillDirectories = ["/skills"],
DisabledSkills = ["skill1"],
};
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
}
2 changes: 2 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
8 changes: 8 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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"`
Expand Down
2 changes: 2 additions & 0 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -764,6 +771,7 @@ export type ResumeSessionConfig = Pick<
| "configDir"
| "mcpServers"
| "customAgents"
| "agent"
| "skillDirectories"
| "disabledSkills"
| "infiniteSessions"
Expand Down
Loading
Loading