Skip to content

feat: serve plugin skills in chatd - #29323

Closed
jscottmiller wants to merge 2 commits into
scott/agent-plugins/3-skillsfrom
scott/agent-plugins/4-chatd
Closed

jscottmiller wants to merge 2 commits into
scott/agent-plugins/3-skillsfrom
scott/agent-plugins/4-chatd

Conversation

@jscottmiller

@jscottmiller jscottmiller commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

tl;dr: Plugin skills reach the model's prompt, the skill tools, skill invocations, and the chat API with their plugin attribution.

chatd lists plugin skills in <available-skills> as - <alias> (plugin: <name>): <description>, and read_skill and read_skill_file serve them like workspace skills, matching on (source, plugin, name). Plugin skill descriptions reach the index unmodified, as workspace skill descriptions do on main; sanitizing descriptions from every source is tracked in CODAGT-1337. The chatd architecture document describes plugin skills in its Skill index section. The extending-agents docs describe plugin skills, add their plugin/<plugin>/<name> alias to the alias table, and say the / menu lists them under Workspace skills.

codersdk.ChatContextResource gains PluginName and the plugin kind. GET /api/v2/chats/{chat} sets plugin_name on skill and MCP server rows in every status and includes agent warnings on OK rows. The frontend gets only a plugin manifest label for the new kind; the composer and context indicator changes are in #30577.

