summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-04-17 23:51:16 -0500
committerGitHub <[email protected]>2026-04-17 23:51:16 -0500
commit9c16bd1e30d631d482ae696f426bf5f7eb73dbdb (patch)
treecd2060d2fec72d3ec0a17400021b0e765b3b1e6f /packages
parent5e9d5c734ea883c00bdb2936e3bd6d786b220db4 (diff)
downloadopencode-9c16bd1e30d631d482ae696f426bf5f7eb73dbdb.tar.gz
opencode-9c16bd1e30d631d482ae696f426bf5f7eb73dbdb.zip
fix: make skills logic more token efficient (#23253)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/tool/skill.ts127
-rw-r--r--packages/opencode/src/tool/skill.txt5
-rw-r--r--packages/opencode/test/tool/skill.test.ts96
3 files changed, 57 insertions, 171 deletions
diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts
index 58a66ee74..d86faec2b 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<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),
+ }
}),
)
diff --git a/packages/opencode/src/tool/skill.txt b/packages/opencode/src/tool/skill.txt
new file mode 100644
index 000000000..44d990317
--- /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 55e126ab4..b12940e4d 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) =>