GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — feat: render plugin skills in chatd and the composer · coder/coder@639212a

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • coderd
    • apidoc
      • docs.go
      • swagger.json
    • x/chatd
      • ARCHITECTURE.md
      • chatd.go
      • chattool
        • skill.go
        • skill_test.go
      • context_prompt.go
      • context_prompt_internal_test.go
      • generation_preparer.go
      • plugin_integration_test.go
  • codersdk
    • chats.go
  • docs/reference/api
    • chats.md
    • schemas.md
  • site/src
    • api
      • typesGenerated.ts
    • pages/AgentsPage/components
      • ChatMessageInput
        • ChatMessageInput.tsx
        • SkillsTriggerMenu.stories.tsx
        • SkillsTriggerMenu.test.tsx
        • SkillsTriggerMenu.tsx
      • ChatPageContent.test.ts
      • ChatPageContent.tsx
      • ContextUsageIndicator.stories.tsx
      • ContextUsageIndicator.test.tsx
      • ContextUsageIndicator.tsx
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎coderd/apidoc/docs.go‎

Copy file name to clipboardExpand all lines: coderd/apidoc/docs.go
+11-5Lines changed: 11 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Collapse file

‎coderd/apidoc/swagger.json‎

Copy file name to clipboardExpand all lines: coderd/apidoc/swagger.json
+16-5Lines changed: 16 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Collapse file

‎coderd/x/chatd/ARCHITECTURE.md‎

Copy file name to clipboardExpand all lines: coderd/x/chatd/ARCHITECTURE.md
+6Lines changed: 6 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,12 @@ Root chats in a project share durable memory; other chats have none. The agent s
10701070

10711071
<!-- TODO(f0ssel): the memory index no longer lives in the `read_memory` tool description. At turn start the generation loop commits a model-only user row: a full `<project-memory-index>` snapshot when the prompt has none (first turn, or after compaction), otherwise a `<project-memory-index-update>` listing changes since the model last saw it. Tool definitions and the system prompt carry no memory state, so memory writes no longer invalidate the provider's cached prefix. Mid-turn only a snapshot dropped by compaction is restored, and never between an assistant step and its tool results. -->
10721072

1073+
##### Skill index
1074+
1075+
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.
1076+
1077+
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.
1078+
10731079
##### Reasoning effort
10741080

10751081
Model configs may carry a `reasoning_effort` config (`{default, max}`) inside `chat_model_configs.options`. Users select a per-turn effort when sending or editing a message; the value is stored on `chat_messages.reasoning_effort` and on `chat_queued_messages.reasoning_effort` for queued messages. Queued messages carry the value through promotion, and `chats.last_reasoning_effort` tracks the most recent message that set one, mirroring `last_model_config_id`.
Collapse file

‎coderd/x/chatd/chatd.go‎

Copy file name to clipboardExpand all lines: coderd/x/chatd/chatd.go
+3-21Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3721,30 +3721,12 @@ type systemPromptBehaviorContext struct {
37213721
isRootChat bool
37223722
}
37233723

3724-
func workspaceSkillsForResolution(workspaceSkills []chattool.SkillMeta) []skillspkg.Skill {
3725-
if len(workspaceSkills) == 0 {
3726-
return nil
3727-
}
3728-
resolved := make([]skillspkg.Skill, 0, len(workspaceSkills))
3729-
for _, skill := range workspaceSkills {
3730-
resolved = append(resolved, skillspkg.Skill{
3731-
Name: skill.Name,
3732-
Description: skill.Description,
3733-
Source: skillspkg.SourceWorkspace,
3734-
})
3735-
}
3736-
return resolved
3737-
}
3738-
37393724
func mergeTurnSkills(
37403725
personalSkills []skillspkg.Skill,
3741-
workspaceSkills []chattool.SkillMeta,
3726+
pinnedSkills []chattool.SkillMeta,
37423727
) []skillspkg.ResolvedSkill {
3743-
return skillspkg.MergeSkills(
3744-
personalSkills,
3745-
workspaceSkillsForResolution(workspaceSkills),
3746-
nil,
3747-
)
3728+
workspace, plugin := chattool.SplitPinnedSkills(pinnedSkills)
3729+
return skillspkg.MergeSkills(personalSkills, workspace, plugin)
37483730
}
37493731

37503732
// buildSystemPrompt applies system-level prompt injections in a fixed
Collapse file

‎coderd/x/chatd/chattool/skill.go‎