Skill-invocation parts (#30861) accept skill_source: "plugin" with a required skill_plugin_name, checked with workspacesdk.ValidatePluginName; other sources reject the field. chatd loads an invoked plugin skill from the pinned skill matching both names, so a workspace invocation no longer matches a same-named plugin skill, and a part whose source and plugin name disagree loads nothing. An unavailable workspace or plugin skill returns a 400 whose detail names the chat's workspace context.

TestChatContextPluginSkillsFromAgentPush runs a real agent against coderd with the experiment on and off and checks the pinned rows, the API listing, and the system prompt.

How plugin skills appear

With a workspace skill deploy and a plugin acme that ships deploy and release-notes, the system prompt's skill index reads:

<available-skills>
Use read_skill to load a skill's full instructions before following them.
Use read_skill_file to read supporting files referenced by a workspace or plugin skill.
When a skill is listed with a source prefix, such as personal/name or plugin/pluginname/name, pass that qualified alias to read_skill.

- workspace/deploy: Deploy the app from this repo
- plugin/acme/deploy (plugin: acme): Deploy through Acme
- release-notes (plugin: acme): Draft release notes
</available-skills>

GET /api/v2/chats/{chat} lists the plugin and its skills as context resources:

[
  { "source": "/home/coder/.coder/plugins/acme", "kind": "plugin", "size_bytes": 312, "plugin_name": "acme", "status": "ok" },
  { "source": "/home/coder/.coder/plugins/acme/skills/deploy", "kind": "skill", "size_bytes": 540, "skill_name": "deploy", "skill_description": "Deploy through Acme", "plugin_name": "acme", "status": "ok" }
]
PR map

scott/agent-plugins/3-skills..bda3ca2a4c

Category Files + -
prod (backend) 8 271 225
prod (frontend) 1 1 0
test (backend) 8 949 139
generated (backend) 4 241 157
generated (frontend) 1 27 2
docs 2 33 7
Total 24 1522 530
  1. coderd/x/chatd/chattool/skill.go (+98/-84): MergePinnedSkills merges personal, organization, and project skills with the pinned skills, split into workspace and plugin skills. ReadSkillOptions takes those skill lists in place of ResolveAlias, and read_skill and read_skill_file resolve an alias to its own pinned skill, with no fallback to a same-named workspace skill.
  2. coderd/x/chatd/context_prompt.go (+90/-83): decodes each pinned body inline, drops OK skills with invalid names (pinnedSkillMeta), keeps warnings on OK rows, and reports plugin rows and PluginName.
  3. coderd/x/chatd/turn_environment.go (+9/-42): the per-turn merge goes through MergePinnedSkills.
  4. coderd/x/chatd/skill_invocation.go (+22/-10): plugin invocations resolve by plugin and skill name; workspace invocations skip plugin skills.
  5. codersdk/chats.go (+24/-2, API): ChatContextResourceKindPlugin, ChatContextResource.PluginName, ChatSkillSourcePlugin, and SkillPluginName on ChatInputPart and ChatMessagePart.
  6. coderd/exp_chats.go (+21/-2, API): validates skill_plugin_name against the source and words the unavailable-skill detail by source.

This PR was generated by Coder Agents on behalf of @jscottmiller.

@ibetitsmike

Copy link
Copy Markdown
Collaborator

UAT finding: plugin_name is omitted on non-OK skill and mcp_server rows

Found while running UAT against the stack tip (#29326 at d8f7100b) on dogfood with agent-plugins enabled. Posting here because this PR adds PluginName to codersdk.ChatContextResource and the listing that fills it.

Observed. In GET /api/v2/chats/{chat} context.resources, every status: ok row shipped by a plugin carries plugin_name, and every non-OK row shipped by a plugin does not. Sanitized rows from the test workspace:

{"kind":"mcp_server","source":"acme/acme","status":"unreadable","error":"server name already in use by an earlier config"}
{"kind":"mcp_server","source":"mixed/httpexample","status":"unreadable","error":"server \"httpexample\": url \"http://example.com/mcp\" must use https for non-loopback hosts"}
{"kind":"mcp_server","source":"exitfast/deadserver","status":"unreadable","error":"failed to connect"}
{"kind":"skill","source":"/home/coder/.agents/plugins/symlinktest/skills/badmd","status":"invalid","error":"symlink target \"/tmp/outside-skill/SKILL.md\" escapes scan root ..."}
{"kind":"mcp_server","source":"exitfast/goodsibling","status":"ok","plugin_name":"exitfast","tools":[...]}

Cause. pinnedContextResources in coderd/x/chatd/context_prompt.go returns early for non-OK rows with only source, kind, status, and error, under the pre-existing comment "the body is empty for non-OK rows". That comment is stale: agentapi.marshalContextResourceBody documents that the body is always populated, and the agent's setResourceBody sets plugin_name regardless of status. At the stack tip pinnedRowBelongsToPlugin already decodes that same body for the experiment-off filter, which is why OFF correctly hides these error rows while ON lists them unattributed.

Impact. Low. source still reads <plugin>/<server> and the skill path sits under the plugin directory, so the UI issues list is attributable by eye. But an API consumer filtering by plugin_name misses exactly the rows that need attention (validation failures, collision losers, connect failures), and the field doc added here says it is set for skill and mcp_server kinds shipped inside a plugin.

Suggested fix. In the non-OK branch, decode by BodyKind (skill: SkillMetaBody.plugin_name, mcp_server: MCPServerBody.plugin_name), run workspacesdk.ValidatePluginName, and set PluginName; drop the stale comment. Invalid plugin kind rows can stay without it since the name itself may be what failed. A non-OK attributed row in TestPinnedContextResources would lock it in.

Everything else exercised on the tip worked as described in #29320; summary on #29326.

Posted by Xum, an AI coding agent, on behalf of @ibetitsmike.

@jscottmiller
jscottmiller force-pushed the scott/agent-plugins/4-chatd branch from 0e5ffd0 to 228d528 Compare September 15, 2026 19:11
@jscottmiller
jscottmiller force-pushed the scott/agent-plugins/4-chatd branch 2 times, most recently from 1b93d15 to db120bb Compare September 17, 2026 21:33
@jscottmiller
jscottmiller force-pushed the scott/agent-plugins/4-chatd branch from db120bb to 571d62b Compare September 18, 2026 22:02
@linear-code

linear-code Bot commented Sep 22, 2026

Copy link
Copy Markdown

CODAGT-1101

@jscottmiller
jscottmiller force-pushed the scott/agent-plugins/4-chatd branch from 571d62b to 2557031 Compare September 24, 2026 22:10
@jscottmiller jscottmiller added the experimental Changes that might not necessarily be merged, until its approved to proceed with. label Sep 24, 2026
@jscottmiller
jscottmiller force-pushed the scott/agent-plugins/4-chatd branch from 2557031 to e4b94a0 Compare September 30, 2026 01:18
@jscottmiller
jscottmiller force-pushed the scott/agent-plugins/4-chatd branch from e4b94a0 to 11deb02 Compare September 30, 2026 01:33
@jscottmiller jscottmiller removed the experimental Changes that might not necessarily be merged, until its approved to proceed with. label Sep 30, 2026
@jscottmiller
jscottmiller force-pushed the scott/agent-plugins/4-chatd branch 2 times, most recently from 475d72e to fc187c2 Compare October 6, 2026 19:47
@jscottmiller
jscottmiller force-pushed the scott/agent-plugins/4-chatd branch from fc187c2 to 463a03a Compare October 6, 2026 21:36
@jscottmiller
jscottmiller force-pushed the scott/agent-plugins/4-chatd branch from 463a03a to 37bf56d Compare October 6, 2026 22:54
@jscottmiller
jscottmiller force-pushed the scott/agent-plugins/4-chatd branch from 37bf56d to bdd9542 Compare October 7, 2026 18:53
@jscottmiller
jscottmiller force-pushed the scott/agent-plugins/4-chatd branch from bdd9542 to f558b5c Compare October 7, 2026 20:29
@jscottmiller

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

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.

This round reviews the rebased PR, which renders plugin skills in the chatd prompt, the skill tools, the chat API, and the Agents UI. Findings inline: 1 P2, 3 P3, 13 P4.

Addressed since the last round: all 25 open findings are fixed, and the site-name part of the "workspace skill" naming dispute is accepted. The P2 is new because of the rebase: this PR's base is now 7cd1e84, and #30341 is based on 1c5c928, so it is a sibling branch, not below this PR. The round 1 split required the description sanitization to merge first, and the stack no longer ensures that.

Out of scope (needs a ticket or explicit acceptance by a human):

  • coderd/x/chatd/chattool/skill.go:172: workspace skill descriptions also reach <available-skills> raw on the base, so any repository SKILL.md can close the block the same way. #30341 targets this.
  • site/src/pages/AgentsPage/components/ContextUsageIndicator.tsx:303: skillBytes sums the full SKILL.md, but only the name and description reach the prompt until read_skill runs, so the Skills section overstates prompt cost.
  • agent/agentcontext/defaults.go:9: the built-in ~/.coder scan roots cannot be overridden through agent.Options, so every test that runs a real agent reads the developer's home skills, plugins, and instruction files.

coderd/x/chatd/generation_preparer.go:531

P3 [CRF-40] The only production caller of mergeTurnSkills still names the pinned list workspaceSkills, and that list now includes plugin skills. (Netero)

The variable is used at 8 sites, including GetSkills: func() []chattool.SkillMeta { return workspaceSkills } for read_skill, so a reader concludes plugin skills are not served there. The parameter it feeds is now pinnedSkills (chatd.go:3726). Rename the variable to pinnedSkills in generation_preparer.go.

🤖

codersdk/chats.go:291

P4 [CRF-47] The ChatContext.Resources doc still says the list holds instruction files and skills that the prompt is built from, but plugin manifest rows are listed and never reach the prompt. (Mafuuu)

The kind doc on the same type was updated for this, so the field doc and the generated docs/reference/api/chats.md now disagree with it. Rewrite the sentence to match the kind doc, then run make gen.

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/x/chatd/chattool/skill.go Resolved
Comment thread coderd/x/chatd/chattool/skill.go Resolved
Comment thread coderd/x/chatd/context_prompt.go Resolved
Comment thread coderd/x/chatd/chattool/skill.go Resolved
@jscottmiller
jscottmiller force-pushed the scott/agent-plugins/4-chatd branch 3 times, most recently from 737f58e to 639212a Compare October 8, 2026 00:36
@jscottmiller

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@jscottmiller
jscottmiller force-pushed the scott/agent-plugins/4-chatd branch from 639212a to 6917a33 Compare October 8, 2026 01:21

@coder-agents-review coder-agents-review Bot left a comment

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.

This round reviews the fixes to the plugin skill rendering in chatd, the chat API, and the Agents UI. Findings inline: 2 P3, 11 P4, 5 Nit.

Addressed since the last round: 16 findings are fixed. The raw plugin description finding is deferred to CODAGT-1337 now that #30341 is closed. Most new findings are about the round 3 fixes. The ResolveAlias removal still leaves two copies of the merge recipe and some unreachable not-found branches, and some reworded comments are still wrong.

Out of scope (needs a ticket or explicit acceptance by a human):

  • site/src/modules/roles/RoleSelector.tsx:58: Object.groupBy (Chrome 117, Safari 17.4) is called with no polyfill while browserslist declares Chrome 111 and Safari 16.4, so the role selector throws TypeError in those supported browsers.

Notes:

  • coderd/x/chatd/context_prompt.go:438: an OK plugin row whose body fails to decode is dropped from the listing with no log line, unlike the prompt path's malformed-row log. This can happen only after a schema change.

coderd/x/chatd/chattool/skill.go:531

P4 [CRF-61] The failed to resolve skill %q branch of skillResolveErrorResponse can no longer run. (Netero)

Lookup returns only ErrSkillNotFound or ErrSkillAmbiguous, both handled above, and the PR deleted ResolveAliasErrorIsSanitized along with the injected resolver. Coverage reports skill.go:531.2,533.3 at 0. Remove the fallback, or keep it with a comment that no current resolver returns another error.

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/x/chatd/generation_preparer.go Resolved
Comment thread coderd/x/chatd/context_prompt.go Resolved
Comment thread coderd/x/chatd/chattool/skill.go Resolved
Comment thread coderd/x/chatd/chattool/skill.go Resolved
Comment thread coderd/x/chatd/generation_preparer.go Resolved
Comment thread coderd/x/chatd/context_prompt.go Resolved
Comment thread coderd/x/chatd/chattool/skill.go Resolved
Comment thread coderd/x/chatd/context_prompt.go Outdated
Comment on lines 26 to 38
// decodeContextBody decodes a protojson resource body into the proto type T.
// ok is false when the body cannot be decoded, letting callers count it as
// malformed rather than silently treating it as empty.
func decodeContextBody[T any, P interface {
*T
proto.Message
}](body json.RawMessage) (P, bool) {
decoded := P(new(T))
if err := contextBodyUnmarshalOptions.Unmarshal(body, decoded); err != nil {
return nil, false
}
return &decoded, true
return decoded, true
}

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.

I'm not sure I see the value in this helper?

At call sites we're going from

var decoded agentproto.MCPServerBody
if err := contextBodyUnmarshalOptions.Unmarshal(body, &decoded); err != nil {
  return nil
}

// Use 'decoded' here

To

decoded, ok := decodeContextBody[agentproto.MCPServerBody](body)
if !ok {
  return nil
}

 // Use 'decoded' here

Maybe this is just a completely useless nit and you can definitely ignore me but it just feels unneeded (in my opinion, feel free to completely disagree with me).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nah, agreed. Inlined.

Comment thread coderd/x/chatd/ARCHITECTURE.md Outdated
Comment on lines +1013 to +1017
##### Skill index

Each turn, chatd merges the user's personal skills with the skills the workspace agent pushed and inserts an `<available-skills>` block into the system prompt, after the chat instruction and before the user prompt. Each entry is a skill alias and its description. A name held by one skill is listed bare; a name held by several is listed as `personal/<name>`, `workspace/<name>`, or `plugin/<plugin>/<name>`. The model passes an alias to `read_skill` to load the body, and to `read_skill_file` to read a supporting file of a workspace or plugin skill.

A pinned skill row with a plugin name is a plugin skill: a skill shipped in an Agent Plugin's `skills/` directory. chatd keeps plugin skills apart from plain workspace skills and identifies each by (plugin, name), so two plugins can ship skills with the same name. Plugin skills follow personal and workspace skills when a name is shared, ordered by plugin name. Each plugin skill entry carries a `(plugin: <name>)` label after its alias, and the qualified-alias hint lists the `plugin/<plugin>/<name>` form only when a plugin skill is listed that way. `read_skill` and `read_skill_file` serve a plugin skill from its own pinned directory and never fall back to a same-named workspace skill.

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.

We try to keep changes to ARCHITECTURE.md as human as possible. I'm just checking that you've given this a thorough read through and verify that it is readable to a human and is as accurate as possible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Did an update pass on this to tighten it up, clarify a few items, and add a little detail on Mike's recent org plugin work.

runPluginTurn := func(t *testing.T, enabled bool) turnResult {
t.Helper()

ctx := testutil.Context(t, testutil.WaitSuperLong)

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.

ctx should be declared as close to first use as possible in tests

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Shifted.

Comment thread coderd/x/chatd/chattool/skill.go Outdated
Comment on lines +33 to +35
// PluginName is the owning Agent Plugin's name, empty for a plain
// workspace skill. Together with Name it identifies the skill.
PluginName string

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.

Comment yap

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeaaah that's some slop. I did a pass over the whole stack to clean things up; we may want to apply the frontend comment logic to the go code as well, though my personal instructions have something similar and the model gleefully ignores it.

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Docs preview

Check off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here.

@coderagents

coderagents Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Documentation Check

Updates Needed

  • docs/ai-coder/agents/extending-agents.md (open) - The ### Skills with the same name source-to-qualified-alias table now includes the Plugin | \plugin//`` row, so its enumeration of qualified aliases ("each copy gets a qualified alias that names its source") is complete for the plugin source this PR introduced. This was addressed.
  • docs/ai-coder/agents/extending-agents.md (open) - In the ### Plugin skills section, the sentence "The / menu in the chat composer lists plugin skills in the Workspace skills group, always by their qualified alias" does not match the code this PR ships. The composer feeds ChatMessageInput with four skill sources (personal, org, project, workspace) in skillLists; workspaceSkillsFromChat filters resource.kind === "skill" with no plugin awareness; resolveSkillTriggers has no plugin source; and the frontend has no PluginName/plugin_name handling in the composer or context indicator. The PR body states "the composer and context indicator changes are in feat(site/src): render Agent Plugin context #30577," so this behavior lands in a different PR. Either drop this sentence from this PR or move it to the PR that implements it.

Automated review via Coder Agents

@jscottmiller

Copy link
Copy Markdown
Contributor Author

UAT finding: plugin_name is omitted on non-OK skill and mcp_server rows

Fixed: pinnedContextResources now sets PluginName on non-OK rows from the stored plugin_name, and TestPinnedContextResources/NonOKRowsKeepPluginAttribution covers a skill row and an MCP server row.

(Coder Agents generated)

Skills pushed by the workspace agent with a plugin_name now reach the chat
as plugin-source skills. The prompt index lists them as
"- <alias> (plugin: <name>): <description>", collisions qualify them as
plugin/<plugin>/<name>, and read_skill and read_skill_file serve them from
the pinned SKILL.md and the skill directory like workspace skills.
The skill tools resolve a name to its pinned row by (source, plugin,
name), so two plugins shipping the same skill name resolve to their own
bodies.

Pinned skill names must match the kebab-case skill grammar; rows that
fail are dropped and logged with their sources. Plugin names are not
re-validated on read, since the Agent API rejects a push carrying an
invalid one.

Plugin skill descriptions reach <available-skills> unmodified, as
workspace skill descriptions do on main; sanitizing descriptions from
every source is tracked in CODAGT-1337.

The chatd architecture document gains a Skill index section covering the
block, bare and qualified aliases, the skill tools and plugin skills.

codersdk.ChatContextResource gains PluginName and the plugin kind. The
listing sets PluginName on non-OK skill and mcp_server rows as well as
OK ones, and copies the agent's Error onto OK rows so spec-mandated
manifest warnings (unknown fields, non-object extensions, an ignored
mcp.json or skills directory) reach the API instead of only the
database.

The extending-agents docs gain a Plugin skills section covering the
skills/ directory, the plugin label in the skill list, and the
plugin/<plugin>/<name> alias, and note that an invalid plugin appears in
the chat's context list.

Skill-invocation parts accept skill_source "plugin" with a required
skill_plugin_name. chatd loads the body from the pinned plugin skill
matching both names, and a workspace invocation no longer matches a plugin
skill that shares its name. A workspace or plugin skill that is not
available is reported as missing from the chat's workspace context.

The frontend gains only a "plugin manifest" label for the new kind; the
composer and context indicator changes ship separately.

An integration test runs a real workspace agent against a scan root with
a plugin skill and a plain skill, and checks the pinned rows, the
GET /chats/{id} context listing, and the system prompt, with the
agent-plugins experiment on and off.
… index

Rewrite the Skill index section of the chatd architecture doc to cover
all four skill sources: personal, organization, workspace, and plugin.
It now lists them in collision order and documents the org/<name>
alias, which resolves the organization skills TODO.

Define pinned skills as the chat's copy of each pushed SKILL.md. A
later push only marks the chat dirty; the copy changes when the chat
is refreshed or bound to a different agent.

Describe how read_skill and read_skill_file resolve each source.
Personal and organization bodies are read from the database on every
call. Workspace and plugin bodies come from the pinned SKILL.md, and
their supporting files are read over a live workspace connection.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale This issue is like stale bread.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants