opencode/packages/core/src/skill/instructions.ts
2026-07-26 20:16:58 -04:00

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] })