convert skill tool to Tool.defineEffect (#21936)

This commit is contained in:
Kit Langton 2026-04-10 19:49:53 -04:00 committed by GitHub
commit 5d6fe01465
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 90 additions and 83 deletions

View file

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

View file

@ -1,17 +1,24 @@
import path from "path" import path from "path"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import z from "zod" import z from "zod"
import { Effect } from "effect"
import * as Stream from "effect/Stream"
import { Tool } from "./tool" import { Tool } from "./tool"
import { Skill } from "../skill" import { Skill } from "../skill"
import { Ripgrep } from "../file/ripgrep" import { Ripgrep } from "../file/ripgrep"
import { iife } from "@/util/iife"
const Parameters = z.object({ const Parameters = z.object({
name: z.string().describe("The name of the skill from available_skills"), name: z.string().describe("The name of the skill from available_skills"),
}) })
export const SkillTool = Tool.define("skill", async () => { export const SkillTool = Tool.defineEffect(
const list = await Skill.available() "skill",
Effect.gen(function* () {
const skill = yield* Skill.Service
const rg = yield* Ripgrep.Service
return async () => {
const list = await Effect.runPromise(skill.available())
const description = const description =
list.length === 0 list.length === 0
@ -34,51 +41,44 @@ export const SkillTool = Tool.define("skill", async () => {
return { return {
description, description,
parameters: Parameters, parameters: Parameters,
async execute(params: z.infer<typeof Parameters>, ctx) { execute: (params: z.infer<typeof Parameters>, ctx: Tool.Context) =>
const skill = await Skill.get(params.name) Effect.gen(function* () {
const info = yield* skill.get(params.name)
if (!skill) { if (!info) {
const available = await Skill.all().then((x) => x.map((skill) => skill.name).join(", ")) 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"}`) throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
} }
await ctx.ask({ yield* Effect.promise(() =>
ctx.ask({
permission: "skill", permission: "skill",
patterns: [params.name], patterns: [params.name],
always: [params.name], always: [params.name],
metadata: {}, metadata: {},
}) }),
)
const dir = path.dirname(skill.location) const dir = path.dirname(info.location)
const base = pathToFileURL(dir).href const base = pathToFileURL(dir).href
const limit = 10 const limit = 10
const files = await iife(async () => { const files = yield* rg.files({ cwd: dir, follow: false, hidden: true }).pipe(
const arr = [] Stream.filter((file) => !file.includes("SKILL.md")),
for await (const file of Ripgrep.files({ Stream.map((file) => path.resolve(dir, file)),
cwd: dir, Stream.take(limit),
follow: false, Stream.runCollect,
hidden: true, Effect.map((chunk) => [...chunk].map((file) => `<file>${file}</file>`).join("\n")),
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"))
return { return {
title: `Loaded skill: ${skill.name}`, title: `Loaded skill: ${info.name}`,
output: [ output: [
`<skill_content name="${skill.name}">`, `<skill_content name="${info.name}">`,
`# Skill: ${skill.name}`, `# Skill: ${info.name}`,
"", "",
skill.content.trim(), info.content.trim(),
"", "",
`Base directory for this skill: ${base}`, `Base directory for this skill: ${base}`,
"Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.", "Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.",
@ -90,10 +90,12 @@ export const SkillTool = Tool.define("skill", async () => {
"</skill_content>", "</skill_content>",
].join("\n"), ].join("\n"),
metadata: { metadata: {
name: skill.name, name: info.name,
dir, dir,
}, },
} }
}, }).pipe(Effect.orDie, Effect.runPromise),
} }
}) }
}),
)

View file

@ -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 { afterEach, describe, expect, test } from "bun:test"
import path from "path" import path from "path"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
@ -148,7 +150,9 @@ Use this skill.
await Instance.provide({ await Instance.provide({
directory: tmp.path, directory: tmp.path,
fn: async () => { 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 requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
const ctx: Tool.Context = { const ctx: Tool.Context = {
...baseCtx, ...baseCtx,