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 @@ -216,6 +216,7 @@ await using var session = await client.CreateSessionAsync(new SessionConfig
| `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 @@ -225,6 +226,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 @@ -316,20 +316,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
1 change: 1 addition & 0 deletions docs/features/streaming-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ A skill was activated for the current conversation.
| `allowedTools` | `string[]` | | Tools auto-approved while this skill is active |
| `pluginName` | `string` | | Plugin the skill originated from |
| `pluginVersion` | `string` | | Plugin version |
| `agentName` | `string` | | Name of the agent that invoked the skill, when invoked by a custom agent |
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc adds an agentName field to the skill.invoked event, but the SDK-generated event payload types currently do not include agentName for skill.invoked (e.g., nodejs/src/generated/session-events.ts and dotnet/src/Generated/SessionEvents.cs). Please either update/regenerate the event schemas/types to include this field, or remove it from the docs until it is actually emitted and modeled.

Suggested change
| `agentName` | `string` | | Name of the agent that invoked the skill, when invoked by a custom agent |

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The agentName field is now documented here for the skill.invoked event, but the Node.js and .NET generated SDK types don't yet expose it in the skill.invoked-specific data structures:

  • Node.js (nodejs/src/generated/session-events.ts): the skill.invoked discriminated union data type only has name, path, content, allowedTools?, pluginName?, pluginVersion?, and description? — no agentName.
  • .NET (dotnet/src/Generated/SessionEvents.cs): SkillInvokedData has the same 7 fields — no AgentName.

By contrast, Go and Python use a flat/unified Data struct that already includes AgentName/agent_name (they expose all fields from all event types in one struct), so they're fine.

Since these are auto-generated files (AUTO-GENERATED FILE - DO NOT EDIT), they'll need to be regenerated from the updated schema once the dependent runtime PR lands. It would be worth adding a follow-up task or a note in this PR to regenerate the Node.js and .NET types so that agentName is accessible in a type-safe way for consumers of those SDKs.


---

Expand Down
10 changes: 10 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1515,6 +1515,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
61 changes: 61 additions & 0 deletions dotnet/test/SkillsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,67 @@ 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
});

Comment on lines +90 to +110
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests configure CustomAgents but do not set SessionConfig.Agent, so they rely on inferred agent selection rather than explicitly exercising the intended agent. To make per-agent skill scoping deterministic, set Agent = "skill-agent" / Agent = "no-skill-agent" in the session config for the respective tests.

Copilot uses AI. Check for mistakes.
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
});

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
75 changes: 75 additions & 0 deletions go/internal/e2e/skills_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,81 @@ 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,
})
Comment on lines +111 to +129
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test configures CustomAgents but does not explicitly set SessionConfig.Agent, so whether the request is handled by the intended custom agent depends on inference/routing behavior. To deterministically validate per-agent skills, set Agent: "skill-agent" (and Agent: "no-skill-agent" in the negative test) in the SessionConfig.

Copilot uses AI. Check for mistakes.
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 message.Data.Content == nil || !strings.Contains(*message.Data.Content, skillMarker) {
t.Errorf("Expected message to contain skill marker '%s', got: %v", skillMarker, message.Data.Content)
}

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,
})
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 message.Data.Content != nil && strings.Contains(*message.Data.Content, skillMarker) {
t.Errorf("Expected message to NOT contain skill marker '%s' when agent has no skills, got: %v", skillMarker, *message.Data.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 @@ -416,6 +416,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