Просмотр исходного кода

fix: make skills logic more token efficient (#23253)

Aiden Cline 18 часов назад
Родитель
Сommit
9c16bd1e30

+ 52 - 75
packages/opencode/src/tool/skill.ts

@@ -3,10 +3,10 @@ import { pathToFileURL } from "url"
 import z from "zod"
 import { Effect } from "effect"
 import * as Stream from "effect/Stream"
-import { EffectLogger } from "@/effect"
 import { Ripgrep } from "../file/ripgrep"
 import { Skill } from "../skill"
 import * as Tool from "./tool"
+import DESCRIPTION from "./skill.txt"
 
 const Parameters = z.object({
   name: z.string().describe("The name of the skill from available_skills"),
@@ -18,82 +18,59 @@ export const SkillTool = Tool.define(
     const skill = yield* Skill.Service
     const rg = yield* Ripgrep.Service
 
-    return () =>
-      Effect.gen(function* () {
-        const list = yield* skill.available().pipe(Effect.provide(EffectLogger.layer))
+    return {
+      description: DESCRIPTION,
+      parameters: Parameters,
+      execute: (params: z.infer<typeof Parameters>, ctx: Tool.Context) =>
+        Effect.gen(function* () {
+          const info = yield* skill.get(params.name)
+          if (!info) {
+            const all = yield* skill.all()
+            const available = all.map((item) => item.name).join(", ")
+            throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
+          }
 
-        const description =
-          list.length === 0
-            ? "Load a specialized skill that provides domain-specific instructions and workflows. No skills are currently available."
-            : [
-                "Load a specialized skill that provides domain-specific instructions and workflows.",
-                "",
-                "When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.",
-                "",
-                "The skill will inject detailed instructions, workflows, and access to bundled resources (scripts, references, templates) into the conversation context.",
-                "",
-                'Tool output includes a `<skill_content name="...">` block with the loaded content.',
-                "",
-                "The following skills provide specialized sets of instructions for particular tasks",
-                "Invoke this tool to load a skill when a task matches one of the available skills listed below:",
-                "",
-                Skill.fmt(list, { verbose: false }),
-              ].join("\n")
+          yield* ctx.ask({
+            permission: "skill",
+            patterns: [params.name],
+            always: [params.name],
+            metadata: {},
+          })
 
-        return {
-          description,
-          parameters: Parameters,
-          execute: (params: z.infer<typeof Parameters>, ctx: Tool.Context) =>
-            Effect.gen(function* () {
-              const info = yield* skill.get(params.name)
-              if (!info) {
-                const all = yield* skill.all()
-                const available = all.map((item) => item.name).join(", ")
-                throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
-              }
+          const dir = path.dirname(info.location)
+          const base = pathToFileURL(dir).href
+          const limit = 10
+          const files = yield* rg.files({ cwd: dir, follow: false, hidden: true, signal: ctx.abort }).pipe(
+            Stream.filter((file) => !file.includes("SKILL.md")),
+            Stream.map((file) => path.resolve(dir, file)),
+            Stream.take(limit),
+            Stream.runCollect,
+            Effect.map((chunk) => [...chunk].map((file) => `<file>${file}</file>`).join("\n")),
+          )
 
-              yield* ctx.ask({
-                permission: "skill",
-                patterns: [params.name],
-                always: [params.name],
-                metadata: {},
-              })
-
-              const dir = path.dirname(info.location)
-              const base = pathToFileURL(dir).href
-              const limit = 10
-              const files = yield* rg.files({ cwd: dir, follow: false, hidden: true, signal: ctx.abort }).pipe(
-                Stream.filter((file) => !file.includes("SKILL.md")),
-                Stream.map((file) => path.resolve(dir, file)),
-                Stream.take(limit),
-                Stream.runCollect,
-                Effect.map((chunk) => [...chunk].map((file) => `<file>${file}</file>`).join("\n")),
-              )
-
-              return {
-                title: `Loaded skill: ${info.name}`,
-                output: [
-                  `<skill_content name="${info.name}">`,
-                  `# Skill: ${info.name}`,
-                  "",
-                  info.content.trim(),
-                  "",
-                  `Base directory for this skill: ${base}`,
-                  "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.",
-                  "Note: file list is sampled.",
-                  "",
-                  "<skill_files>",
-                  files,
-                  "</skill_files>",
-                  "</skill_content>",
-                ].join("\n"),
-                metadata: {
-                  name: info.name,
-                  dir,
-                },
-              }
-            }).pipe(Effect.orDie),
-        }
-      })
+          return {
+            title: `Loaded skill: ${info.name}`,
+            output: [
+              `<skill_content name="${info.name}">`,
+              `# Skill: ${info.name}`,
+              "",
+              info.content.trim(),
+              "",
+              `Base directory for this skill: ${base}`,
+              "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.",
+              "Note: file list is sampled.",
+              "",
+              "<skill_files>",
+              files,
+              "</skill_files>",
+              "</skill_content>",
+            ].join("\n"),
+            metadata: {
+              name: info.name,
+              dir,
+            },
+          }
+        }).pipe(Effect.orDie),
+    }
   }),
 )

+ 5 - 0
packages/opencode/src/tool/skill.txt

@@ -0,0 +1,5 @@
+Load a specialized skill when the task at hand matches one of the skills listed in the system prompt.
+
+Use this tool to inject the skill's instructions and resources into current conversation. The output may contain detailed workflow guidance as well as references to scripts, files, etc in the same directory as the skill.
+
+The skill name must match one of the skills listed in your system prompt.

+ 0 - 96
packages/opencode/test/tool/skill.test.ts

@@ -31,102 +31,6 @@ const node = CrossSpawnSpawner.defaultLayer
 const it = testEffect(Layer.mergeAll(ToolRegistry.defaultLayer, node))
 
 describe("tool.skill", () => {
-  it.live("description lists skill location URL", () =>
-    provideTmpdirInstance(
-      (dir) =>
-        Effect.gen(function* () {
-          const skill = path.join(dir, ".opencode", "skill", "tool-skill")
-          yield* Effect.promise(() =>
-            Bun.write(
-              path.join(skill, "SKILL.md"),
-              `---
-name: tool-skill
-description: Skill for tool tests.
----
-
-# Tool Skill
-`,
-            ),
-          )
-          const home = process.env.OPENCODE_TEST_HOME
-          process.env.OPENCODE_TEST_HOME = dir
-          yield* Effect.addFinalizer(() =>
-            Effect.sync(() => {
-              process.env.OPENCODE_TEST_HOME = home
-            }),
-          )
-          const registry = yield* ToolRegistry.Service
-          const desc =
-            (yield* registry.tools({
-              providerID: "opencode" as any,
-              modelID: "gpt-5" as any,
-              agent: { name: "build", mode: "primary", permission: [], options: {} },
-            })).find((tool) => tool.id === SkillTool.id)?.description ?? ""
-          expect(desc).toContain("**tool-skill**: Skill for tool tests.")
-        }),
-      { git: true },
-    ),
-  )
-
-  it.live("description sorts skills by name and is stable across calls", () =>
-    provideTmpdirInstance(
-      (dir) =>
-        Effect.gen(function* () {
-          for (const [name, description] of [
-            ["zeta-skill", "Zeta skill."],
-            ["alpha-skill", "Alpha skill."],
-            ["middle-skill", "Middle skill."],
-          ]) {
-            const skill = path.join(dir, ".opencode", "skill", name)
-            yield* Effect.promise(() =>
-              Bun.write(
-                path.join(skill, "SKILL.md"),
-                `---
-name: ${name}
-description: ${description}
----
-
-# ${name}
-`,
-              ),
-            )
-          }
-          const home = process.env.OPENCODE_TEST_HOME
-          process.env.OPENCODE_TEST_HOME = dir
-          yield* Effect.addFinalizer(() =>
-            Effect.sync(() => {
-              process.env.OPENCODE_TEST_HOME = home
-            }),
-          )
-
-          const agent = { name: "build", mode: "primary" as const, permission: [], options: {} }
-          const registry = yield* ToolRegistry.Service
-          const load = Effect.fnUntraced(function* () {
-            return (
-              (yield* registry.tools({
-                providerID: "opencode" as any,
-                modelID: "gpt-5" as any,
-                agent,
-              })).find((tool) => tool.id === SkillTool.id)?.description ?? ""
-            )
-          })
-          const first = yield* load()
-          const second = yield* load()
-
-          expect(first).toBe(second)
-
-          const alpha = first.indexOf("**alpha-skill**: Alpha skill.")
-          const middle = first.indexOf("**middle-skill**: Middle skill.")
-          const zeta = first.indexOf("**zeta-skill**: Zeta skill.")
-
-          expect(alpha).toBeGreaterThan(-1)
-          expect(middle).toBeGreaterThan(alpha)
-          expect(zeta).toBeGreaterThan(middle)
-        }),
-      { git: true },
-    ),
-  )
-
   it.live("execute returns skill content block with files", () =>
     provideTmpdirInstance(
       (dir) =>