feat: return workspace skill directory from read_skill (#26713)

Workspace skills live on the workspace filesystem, and the agent's read_file
and execute tools already operate there. read_skill now returns "dir", the
absolute skill directory, for workspace skills, so the agent can read or run
bundled supporting files (for example a scripts/ helper) with the workspace
tools. The field is omitted for personal skills, which are database-backed and
have no files. read_skill_file is unchanged.

Generated with Coder Agents on behalf of @kylecarbs.
This commit is contained in:
Kyle Carberry
2026-06-25 12:05:54 -06:00
committed by GitHub
parent a1921b6bc0
commit 48fd0ef4bc
3 changed files with 38 additions and 4 deletions
+3
View File
@@ -351,8 +351,11 @@ func ReadSkill(options ReadSkillOptions) fantasy.AgentTool {
if ok {
return response, nil
}
// Include the absolute skill directory so the agent can
// reach supporting files with read_file and execute.
return toolResponse(map[string]any{
"name": args.Name,
"dir": content.Dir,
"body": content.Body,
"files": nonNilFiles(content.Files),
}), nil
+24
View File
@@ -35,6 +35,18 @@ func responseName(t *testing.T, resp fantasy.ToolResponse) string {
return payload.Name
}
// responseDir extracts the "dir" field from a read_skill response. It is
// empty when the field is absent, as it is for personal skills.
func responseDir(t *testing.T, resp fantasy.ToolResponse) string {
t.Helper()
var payload struct {
Dir string `json:"dir"`
}
require.NoError(t, json.Unmarshal([]byte(resp.Content), &payload))
return payload.Dir
}
func TestFormatResolvedSkillIndex(t *testing.T) {
t.Parallel()
@@ -305,6 +317,9 @@ func TestReadSkillTool(t *testing.T) {
assert.False(t, resp.IsError)
assert.Contains(t, resp.Content, "Do the thing.")
assert.Contains(t, resp.Content, "helper.md")
// Workspace skills expose the absolute skill directory so the
// model can reach supporting files with read_file/execute.
assert.Equal(t, "/work/.agents/skills/my-skill", responseDir(t, resp))
})
t.Run("PinnedBodyServedWhenWorkspaceUnreachable", func(t *testing.T) {
@@ -334,6 +349,10 @@ func TestReadSkillTool(t *testing.T) {
assert.False(t, resp.IsError)
assert.Contains(t, resp.Content, "Do the thing.")
assert.Contains(t, resp.Content, `"files":[]`)
// The dir comes from the pinned SkillMeta, so it is still
// returned even when the workspace is unreachable and the file
// list degrades to empty.
assert.Equal(t, "/work/.agents/skills/my-skill", responseDir(t, resp))
})
t.Run("PersonalSkill", func(t *testing.T) {
@@ -372,6 +391,9 @@ func TestReadSkillTool(t *testing.T) {
assert.False(t, resp.IsError)
assert.Contains(t, resp.Content, "Personal instructions.")
assert.Contains(t, resp.Content, `"files":[]`)
// Personal skills are database-backed and have no directory.
assert.Empty(t, responseDir(t, resp))
assert.NotContains(t, resp.Content, `"dir"`)
})
t.Run("PersonalQualifiedAliasPreservesAlias", func(t *testing.T) {
@@ -459,6 +481,7 @@ func TestReadSkillTool(t *testing.T) {
assert.False(t, resp.IsError)
assert.Equal(t, "workspace/my-skill", responseName(t, resp))
assert.Contains(t, resp.Content, "Do the thing.")
assert.Equal(t, "/work/.agents/skills/my-skill", responseDir(t, resp))
})
t.Run("CollisionAliasRoundTrip", func(t *testing.T) {
@@ -530,6 +553,7 @@ func TestReadSkillTool(t *testing.T) {
assert.False(t, workspaceResp.IsError)
workspaceName := responseName(t, workspaceResp)
assert.Equal(t, "workspace/deploy", workspaceName)
assert.Equal(t, "/work/.agents/skills/deploy", responseDir(t, workspaceResp))
workspaceResolved, err := resolveAlias(workspaceName)
require.NoError(t, err)
assert.Equal(t, skillspkg.SourceWorkspace, workspaceResolved.Source)