feat(core): admit v2 skill guidance (#30843)
This commit is contained in:
parent
cc487dd032
commit
3f64b5e621
40 changed files with 3119 additions and 174 deletions
|
|
@ -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* (
|
||||
|
|
|
|||
|
|
@ -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)))))
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue