Skip to content
Draft
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
28 changes: 28 additions & 0 deletions docs/features/custom-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ try (var client = new CopilotClient()) {
| `prompt` | `string` | ✅ | System prompt for the agent |
| `mcpServers` | `object` | | MCP server configurations specific to this agent |
| `infer` | `boolean` | | Whether the runtime can auto-select this agent (default: `true`) |
| `skills` | `string[]` | | Skill names to preload into the agent's context at startup |

> **Tip:** A good `description` helps the runtime match user intent to the right agent. Be specific about the agent's expertise and capabilities.

Expand All @@ -261,6 +262,33 @@ In addition to per-agent configuration above, you can set `agent` on the **sessi
|-------------------------|------|-------------|
| `agent` | `string` | Name of the custom agent to pre-select at session creation. Must match a `name` in `customAgents`. |

## Per-Agent Skills

You can preload skills into an agent's context using the `skills` property. When specified, the **full content** of each listed skill is eagerly injected into the agent's context at startup — the agent doesn't need to invoke a skill tool; the instructions are already present. Skills are **opt-in**: agents receive no skills by default, and sub-agents do not inherit skills from the parent. Skill names are resolved from the session-level `skillDirectories`.

```typescript
const session = await client.createSession({
skillDirectories: ["./skills"],
customAgents: [
{
name: "security-auditor",
description: "Security-focused code reviewer",
prompt: "Focus on OWASP Top 10 vulnerabilities",
skills: ["security-scan", "dependency-check"],
},
{
name: "docs-writer",
description: "Technical documentation writer",
prompt: "Write clear, concise documentation",
skills: ["markdown-lint"],
},
],
onPermissionRequest: async () => ({ kind: "approved" }),
});
```

In this example, `security-auditor` starts with `security-scan` and `dependency-check` already injected into its context, while `docs-writer` starts with `markdown-lint`. An agent without a `skills` field receives no skill content.

## 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`.
Expand Down
7 changes: 5 additions & 2 deletions docs/features/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,20 +364,23 @@ The markdown body contains the instructions that are injected into the session c

### Skills + Custom Agents

Skills work alongside custom agents:
Skills listed in an agent's `skills` field are **eagerly preloaded** — their full content is injected into the agent's context at startup, so the agent has access to the skill instructions immediately without needing to invoke a skill tool. Skill names are resolved from the session-level `skillDirectories`.

```typescript
const session = await client.createSession({
skillDirectories: ["./skills/security"],
skillDirectories: ["./skills"],
customAgents: [{
name: "security-auditor",
description: "Security-focused code reviewer",
prompt: "Focus on OWASP Top 10 vulnerabilities",
skills: ["security-scan", "dependency-check"],
}],
onPermissionRequest: async () => ({ kind: "approved" }),
});
```

> **Note:** Skills are opt-in — when `skills` is omitted, no skill content is injected. Sub-agents do not inherit skills from the parent; you must list them explicitly per agent.

### Skills + MCP Servers

Skills can complement MCP server capabilities:
Expand Down
10 changes: 10 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1546,6 +1546,16 @@ public class CustomAgentConfig
/// </summary>
[JsonPropertyName("infer")]
public bool? Infer { get; set; }

/// <summary>
/// List of skill names to preload into this agent's context.
/// When set, the full content of each listed skill is eagerly injected into
/// the agent's context at startup. Skills are resolved by name from the
/// session's configured skill directories (skillDirectories).
/// When omitted, no skills are injected (opt-in model).
/// </summary>
[JsonPropertyName("skills")]
public List<string>? Skills { get; set; }
}

/// <summary>
Expand Down
63 changes: 63 additions & 0 deletions dotnet/test/SkillsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,69 @@ public async Task Should_Not_Apply_Skill_When_Disabled_Via_DisabledSkills()
await session.DisposeAsync();
}

[Fact]
public async Task Should_Allow_Agent_With_Skills_To_Invoke_Skill()
{
var skillsDir = CreateSkillDir();
var customAgents = new List<CustomAgentConfig>
{
new CustomAgentConfig
{
Name = "skill-agent",
Description = "An agent with access to test-skill",
Prompt = "You are a helpful test agent.",
Skills = ["test-skill"]
}
};

var session = await CreateSessionAsync(new SessionConfig
{
SkillDirectories = [skillsDir],
CustomAgents = customAgents,
Agent = "skill-agent"
});

Assert.Matches(@"^[a-f0-9-]+$", session.SessionId);

// The agent has Skills = ["test-skill"], so the skill content is preloaded into its context
var message = await session.SendAndWaitAsync(new MessageOptions { Prompt = "Say hello briefly using the test skill." });
Assert.NotNull(message);
Assert.Contains(SkillMarker, message!.Data.Content);

await session.DisposeAsync();
}

