convert skill tool to Tool.defineEffect (#21936)
This commit is contained in:
parent
cf27a73397
commit
5d6fe01465
3 changed files with 90 additions and 83 deletions
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -1,99 +1,101 @@
|
||||||
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
|
||||||
|
|
||||||
const description =
|
return async () => {
|
||||||
list.length === 0
|
const list = await Effect.runPromise(skill.available())
|
||||||
? "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 {
|
const description =
|
||||||
description,
|
list.length === 0
|
||||||
parameters: Parameters,
|
? "Load a specialized skill that provides domain-specific instructions and workflows. No skills are currently available."
|
||||||
async execute(params: z.infer<typeof Parameters>, ctx) {
|
: [
|
||||||
const skill = await Skill.get(params.name)
|
"Load a specialized skill that provides domain-specific instructions and workflows.",
|
||||||
|
"",
|
||||||
if (!skill) {
|
"When you recognize that a task matches one of the available skills listed below, use this tool to load the full skill instructions.",
|
||||||
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"}`)
|
"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.',
|
||||||
await ctx.ask({
|
"",
|
||||||
permission: "skill",
|
"The following skills provide specialized sets of instructions for particular tasks",
|
||||||
patterns: [params.name],
|
"Invoke this tool to load a skill when a task matches one of the available skills listed below:",
|
||||||
always: [params.name],
|
"",
|
||||||
metadata: {},
|
Skill.fmt(list, { verbose: false }),
|
||||||
})
|
].join("\n")
|
||||||
|
|
||||||
const dir = path.dirname(skill.location)
|
|
||||||
const base = pathToFileURL(dir).href
|
|
||||||
|
|
||||||
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"))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `Loaded skill: ${skill.name}`,
|
description,
|
||||||
output: [
|
parameters: Parameters,
|
||||||
`<skill_content name="${skill.name}">`,
|
execute: (params: z.infer<typeof Parameters>, ctx: Tool.Context) =>
|
||||||
`# Skill: ${skill.name}`,
|
Effect.gen(function* () {
|
||||||
"",
|
const info = yield* skill.get(params.name)
|
||||||
skill.content.trim(),
|
|
||||||
"",
|
if (!info) {
|
||||||
`Base directory for this skill: ${base}`,
|
const all = yield* skill.all()
|
||||||
"Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory.",
|
const available = all.map((s) => s.name).join(", ")
|
||||||
"Note: file list is sampled.",
|
throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`)
|
||||||
"",
|
}
|
||||||
"<skill_files>",
|
|
||||||
files,
|
yield* Effect.promise(() =>
|
||||||
"</skill_files>",
|
ctx.ask({
|
||||||
"</skill_content>",
|
permission: "skill",
|
||||||
].join("\n"),
|
patterns: [params.name],
|
||||||
metadata: {
|
always: [params.name],
|
||||||
name: skill.name,
|
metadata: {},
|
||||||
dir,
|
}),
|
||||||
},
|
)
|
||||||
|
|
||||||
|
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 }).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),
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}),
|
||||||
})
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue