PAIPAI

defineSkills()

A per-request skill catalog and one skill tool over generated skill entries.

defineSkills() takes the entries pai generate skills writes and returns a catalog for the agent's instructions and the skill tool that serves them.

import { defineSkills } from "@pai/skills";
import { skills as generated } from "./skills/generated";

const skills = defineSkills({
  skills: generated,
  contextSchema: z.object({ plan: z.enum(["enterprise", "starter"]) }),
  // matter-intake is enterprise-only; every other skill is on every plan.
  available: ({ runtimeContext, skill }) =>
    skill.name !== "matter-intake" || runtimeContext.plan === "enterprise",
});

defineAgent({
  // name, model, …
  runtimeContext: async ({ identity }) => ({ plan: await planOf(identity) }),
  instructions: async (input) => skills.instructions(input),
  tools: { ...skills.tools },
});

Import

pnpm add @pai/skills
import { defineSkills, type SkillEntry, type SkillToolOutput } from "@pai/skills";

Signature

function defineSkills<TName extends string, TContextSchema extends z.ZodType>(options: {
  skills: readonly SkillEntry<TName>[];
  contextSchema?: TContextSchema;
  available?: (input: { runtimeContext; skill: SkillEntry<TName>; signal? }) => boolean | Promise<boolean>;
  sandbox?: true | ((input: { runtimeContext; signal? }) => boolean | Promise<boolean>);
  sandboxDir?: string;
}): {
  instructions(input: { runtimeContext; signal? }): Promise<string>;
  tools: { skill: { tool; enabled } };
  sandboxFiles: readonly SandboxSeedFile[];
};

Parameters

options: object

skills: readonly SkillEntry[]

The skills export of a generated module. Merge several arrays freely; duplicate names throw.

name: TName

The frontmatter name, equal to the folder name. The generated SkillName union types it.

description: string

The frontmatter description with whitespace collapsed: the skill's catalog line.

requiresSandbox: boolean

True when the frontmatter declares pai-requires: sandbox under metadata.

body: string

SKILL.md without its frontmatter.

files: readonly SkillsV4File[]

The skill's other files, each an AI SDK SkillsV4File (from @ai-sdk/provider): { path, data }, where data is { type: "text", text }, or { type: "data", data } with base64 for a sandbox skill's binary file.

contextSchema?: z.ZodType

Types the agent's runtimeContext as available, sandbox, instructions(), and the tool see it. It types the context; it does not parse it. Describe only the fields the predicates read; the agent's runtimeContext must include them.

available?: ({ runtimeContext, skill, signal }) => boolean | Promise<boolean>

Whether a skill is offered for this request. Applied to the catalog and again inside the tool, so a skill that isn't offered can't be read by name. Runs for the catalog, each model step, and each call.

sandbox?: true | (({ runtimeContext, signal }) => boolean | Promise<boolean>)

Whether this request has a sandbox: true when the agent always has one, or a function of the request. Omitting it, or false, gives the read-only tool; a boolean variable is a type error, so pass () => flag. Skills with requiresSandbox are offered only when it is or resolves true, and never when it is omitted. A function runs only when available offers such a skill. Setting it makes the skill tool use the sandbox capability: loading a sandbox skill with a scripts/ folder or binary files copies its folder into the sandbox. The runtime acquires the binding on every skill call, so a custom binding must acquire without error on requests that have no sandbox.

sandboxDir?: string

Sandbox folder for copied skills, at <sandboxDir>/<name>/, each with a .pai-skill marker recording the copied version. An absolute folder other than /; defineSkills throws otherwise. The provider must be able to write it: with GKE keep it under /workspace, or the first copy fails.

Defaults to "/workspace/.skills".

Returns

A object.

Place the catalog in the agent's instructions and spread the tools into its tools.

instructions: (input) => Promise<string>

The catalog for this request: a ## Skills heading, one line of guidance, and - name: description for each offered skill. An empty string when no skill is offered. Takes the agent's instructions input, or any { runtimeContext, signal }; runtimeContext is optional when there is no contextSchema.

tools.skill: { tool, enabled }

The skill tool (id pai.skills.skill), disabled when no skill is offered for the request. Spread tools into the agent unchanged: the catalog tells the model to call a tool named skill. With sandbox set, it declares the sandbox capability.

sandboxFiles: readonly SandboxSeedFile[]

Every file loading a sandbox skill copies, as seed files for the sandbox binding's seed: each skill's files except SKILL.md at <sandboxDir>/<name>/, with scripts/ files executable, then its .pai-skill marker. A load of a seeded skill finds the marker current and writes nothing. Covers every skill that copies files, whatever available or a sandbox function says for a request; filter it to leave one out. Empty without sandbox. Its paths follow sandboxDir, so keep that inside /workspace and outside /workspace/.pai, or the seed refuses the list.

Tool results

input: { name: string; path?: string } // path defaults to SKILL.md

output: SkillToolOutput =
  | { status: "ok"; name; path; text; files?: string[]; binary?: true; sandboxPath?: string; environmentReset?: true }
  | { status: "not_found"; name; path?; available: string[] } // offered skill names, or this skill's files
  | { status: "unavailable"; name }                           // needs a sandbox this request lacks

SKILL.md is returned without its frontmatter, with files listing the skill's other files. A leading ./ in path is ignored; otherwise paths match exactly. For a sandbox skill with a scripts/ folder or a binary file, every call first checks its copy in the sandbox, starting the sandbox if needed, and sandboxPath gives the skill's folder there. environmentReset reports that the sandbox was recreated, as the sandbox tools do. A binary file has binary: true and empty text; it is only usable from the sandbox. The model reads each result as text: the skill wrapped in <skill_content> with its files listed, a file wrapped in <skill_file>, or a sentence for not_found and unavailable. Results are saved in the thread, and replay reads the saved text rather than the skill.

Learn more

On this page