97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
export * as SkillInstructions from "./instructions"
|
|
|
|
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
|
import { Context, Effect, Layer, Schema } from "effect"
|
|
import { Agent } from "../agent"
|
|
import { Skill } from "../skill"
|
|
import { Instructions } from "../instructions/index"
|
|
|
|
const Summary = Schema.Struct({
|
|
id: Skill.ID,
|
|
name: Skill.Name,
|
|
description: Schema.String,
|
|
})
|
|
type Summary = typeof Summary.Type
|
|
|
|
const entries = (skills: ReadonlyArray<Summary>) =>
|
|
skills.flatMap((skill) => [
|
|
" <skill>",
|
|
` <id>${skill.id}</id>`,
|
|
` <name>${skill.name}</name>`,
|
|
` <description>${skill.description}</description>`,
|
|
" </skill>",
|
|
])
|
|
|
|
const render = (skills: ReadonlyArray<Summary>) =>
|
|
[
|
|
"Skills provide specialized instructions and workflows for specific tasks.",
|
|
"Use the skill tool to load a skill when a task matches its description.",
|
|
...(skills.length === 0
|
|
? ["No skills are currently available."]
|
|
: ["<available_skills>", ...entries(skills), "</available_skills>"]),
|
|
].join("\n")
|
|
|
|
const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary>) => {
|
|
const diff = Instructions.diffByKey(
|
|
previous,
|
|
current,
|
|
(skill) => skill.id,
|
|
(before, after) => before.name !== after.name || before.description !== after.description,
|
|
)
|
|
// Additions and removals render as small deltas; anything else restates the full list.
|
|
if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0))
|
|
return [
|
|
"The available skills have changed. This list supersedes the previous available skills list.",
|
|
render(current),
|
|
].join("\n")
|
|
return [
|
|
...(diff.added.length === 0
|
|
? []
|
|
: ["New skills are available in addition to those previously listed:", ...entries(diff.added)]),
|
|
...(diff.removed.length === 0
|
|
? []
|
|
: [
|
|
`The following skill IDs are no longer available and must not be used: ${diff.removed.map((skill) => skill.id).join(", ")}.`,
|
|
]),
|
|
].join("\n")
|
|
}
|
|
|
|
export interface Interface {
|
|
readonly load: (agent: Agent.Selection) => Effect.Effect<Instructions.Instructions>
|
|
}
|
|
|
|
export class Service extends Context.Service<Service, Interface>()("@opencode/SkillInstructions") {}
|
|
|
|
const layer = Layer.effect(
|
|
Service,
|
|
Effect.gen(function* () {
|
|
const skills = yield* Skill.Service
|
|
|
|
return Service.of({
|
|
load: Effect.fn("SkillInstructions.load")(function* (selection) {
|
|
const agent = selection.info
|
|
if (!agent) return Instructions.empty
|
|
const permitted = Skill.available(yield* skills.list(), agent)
|
|
const available = permitted
|
|
.flatMap((skill) =>
|
|
skill.description === undefined || skill.autoinvoke === false
|
|
? []
|
|
: [{ id: skill.id, name: skill.name, description: skill.description }],
|
|
)
|
|
.toSorted((a, b) => a.id.localeCompare(b.id))
|
|
return Instructions.make<ReadonlyArray<Summary>>({
|
|
key: Instructions.Key.make("core/skill-guidance"),
|
|
codec: Schema.toCodecJson(Schema.Array(Summary)),
|
|
read: Effect.succeed(available.length === 0 ? Instructions.removed : available),
|
|
render: {
|
|
initial: render,
|
|
changed: update,
|
|
removed: () => "Skill guidance is no longer available. Do not use any previously listed skill.",
|
|
},
|
|
})
|
|
}),
|
|
})
|
|
}),
|
|
)
|
|
|
|
export const node = makeLocationNode({ service: Service, layer, deps: [Skill.node] })
|