Skip to content

Skills: load, select and compose instructions ​

A Skill is application-maintained instructions and reference material. It is not a Worker that automatically grants tool permissions. Ditto provides Context loading, selection, updates and runSkillFlow; the application controls catalog discovery, file access, versions and authorization.

1. Directory layout ​

text
skills/
  review/
    SKILL.md
    references/
      checklist.md
    scripts/
      verify.mjs

Load only SKILL.md and references needed by the task into Context. Execute supporting scripts through controlled tools; a command in a document does not grant execution rights.

Trusted catalogs may come from application code, administrator configuration or signed packages. An arbitrary user upload with the same filename should not become system instructions.

2. Run the example ​

sh
node examples/handbook/skills.ts 'Review this technical answer'

This focused example verifies loading and assembly using explicit Context; it is not a complete model Agent. It reads skills/review/SKILL.md, checks skills and tools permissions and composes the read and assembly Graphs in one Loop:

ts
import { dirname, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { createDitto, graph, graphStep, loop, loadRuntimeConfig, type GraphPlan } from "@codesoul-co/ditto/runtime";
import { createContextWorker } from "@codesoul-co/ditto/worker/context";
import { createInteractionWorker, type RegisteredTool } from "@codesoul-co/ditto/worker/interaction";

// A trusted host chooses this catalog. User input never becomes a file path.
const catalog: Readonly<Record<string, string>> = { review: "skills/review/SKILL.md" };
export const loadSkill: RegisteredTool = {
  name: "load_skill", description: "Load an allowed application skill",
  inputSchema: { type: "object", properties: { skill: { type: "string", enum: ["review"] } }, required: ["skill"] },
  validate(args) { if (typeof args.skill !== "string" || !Object.hasOwn(catalog, args.skill)) throw new TypeError("Unknown skill"); },
  async execute(args, ctx) {
    const skill = String(args.skill);
    ctx.services.sandbox.assert("skills", skill);
    const content = await ctx.services.sandbox.readText(catalog[skill]!, ctx.signal);
    if (Buffer.byteLength(content) > 16_384) throw new Error("Skill content is too large");
    return { status: "success", content };
  },
};
const load = graph<string>("skill-load")
  .node("skill", "INTERACTION.ACT.TOOL", [], skill => ({ call: { id: "load-skill", name: "load_skill", arguments: { skill } } }));
const assemble = graph<{ instructions: string; question: string }>("skill-context")
  .node("loaded", "CONTEXT.LOAD", [], input => ({ sources: [
    { id: "skill:review", content: input.instructions, metadata: { role: "system", protected: true } },
    { role: "user", content: input.question },
  ] }))
  .node("selected", "CONTEXT.SELECT", ["loaded"], (_input, { loaded }) => ({ context: loaded, purpose: "infer" }));
const plan = loop({
  id: "skill-preparation", maxIterations: 2,
  *plan(question: string): GraphPlan<unknown> {
    const { skill } = yield* graphStep(load, "review");
    if (skill.status !== "success" || typeof skill.content !== "string") throw new Error("Skill could not be loaded");
    return yield* graphStep(assemble, { instructions: skill.content, question });
  },
});
export async function runSkills(question = "Review my technical answer") {
  const workspace = resolve(dirname(fileURLToPath(import.meta.url)));
  const runtime = createDitto({ config: loadRuntimeConfig({ DITTO_RUNTIME_WORKSPACE: workspace }),
    sandbox: { read: true, tools: ["load_skill"], skills: ["review"] },
    workers: [createContextWorker(), createInteractionWorker({ tools: [loadSkill] })] });
  try { return await runtime.loop(plan, question); }
  finally { await runtime.close(); }
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) console.log(JSON.stringify(await runSkills(process.argv[2]), null, 2));

Read the example SKILL.md. In a complete conversation Agent, use Redis Context with a trusted scope for assembly and map selected items to INFER messages.

3. Two integration paths ​

PathSuitable forRequired work
Preloaded content → runSkillFlowSimple integration or existing skill systemsPass sources and optional context; preserve metadata/source
Tool read → Graph/Loop → ContextScheduled and verified loadingRegister a reader, constrain paths, grant permissions and check results

runSkillFlow(runtime,{sources,context?}) accepts parsed content. It does not parse Skill files or download packages. With context, it performs UPDATE after LOAD; otherwise it returns a new Context. It does not automatically save a Redis session. Use scoped CONTEXT nodes for cached operation.

4. Selection and versions ​

Filter the allowed catalog by task domain, user permissions and tenant policy, then load necessary skills. A model may suggest IDs; the trusted controller checks membership in the allowed set. Pin versions or content digests and record them with the task so recovery does not silently change instructions.

Use stable Context IDs such as skill:review:v2. Explicitly replace or remove old entries when updating; avoid stacking conflicting versions.

5. Budgets and trust ​

Critical instructions may use metadata.role=system and protected=true, but still need enough item/token budget. Default compression protects system/currentGoal/pending groups and fails if protected content alone exceeds the budget. Do not silently discard safety requirements to fit.

Treat hostile instructions in pages or user documents as evidence, not skill instructions. References can carry source metadata for verification and update tracking.

6. A Skill does not grant tool permissions ​

A declaration that a Skill needs network, MCP, database or write access does not authorize those actions. Runtime Sandbox, argument validation, external identity and human approval still apply. Configure a skill's capabilities separately from permissions for the current request.

Context selection and compression · Skill flows · Tools

Ditto · @codesoul-co/ditto · Node.js 24+