mirror of
https://github.com/coder/coder.git
synced 2026-06-04 05:28:20 +00:00
919dc299fc
Piggybacks on #23878. Moves instruction file reading and skill discovery from `chatd` (server-side, via multiple `LS`/`ReadFile` round-trips through the agent connection) to the agent itself (local filesystem access). This intentionally drops backward compatibility with older agents that don't support the context-config endpoint. Agents and server are deployed together; there is no rolling-update contract to maintain here. ## What changed The agent's `GET /api/v0/context-config` response now returns `[]ChatMessagePart` directly — the same types chatd persists. This eliminates intermediate type conversions and makes the protocol extensible. | Field | Type | Description | |---|---|---| | `parts` | `[]ChatMessagePart` | Context-file and skill parts, ready to persist | | `working_dir` | `string` | Agent's resolved working directory | Removed from the response: `instructions_dirs`, `instructions_file`, `skills_dirs`, `skill_meta_file`, `mcp_config_files` — the agent reads files locally and returns their content as parts. Removed from chatd: all legacy `LS`/`ReadFile` fallback code (`readHomeInstructionFile`, `readInstructionDirFile`, `DiscoverSkills` via LS, etc). ## Why The previous architecture had the agent resolve paths, serve them over HTTP, then `chatd` make N+1 round-trips back through the agent connection to read files. The agent has direct filesystem access and should just read the files. ## Key design decisions - **Agent returns `ChatMessagePart` directly** — same types chatd persists. No intermediate `InstructionFileEntry`/`SkillEntry` types needed. - **`SkillMeta.MetaFile`** — persisted via `ContextFileSkillMetaFile` on the skill part, so custom meta file names (`CODER_AGENT_EXP_SKILL_META_FILE`) survive across chat turns. - **No pre-read body** — `read_skill` always dials the workspace to fetch the skill body on demand. Simpler than caching the body in the response. - **MCP config paths kept agent-internal** — `MCPConfigFiles()` getter, not sent over the wire. - **No backward compat fallback** — old agents that don't support context-config get no instruction files. This is acceptable since agent and server deploy together.
80 lines
1.9 KiB
Go
80 lines
1.9 KiB
Go
package workspacesdk
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"golang.org/x/xerrors"
|
|
)
|
|
|
|
// markdownCommentRe strips HTML comments from skill file bodies so
|
|
// they don't leak into the LLM prompt.
|
|
var markdownCommentRe = regexp.MustCompile(`<!--[\s\S]*?-->`)
|
|
|
|
// ParseSkillFrontmatter extracts name, description, and the
|
|
// remaining body from a skill meta file. The expected format is
|
|
// YAML-ish frontmatter delimited by "---" lines:
|
|
//
|
|
// ---
|
|
// name: my-skill
|
|
// description: Does a thing
|
|
// ---
|
|
// Body text here...
|
|
func ParseSkillFrontmatter(content string) (name, description, body string, err error) {
|
|
content = strings.TrimPrefix(content, "\xef\xbb\xbf")
|
|
lines := strings.Split(content, "\n")
|
|
if len(lines) == 0 || strings.TrimSpace(lines[0]) != "---" {
|
|
return "", "", "", xerrors.New(
|
|
"missing opening frontmatter delimiter",
|
|
)
|
|
}
|
|
|
|
closingIdx := -1
|
|
for i := 1; i < len(lines); i++ {
|
|
if strings.TrimSpace(lines[i]) == "---" {
|
|
closingIdx = i
|
|
break
|
|
}
|
|
}
|
|
if closingIdx < 0 {
|
|
return "", "", "", xerrors.New(
|
|
"missing closing frontmatter delimiter",
|
|
)
|
|
}
|
|
|
|
for _, line := range lines[1:closingIdx] {
|
|
key, value, ok := strings.Cut(line, ":")
|
|
if !ok {
|
|
continue
|
|
}
|
|
key = strings.TrimSpace(key)
|
|
value = strings.TrimSpace(value)
|
|
// Strip surrounding quotes from YAML string values.
|
|
if len(value) >= 2 {
|
|
if (value[0] == '"' && value[len(value)-1] == '"') ||
|
|
(value[0] == '\'' && value[len(value)-1] == '\'') {
|
|
value = value[1 : len(value)-1]
|
|
}
|
|
}
|
|
switch strings.ToLower(key) {
|
|
case "name":
|
|
name = value
|
|
case "description":
|
|
description = value
|
|
}
|
|
}
|
|
|
|
if name == "" {
|
|
return "", "", "", xerrors.New(
|
|
"frontmatter missing required 'name' field",
|
|
)
|
|
}
|
|
|
|
// Everything after the closing delimiter is the body.
|
|
body = strings.Join(lines[closingIdx+1:], "\n")
|
|
body = markdownCommentRe.ReplaceAllString(body, "")
|
|
body = strings.TrimSpace(body)
|
|
|
|
return name, description, body, nil
|
|
}
|