[Fact]
public async Task Should_Not_Provide_Skills_To_Agent_Without_Skills_Field()
{
var skillsDir = CreateSkillDir();
var customAgents = new List<CustomAgentConfig>
{
new CustomAgentConfig
{
Name = "no-skill-agent",
Description = "An agent without skills access",
Prompt = "You are a helpful test agent."
}
};

var session = await CreateSessionAsync(new SessionConfig
{
SkillDirectories = [skillsDir],
CustomAgents = customAgents,
Agent = "no-skill-agent"
});

Assert.Matches(@"^[a-f0-9-]+$", session.SessionId);

// The agent has no Skills field, so no skill content is injected
var message = await session.SendAndWaitAsync(new MessageOptions { Prompt = "Say hello briefly using the test skill." });
Assert.NotNull(message);
Assert.DoesNotContain(SkillMarker, message!.Data.Content);

await session.DisposeAsync();
}

[Fact(Skip = "See the big comment around the equivalent test in the Node SDK. Skipped because the feature doesn't work correctly yet.")]
public async Task Should_Apply_Skill_On_Session_Resume_With_SkillDirectories()
{
Expand Down
77 changes: 77 additions & 0 deletions go/internal/e2e/skills_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,83 @@ func TestSkills(t *testing.T) {
session.Disconnect()
})

t.Run("should allow agent with skills to invoke skill", func(t *testing.T) {
ctx.ConfigureForTest(t)
cleanSkillsDir(t, ctx.WorkDir)
skillsDir := createTestSkillDir(t, ctx.WorkDir, skillMarker)

customAgents := []copilot.CustomAgentConfig{
{
Name: "skill-agent",
Description: "An agent with access to test-skill",
Prompt: "You are a helpful test agent.",
Skills: []string{"test-skill"},
},
}

session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
SkillDirectories: []string{skillsDir},
CustomAgents: customAgents,
Agent: "skill-agent",
})
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}

// The agent has Skills: ["test-skill"], so the skill content is preloaded into its context
message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
Prompt: "Say hello briefly using the test skill.",
})
if err != nil {
t.Fatalf("Failed to send message: %v", err)
}

if md, ok := message.Data.(*copilot.AssistantMessageData); !ok || !strings.Contains(md.Content, skillMarker) {
t.Errorf("Expected message to contain skill marker '%s', got: %v", skillMarker, message.Data)
}

session.Disconnect()
})

t.Run("should not provide skills to agent without skills field", func(t *testing.T) {
ctx.ConfigureForTest(t)
cleanSkillsDir(t, ctx.WorkDir)
skillsDir := createTestSkillDir(t, ctx.WorkDir, skillMarker)

customAgents := []copilot.CustomAgentConfig{
{
Name: "no-skill-agent",
Description: "An agent without skills access",
Prompt: "You are a helpful test agent.",
},
}

session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
SkillDirectories: []string{skillsDir},
CustomAgents: customAgents,
Agent: "no-skill-agent",
})
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}

// The agent has no Skills field, so no skill content is injected
message, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
Prompt: "Say hello briefly using the test skill.",
})
if err != nil {
t.Fatalf("Failed to send message: %v", err)
}

if md, ok := message.Data.(*copilot.AssistantMessageData); ok && strings.Contains(md.Content, skillMarker) {
t.Errorf("Expected message to NOT contain skill marker '%s' when agent has no skills, got: %v", skillMarker, md.Content)
}

session.Disconnect()
})

t.Run("should apply skill on session resume with skillDirectories", func(t *testing.T) {
t.Skip("See the big comment around the equivalent test in the Node SDK. Skipped because the feature doesn't work correctly yet.")
ctx.ConfigureForTest(t)
Expand Down
2 changes: 2 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,8 @@ type CustomAgentConfig struct {
MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"`
// Infer indicates whether the agent should be available for model inference
Infer *bool `json:"infer,omitempty"`
// Skills is the list of skill names to preload into this agent's context at startup (opt-in; omit for none)
Skills []string `json:"skills,omitempty"`
}

// InfiniteSessionConfig configures infinite sessions with automatic context compaction
Expand Down
8 changes: 8 additions & 0 deletions nodejs/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,14 @@ export interface CustomAgentConfig {
* @default true
*/
infer?: boolean;
/**
* List of skill names to preload into this agent's context.
* When set, the full content of each listed skill is eagerly injected into
* the agent's context at startup. Skills are resolved by name from the
* session's configured skill directories (`skillDirectories`).
* When omitted, no skills are injected (opt-in model).
*/
skills?: string[];
}

/**
Expand Down
Loading
Loading