feat(core): admit v2 skill guidance (#30843)

This commit is contained in:
Kit Langton 2026-06-05 11:19:55 -04:00 committed by GitHub
commit 3f64b5e621
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 3119 additions and 174 deletions

View file

@ -18,9 +18,11 @@ import { State } from "../state"
import { SessionSchema } from "../session/schema"
import type { SessionV2 } from "../session"
import { ApplicationTools } from "./application-tools"
import { AgentV2 } from "../agent"
export type ExecuteInput = {
readonly sessionID: SessionSchema.ID
readonly agent?: AgentV2.ID
readonly call: ToolCall
}
@ -37,7 +39,7 @@ export type ExecuteInput = {
export type Invocation = ExecuteInput & {
readonly source?: PermissionV2.Source
readonly assertPermission: (
input: Omit<PermissionV2.AssertInput, "sessionID" | "source">,
input: Omit<PermissionV2.AssertInput, "sessionID" | "agent" | "source">,
) => Effect.Effect<void, PermissionV2.Error | SessionV2.NotFoundError>
}
@ -129,7 +131,8 @@ export const layer = Layer.effect(
const invocation = (input: ExecuteInput): Invocation => ({
...input,
// Source needs the durable owning assistant message ID, which the registry does not receive yet.
assertPermission: (request) => permission.assert({ ...request, sessionID: input.sessionID }),
assertPermission: (request) =>
permission.assert({ ...request, sessionID: input.sessionID, ...(input.agent ? { agent: input.agent } : {}) }),
})
const settleEntry = Effect.fn("ToolRegistry.settleEntry")(function* (

View file

@ -25,18 +25,13 @@ export const Success = Schema.Struct({
resource: ToolOutputStore.Resource.pipe(Schema.optional),
})
export const description = (skills: ReadonlyArray<SkillV2.Info>) =>
[
"Load a specialized skill when the task at hand matches one of the available skills listed below.",
"",
"Use this tool to inject the skill's instructions and resources into the current conversation. The output may contain detailed workflow guidance as well as references to scripts, files, etc. in the same directory as the skill.",
"",
"The skill name must match one of the available skills listed below:",
"",
...(skills.length
? skills.map((skill) => `- **${skill.name}**: ${skill.description ?? "No description provided."}`)
: ["No skills are currently available."]),
].join("\n")
export const description = [
"Load a specialized skill when the task at hand matches one of the available skills in the system context.",
"",
"Use this tool to inject the skill's instructions and resources into the current conversation. The output may contain detailed workflow guidance as well as references to scripts, files, etc. in the same directory as the skill.",
"",
"The skill name must match one of the available skills in the system context.",
].join("\n")
export const toModelOutput = (skill: SkillV2.Info, files: ReadonlyArray<string>) => {
const directory = path.dirname(skill.location)
@ -57,10 +52,8 @@ export const toModelOutput = (skill: SkillV2.Info, files: ReadonlyArray<string>)
].join("\n")
}
const notFound = (name: string, skills: ReadonlyArray<SkillV2.Info>) =>
new ToolFailure({
message: `Skill "${name}" not found. Available skills: ${skills.map((skill) => skill.name).join(", ") || "none"}`,
})
const unableToLoad = (name: string, error?: unknown) =>
new ToolFailure({ message: `Unable to load skill ${name}`, error })
export const layer = Layer.effectDiscard(
Effect.gen(function* () {
@ -70,9 +63,8 @@ export const layer = Layer.effectDiscard(
const skills = yield* SkillV2.Service
const resources = yield* ToolOutputStore.Service
yield* boot.wait()
const available = yield* skills.list()
const definition = Tool.make({
description: description(available),
description,
parameters: Parameters,
success: Success,
toModelOutput: ({ output }) => [toolText({ type: "text", text: output.output })],
@ -85,14 +77,17 @@ export const layer = Layer.effectDiscard(
Effect.gen(function* () {
const current = yield* skills.list()
const skill = current.find((skill) => skill.name === parameters.name)
if (!skill) return yield* notFound(parameters.name, current)
if (!skill) return yield* unableToLoad(parameters.name)
return yield* Effect.gen(function* () {
yield* assertPermission({ action: name, resources: [skill.name], save: [skill.name] })
const directory = path.dirname(skill.location)
const files = (yield* fs.glob("**/*", { cwd: directory, absolute: true, include: "file", dot: true }))
.filter((file) => path.basename(file) !== "SKILL.md")
.toSorted()
.slice(0, FILE_LIMIT)
const files =
path.basename(skill.location) === "SKILL.md"
? (yield* fs.glob("**/*", { cwd: directory, absolute: true, include: "file", dot: true }))
.filter((file) => path.basename(file) !== "SKILL.md")
.toSorted()
.slice(0, FILE_LIMIT)
: []
const output = yield* resources.truncate({
sessionID,
toolCallID: call.id,
@ -105,13 +100,7 @@ export const layer = Layer.effectDiscard(
truncated: output.truncated,
...(output.truncated ? { resource: output.resource } : {}),
}
}).pipe(
Effect.catchCause((cause) =>
Effect.fail(
new ToolFailure({ message: `Unable to load skill ${parameters.name}`, error: Cause.squash(cause) }),
),
),
)
}).pipe(Effect.catchCause((cause) => Effect.fail(unableToLoad(parameters.name, Cause.squash(cause)))))
}),
}),
)