feat: add per-agent skills support to SDK types and docs (#958)#995
feat: add per-agent skills support to SDK types and docs (#958)#995MackinnonBuck wants to merge 2 commits intomainfrom
Conversation
Add a 'skills' field to CustomAgentConfig across all four SDK languages (Node.js, Python, Go, .NET) to support scoping skills to individual subagents. Skills are opt-in: agents get no skills by default. Changes: - Add skills?: string[] to CustomAgentConfig in all SDKs - Update custom-agents.md with skills in config table and new section - Update skills.md with per-agent skills example and opt-in note - Update streaming-events.md with agentName on skill.invoked event - Add E2E tests for agent-scoped skills in all four SDKs - Add snapshot YAML files for new test scenarios Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds per-custom-agent skill scoping to the SDK surface area (types + docs) and introduces new cross-SDK E2E coverage + replay snapshots for the opt-in behavior.
Changes:
- Added
skillstoCustomAgentConfigacross Node.js, Python, Go, and .NET SDK types. - Updated docs to describe per-agent skill scoping and added
agentNametoskill.invokedevent docs. - Added E2E tests and new replay snapshots for “agent with skills” vs “agent without skills field”.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
nodejs/src/types.ts |
Adds skills?: string[] to CustomAgentConfig with JSdoc explaining opt-in semantics. |
python/copilot/session.py |
Adds skills to the Python CustomAgentConfig TypedDict. |
go/types.go |
Adds Skills []string to Go CustomAgentConfig for JSON serialization. |
dotnet/src/Types.cs |
Adds List<string>? Skills to .NET CustomAgentConfig with XML docs. |
docs/features/custom-agents.md |
Documents the new skills field and adds a dedicated per-agent skills section. |
docs/features/skills.md |
Updates “Skills + Custom Agents” section with per-agent skills example and opt-in note. |
docs/features/streaming-events.md |
Documents agentName on skill.invoked. |
nodejs/test/e2e/skills.test.ts |
Adds Node E2E tests for agent-scoped skills behavior. |
python/e2e/test_skills.py |
Adds Python E2E tests for agent-scoped skills behavior. |
go/internal/e2e/skills_test.go |
Adds Go E2E tests for agent-scoped skills behavior. |
dotnet/test/SkillsTests.cs |
Adds .NET E2E tests for agent-scoped skills behavior. |
test/snapshots/skills/should_allow_agent_with_skills_to_invoke_skill.yaml |
Replay snapshot for the “agent with skills can invoke” scenario. |
test/snapshots/skills/should_not_provide_skills_to_agent_without_skills_field.yaml |
Replay snapshot for the “no skills field => no skills” scenario. |
nodejs/package-lock.json |
Lockfile metadata updates. |
Files not reviewed (1)
- nodejs/package-lock.json: Language not supported
python/copilot/session.py
Outdated
| # List of skill names available to this agent (opt-in; omit for no skills) | ||
| skills: NotRequired[list[str]] |
There was a problem hiding this comment.
The Python client currently converts CustomAgentConfig to wire format via CopilotClient._convert_custom_agent_to_wire_format(), but that converter does not include the new skills field, so any skills you set here will be silently dropped and never reach the CLI/runtime. Please update the converter to pass through skills (and keep the opt-in semantics by only including it when present).
| it("should allow agent with skills to invoke skill", async () => { | ||
| const skillsDir = createSkillDir(); | ||
| const customAgents: CustomAgentConfig[] = [ | ||
| { | ||
| name: "skill-agent", | ||
| description: "An agent with access to test-skill", | ||
| prompt: "You are a helpful test agent.", | ||
| skills: ["test-skill"], | ||
| }, | ||
| ]; | ||
|
|
||
| const session = await client.createSession({ | ||
| onPermissionRequest: approveAll, | ||
| skillDirectories: [skillsDir], | ||
| customAgents, | ||
| }); |
There was a problem hiding this comment.
These tests create custom agents but never explicitly select which agent should handle the prompt. That makes the assertions depend on inference/selection behavior rather than deterministically validating per-agent skill scoping. Consider setting agent: "skill-agent" (and agent: "no-skill-agent" in the negative case) in the session config so the test always exercises the intended agent.
| session = await ctx.client.create_session( | ||
| on_permission_request=PermissionHandler.approve_all, | ||
| skill_directories=[skills_dir], | ||
| custom_agents=custom_agents, |
There was a problem hiding this comment.
These tests add custom agents but don't explicitly select the agent for the session, so the prompt may be handled by the default agent or inferred routing, which weakens the guarantee that you're testing per-agent skill scoping. Consider passing agent="skill-agent" / agent="no-skill-agent" when creating the session to make the test deterministic.
| custom_agents=custom_agents, | |
| custom_agents=custom_agents, | |
| agent="skill-agent", |
| 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, | ||
| }) |
There was a problem hiding this comment.
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.
| [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 | ||
| }); | ||
|
|
There was a problem hiding this comment.
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.
| | `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 | |
There was a problem hiding this comment.
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.
| | `agentName` | `string` | | Name of the agent that invoked the skill, when invoked by a custom agent | |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #995
| | `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 | |
There was a problem hiding this comment.
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): theskill.invokeddiscriminated union data type only hasname,path,content,allowedTools?,pluginName?,pluginVersion?, anddescription?— noagentName. - .NET (
dotnet/src/Generated/SessionEvents.cs):SkillInvokedDatahas the same 7 fields — noAgentName.
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.
Update type comments, docs, and test descriptions to reflect that per-agent skills are eagerly injected into the agent's context at startup rather than filtered for invocation. Sub-agents do not inherit skills from the parent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
SDK Consistency ReviewThis PR adds 🐛 Python:
|
| SDK | Type added | Test added | Wire-format correct |
|---|---|---|---|
| Node.js | ✅ types.ts |
✅ | ✅ (direct serialization) |
| Go | ✅ types.go |
✅ | ✅ (direct JSON marshaling) |
| .NET | ✅ Types.cs |
✅ | ✅ ([JsonPropertyName("skills")]) |
| Python | ✅ session.py |
✅ | ❌ client.py conversion missing |
Generated by SDK Consistency Review Agent for issue #995 · ◷
Summary
Adds SDK support for scoping skills to individual subagents, addressing #958.
Changes
Types (all 4 SDKs):
nodejs/src/types.ts): Addedskills?: string[]toCustomAgentConfigpython/copilot/session.py): Addedskillsfield to agent configgo/types.go): AddedSkills []stringto agent config structdotnet/src/Types.cs): AddedList<string>? Skillsto agent config classDocumentation:
docs/features/custom-agents.md: Addedskillsto config reference table and new section on per-agent skillsdocs/features/skills.md: Updated "Skills + Custom Agents" section with per-agent exampledocs/features/streaming-events.md: AddedagentNamefield toskill.invokedevent docsTests (all 4 SDKs):
skillscan invoke listed skillsskillsfield gets no skills (backward compatible)Design decisions
skillsis optional. Omitting it means the agent has no access to skills (not all skills).skillDirectoriespool.Depends on: github/copilot-agent-runtime PR (runtime must land first for e2e tests to pass)
Closes #958