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

convert skill tool to Tool.defineEffect (#21936)

Kit Langton 6 дней назад
Родитель
Сommit
5d6fe01465

+ 2 - 1
packages/opencode/src/tool/registry.ts

@@ -116,6 +116,7 @@ export namespace ToolRegistry {
       const edit = yield* EditTool
       const greptool = yield* GrepTool
       const patchtool = yield* ApplyPatchTool
+      const skilltool = yield* SkillTool
 
       const state = yield* InstanceState.make<State>(
         Effect.fn("ToolRegistry.state")(function* (ctx) {
@@ -185,7 +186,7 @@ export namespace ToolRegistry {
             todo: Tool.init(todo),
             search: Tool.init(websearch),
             code: Tool.init(codesearch),
-            skill: Tool.init(SkillTool),
+            skill: Tool.init(skilltool),
             patch: Tool.init(patchtool),
             question: Tool.init(question),
             lsp: Tool.init(lsptool),

+ 82 - 80
packages/opencode/src/tool/skill.ts

@@ -1,99 +1,101 @@
 import path from "path"
 import { pathToFileURL } from "url"
 import z from "zod"
+import { Effect } from "effect"
+import * as Stream from "effect/Stream"
 import { Tool } from "./tool"
 import { Skill } from "../skill"
 import { Ripgrep } from "../file/ripgrep"
-import { iife } from "@/util/iife"
 
 const Parameters = z.object({
   name: z.string().describe("The name of the skill from available_skills"),
 })
 
-export const SkillTool = Tool.define("skill", async () => {
-  const list = await Skill.available()
+export const SkillTool = Tool.defineEffect(
+  "skill",
+  Effect.gen(function* () {
+    const skill = yield* Skill.Service
+    const rg = yield* Ripgrep.Service
 
-  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")
+    return async () => {
+      const list = await Effect.runPromise(skill.available())
 
-  return {
-    description,
-    parameters: Parameters,
-    async execute(params: z.infer<typeof Parameters>, ctx) {
-      const skill = await Skill.get(params.name)
+      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")
 
-      if (!skill) {
-        const available = await Skill.all().then((x) => x.map((skill) => skill.name).join(", "))
-        throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
-      }
+      return {
+        description,
+        parameters: Parameters,
+        execute: (params: z.infer<typeof Parameters>, ctx: Tool.Context) =>
+          Effect.gen(function* () {
+            const info = yield* skill.get(params.name)
 
-      await ctx.ask({
-        permission: "skill",
-        patterns: [params.name],
-        always: [params.name],
-        metadata: {},
-      })
+            if (!info) {
+              const all = yield* skill.all()
+              const available = all.map((s) => s.name).join(", ")
+              throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
+            }
 
-      const dir = path.dirname(skill.location)
-      const base = pathToFileURL(dir).href
+            yield* Effect.promise(() =>
+              ctx.ask({
+                permission: "skill",
+                patterns: [params.name],
+                always: [params.name],
+                metadata: {},
+              }),
+            )
 
-      const limit = 10
-      const files = await iife(async () => {
-        const arr = []
-        for await (const file of Ripgrep.files({
-          cwd: dir,
-          follow: false,
-          hidden: true,
-          signal: ctx.abort,
-        })) {
-          if (file.includes("SKILL.md")) {
-            continue
-          }
-          arr.push(path.resolve(dir, file))
-          if (arr.length >= limit) {
-            break
-          }
-        }
-        return arr
-      }).then((f) => f.map((file) => `<file>${file}</file>`).join("\n"))
+            const dir = path.dirname(info.location)
+            const base = pathToFileURL(dir).href
 
-      return {
-        title: `Loaded skill: ${skill.name}`,
-        output: [
-          `<skill_content name="${skill.name}">`,
-          `# Skill: ${skill.name}`,
-          "",
-          skill.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: skill.name,
-          dir,
-        },
+            const limit = 10
+            const files = yield* rg.files({ cwd: dir, follow: false, hidden: true }).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, Effect.runPromise),
       }
-    },
-  }
-})
+    }
+  }),
+)

+ 6 - 2
packages/opencode/test/tool/skill.test.ts

@@ -1,4 +1,6 @@
-import { Effect } from "effect"
+import { Effect, Layer, ManagedRuntime } from "effect"
+import { Skill } from "../../src/skill"
+import { Ripgrep } from "../../src/file/ripgrep"
 import { afterEach, describe, expect, test } from "bun:test"
 import path from "path"
 import { pathToFileURL } from "url"
@@ -148,7 +150,9 @@ Use this skill.
       await Instance.provide({
         directory: tmp.path,
         fn: async () => {
-          const tool = await SkillTool.init()
+          const runtime = ManagedRuntime.make(Layer.mergeAll(Skill.defaultLayer, Ripgrep.defaultLayer))
+          const info = await runtime.runPromise(SkillTool)
+          const tool = await info.init()
           const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
           const ctx: Tool.Context = {
             ...baseCtx,