From 9c16bd1e30d631d482ae696f426bf5f7eb73dbdb Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Fri, 17 Apr 2026 23:51:16 -0500 Subject: [PATCH] fix: make skills logic more token efficient (#23253) --- packages/opencode/src/tool/skill.ts | 127 +++++++++------------- packages/opencode/src/tool/skill.txt | 5 + packages/opencode/test/tool/skill.test.ts | 96 ---------------- 3 files changed, 57 insertions(+), 171 deletions(-) create mode 100644 packages/opencode/src/tool/skill.txt diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 58a66ee744..d86faec2b4 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/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, 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 `` 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, 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}`).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}`).join("\n")), - ) - - return { - title: `Loaded skill: ${info.name}`, - output: [ - ``, - `# 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.", - "", - "", - files, - "", - "", - ].join("\n"), - metadata: { - name: info.name, - dir, - }, - } - }).pipe(Effect.orDie), - } - }) + return { + title: `Loaded skill: ${info.name}`, + output: [ + ``, + `# 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.", + "", + "", + files, + "", + "", + ].join("\n"), + metadata: { + name: info.name, + dir, + }, + } + }).pipe(Effect.orDie), + } }), ) diff --git a/packages/opencode/src/tool/skill.txt b/packages/opencode/src/tool/skill.txt new file mode 100644 index 0000000000..44d990317a --- /dev/null +++ b/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. diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts index 55e126ab47..b12940e4dc 100644 --- a/packages/opencode/test/tool/skill.test.ts +++ b/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) =>