Copy file name to clipboardExpand all lines: coderd/x/chatd/chattool/skill.go
+80-36Lines changed: 80 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ const (
3030
type SkillMeta struct {
3131
Name string
3232
Description string
33+
// PluginName is the owning Agent Plugin's name, empty for a plain
34+
// workspace skill. Together with Name it identifies the skill.
35+
PluginName string
3336
// Dir is the absolute path to the skill directory inside
3437
// the workspace filesystem.
3538
Dir string
@@ -44,6 +47,28 @@ type SkillMeta struct {
4447
Meta []byte
4548
}
4649

50+
// SplitPinnedSkills splits pinned skill metadata into SourceWorkspace skills
51+
// and SourcePlugin skills, in the argument shape skillspkg.MergeSkills takes.
52+
func SplitPinnedSkills(pinned []SkillMeta) (workspace, plugin []skillspkg.Skill) {
53+
for _, skill := range pinned {
54+
if skill.PluginName != "" {
55+
plugin = append(plugin, skillspkg.Skill{
56+
Name: skill.Name,
57+
Description: skill.Description,
58+
Source: skillspkg.SourcePlugin,
59+
PluginName: skill.PluginName,
60+
})
61+
continue
62+
}
63+
workspace = append(workspace, skillspkg.Skill{
64+
Name: skill.Name,
65+
Description: skill.Description,
66+
Source: skillspkg.SourceWorkspace,
67+
})
68+
}
69+
return workspace, plugin
70+
}
71+
4772
// SkillContent is the full body of a skill, loaded on demand
4873
// when the model calls read_skill.
4974
type SkillContent struct {
@@ -65,32 +90,44 @@ func FormatResolvedSkillIndex(resolved []skillspkg.ResolvedSkill) string {
6590

6691
entries := make([]skillIndexEntry, 0, len(resolved))
6792
hasQualifiedAlias := false
68-
hasWorkspaceSkill := false
93+
hasQualifiedPluginAlias := false
94+
hasFileBackedSkill := false
6995
for _, s := range resolved {
70-
entries = append(entries, skillIndexEntry{
96+
entry := skillIndexEntry{
7197
Alias: s.Alias,
7298
Description: s.Description,
73-
})
74-
if s.Source == skillspkg.SourceWorkspace {
75-
hasWorkspaceSkill = true
99+
}
100+
if s.Source == skillspkg.SourcePlugin {
101+
entry.PluginName = s.PluginName
102+
}
103+
entries = append(entries, entry)
104+
if s.Source == skillspkg.SourceWorkspace || s.Source == skillspkg.SourcePlugin {
105+
hasFileBackedSkill = true
76106
}
77107
if s.Alias == s.QualifiedAlias() {
78108
hasQualifiedAlias = true
109+
if s.Source == skillspkg.SourcePlugin {
110+
hasQualifiedPluginAlias = true
111+
}
79112
}
80113
}
81114
return renderSkillIndex(entries, skillIndexFormatOptions{
82115
includeQualifiedAliasInstruction: hasQualifiedAlias,
83-
includeReadSkillFileInstruction: hasWorkspaceSkill,
116+
qualifiedAliasHintListsPlugin: hasQualifiedPluginAlias,
117+
includeReadSkillFileInstruction: hasFileBackedSkill,
84118
})
85119
}
86120

87121
type skillIndexEntry struct {
88122
Alias string
89123
Description string
124+
// PluginName is rendered as a "(plugin: <name>)" label after the alias.
125+
PluginName string
90126
}
91127

92128
type skillIndexFormatOptions struct {
93129
includeQualifiedAliasInstruction bool
130+
qualifiedAliasHintListsPlugin bool
94131
includeReadSkillFileInstruction bool
95132
}
96133

@@ -108,19 +145,28 @@ func renderSkillIndex(entries []skillIndexEntry, opts skillIndexFormatOptions) s
108145
if opts.includeReadSkillFileInstruction {
109146
_, _ = b.WriteString(
110147
"Use read_skill_file to read supporting files " +
111-
"referenced by a workspace skill.\n",
148+
"referenced by a workspace or plugin skill.\n",
112149
)
113150
}
114151
if opts.includeQualifiedAliasInstruction {
152+
aliasForms := "personal/name or workspace/name"
153+
if opts.qualifiedAliasHintListsPlugin {
154+
aliasForms = "personal/name, workspace/name, or plugin/pluginname/name"
155+
}
115156
_, _ = b.WriteString(
116-
"When a skill is listed as personal/name or workspace/name, " +
157+
"When a skill is listed as " + aliasForms + ", " +
117158
"pass that qualified alias to read_skill.\n",
118159
)
119160
}
120161
_, _ = b.WriteString("\n")
121162
for _, s := range entries {
122163
_, _ = b.WriteString("- ")
123164
_, _ = b.WriteString(s.Alias)
165+
if s.PluginName != "" {
166+
_, _ = b.WriteString(" (plugin: ")
167+
_, _ = b.WriteString(s.PluginName)
168+
_, _ = b.WriteString(")")
169+
}
124170
if s.Description != "" {
125171
_, _ = b.WriteString(": ")
126172
_, _ = b.WriteString(s.Description)
@@ -288,9 +334,11 @@ const DefaultSkillMetaFile = "SKILL.md"
288334
// ReadSkillOptions configures the read_skill and read_skill_file
289335
// tools.
290336
type ReadSkillOptions struct {
291-
GetWorkspaceConn func(context.Context) (workspacesdk.AgentConn, error)
292-
GetSkills func() []SkillMeta
293-
ResolveAlias func(string) (skillspkg.ResolvedSkill, error)
337+
GetWorkspaceConn func(context.Context) (workspacesdk.AgentConn, error)
338+
GetSkills func() []SkillMeta
339+
// PersonalSkills are merged with GetSkills to resolve names and
340+
// qualified aliases.
341+
PersonalSkills []skillspkg.Skill
294342
LoadPersonalSkillBody func(context.Context, string) (skillspkg.ParsedSkill, error)
295343
}
296344

@@ -342,8 +390,8 @@ func ReadSkill(options ReadSkillOptions) fantasy.AgentTool {
342390
"body": content.Body,
343391
"files": []string{},
344392
}), nil
345-
case skillspkg.SourceWorkspace:
346-
content, response, ok := readWorkspaceSkillBody(ctx, options, args.Name, resolved.Name)
393+
case skillspkg.SourceWorkspace, skillspkg.SourcePlugin:
394+
content, response, ok := readPinnedSkillBody(ctx, options, args.Name, resolved.Skill)
347395
if ok {
348396
return response, nil
349397
}
@@ -397,11 +445,11 @@ func ReadSkillFile(options ReadSkillOptions) fantasy.AgentTool {
397445
"read_skill_file is not supported for personal skills (no supporting files)",
398446
), nil
399447
}
400-
if resolved.Source != skillspkg.SourceWorkspace {
448+
if resolved.Source != skillspkg.SourceWorkspace && resolved.Source != skillspkg.SourcePlugin {
401449
return skillNotFoundResponse(args.Name), nil
402450
}
403451

404-
skill, ok := findSkill(options.GetSkills, resolved.Name)
452+
skill, ok := findSkill(options.GetSkills, resolved.Skill)
405453
if !ok {
406454
return skillNotFoundResponse(args.Name), nil
407455
}
@@ -443,31 +491,21 @@ func ReadSkillFile(options ReadSkillOptions) fantasy.AgentTool {
443491
}
444492

445493
func resolveSkillAlias(options ReadSkillOptions, name string) (skillspkg.ResolvedSkill, error) {
446-
if options.ResolveAlias != nil {
447-
return options.ResolveAlias(name)
448-
}
449-
450-
skill, ok := findSkill(options.GetSkills, name)
451-
if !ok {
452-
return skillspkg.ResolvedSkill{}, skillspkg.ErrSkillNotFound
494+
var pinned []SkillMeta
495+
if options.GetSkills != nil {
496+
pinned = options.GetSkills()
453497
}
454-
return skillspkg.ResolvedSkill{
455-
Skill: skillspkg.Skill{
456-
Name: skill.Name,
457-
Description: skill.Description,
458-
Source: skillspkg.SourceWorkspace,
459-
},
460-
Alias: skill.Name,
461-
}, nil
498+
workspace, plugin := SplitPinnedSkills(pinned)
499+
return skillspkg.Lookup(skillspkg.MergeSkills(options.PersonalSkills, workspace, plugin), name)
462500
}
463501

464-
func readWorkspaceSkillBody(
502+
func readPinnedSkillBody(
465503
ctx context.Context,
466504
options ReadSkillOptions,
467505
requestedName string,
468-
canonicalName string,
506+
resolved skillspkg.Skill,
469507
) (SkillContent, fantasy.ToolResponse, bool) {
470-
skill, ok := findSkill(options.GetSkills, canonicalName)
508+
skill, ok := findSkill(options.GetSkills, resolved)
471509
if !ok {
472510
return SkillContent{}, skillNotFoundResponse(requestedName), true
473511
}
@@ -508,16 +546,22 @@ func nonNilFiles(files []string) []string {
508546
return files
509547
}
510548

511-
// findSkill looks up a skill by name in the current skill list.
549+
// findSkill returns the pinned skill matching a resolved identity. A
550+
// SourcePlugin identity matches on both plugin name and skill name; any
551+
// other source matches a skill with no plugin name by skill name alone.
512552
func findSkill(
513553
getSkills func() []SkillMeta,
514-
name string,
554+
resolved skillspkg.Skill,
515555
) (SkillMeta, bool) {
516556
if getSkills == nil {
517557
return SkillMeta{}, false
518558
}
559+
wantPlugin := ""
560+
if resolved.Source == skillspkg.SourcePlugin {
561+
wantPlugin = resolved.PluginName
562+
}
519563
for _, s := range getSkills() {
520-
if s.Name == name {
564+
if s.Name == resolved.Name && s.PluginName == wantPlugin {
521565
return s, true
522566
}
523567
}

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.