Deuz SDK 2.4 已发布 — 版本化记忆与上下文编译器、目标监督器与持久调度器、本地 Docker 运行器以及持久聊天流。 2.4 新特性
Deuz SDK
Modules

Skills

Agent Skills (SKILL.md) with progressive disclosure — catalog, trigger, and bundled resources over a pluggable source seam.

文档正文为英文。导航、搜索和界面会跟随你选择的语言。

Agent Skills package reusable instructions as SKILL.md files: YAML frontmatter (name, description, optional allowed-tools) plus a markdown body. The SDK loads them with progressive disclosure — only lightweight name+description pairs (the catalog) enter context until the model triggers a skill, then its body and bundled resources load on demand. Use this to give a model a large library of capabilities without paying the token cost of every instruction up front.

Everything in @deuz-sdk/core/skills is pure and edge-safe: parsing, matching, and rendering touch no filesystem. The only IO touchpoint is the SkillSource seam. The Node filesystem loader lives in a separate subpath, @deuz-sdk/core/skills/node.

The three disclosure levels

LevelWhat loadsAPI
1 — Catalogid + name + description onlyregistry.catalog() → render with renderSkillCatalog
2 — TriggerThe parsed SKILL.md body + frontmatterregistry.trigger(id) → SkillManifest
3 — ResourceA bundled file next to the skillregistry.resource(id, relativePath)

The model decides what to trigger. A SkillMatcher only prunes a large catalog before you render it — it is not a hidden router and never auto-triggers anything.

SKILL.md format

A skill is a directory containing a SKILL.md file. The leading --- fence is the frontmatter; everything after it is the body (the body may contain its own --- rules and code fences — only the first fence is treated as frontmatter).

skills/pdf-filler/SKILL.md
---
name: pdf-filler
description: Fill in PDF forms from structured data.
license: MIT
allowed-tools:
  - Read
  - Bash(python:*)
---

# PDF Filler

Use this skill to fill PDF forms. Read the template, map fields, then run
the fill script. See forms/w2.json for an example field map.

parseSkill(raw) returns { manifest, issues } and never throws on content. allowed-tools is normalized to manifest.allowedTools (block list, flow list [Read, Write], or comma scalar Read, Bash(git:*) all work). Unknown frontmatter keys survive untyped under manifest.metadata.

SkillManifest fieldTypeNotes
namestringValidated against ^[a-z0-9-]{1,64}$, no anthropic/claude substring
descriptionstringRequired, ≤ 1024 chars, no </>
licensestring | undefinedOptional
allowedToolsstring[] | undefinedundefined means "no restriction"
metadataRecord<string, unknown>All frontmatter keys (host extensions survive here)
bodystringTrimmed markdown after the frontmatter
rawstringThe original file text

Validation issues are advisory — parseSkill collects them, it does not reject. Validate explicitly with validateSkillName(name) / validateSkillDescription(description), each returning a SkillValidationIssue[] (empty = valid).

createSkillRegistry

createSkillRegistry wires a SkillSource (and optional matcher) into a four-method registry that orchestrates the three levels.

registry.ts
import { createSkillRegistry, type SkillRegistry } from '@deuz-sdk/core/skills';
import { nodeSkillSource } from '@deuz-sdk/core/skills/node';

const registry: SkillRegistry = createSkillRegistry({
  source: nodeSkillSource(['./skills']),
});

const catalog = await registry.catalog(); // Level 1 — SkillCandidate[]
const manifest = await registry.trigger('pdf-filler'); // Level 2 — SkillManifest
const bytes = await registry.resource('pdf-filler', 'forms/w2.json'); // Level 3
MethodReturnsLevel
catalog()Promise<SkillCandidate[]> ({ id, name, description })1
trigger(id)Promise<SkillManifest>2
resource(id, rel)Promise<Uint8Array | string>3
match(query, opts?)Promise<SkillMatch[]> (prunes the catalog)—

createSkillRegistry accepts { source, matcher?, parseYaml? }. With no matcher, it defaults to the zero-dep lexicalMatcher. Pass parseYaml to swap in a full YAML parser instead of the built-in subset parser.

Example: catalog in the system prompt

Render the Level-1 catalog into a system message so the model knows which skills exist. renderSkillCatalog emits an <available_skills> block (empty string when there are no candidates).

agent.ts
import { generateText } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
import { createSkillRegistry, renderSkillCatalog } from '@deuz-sdk/core/skills';
import { nodeSkillSource } from '@deuz-sdk/core/skills/node';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
const registry = createSkillRegistry({ source: nodeSkillSource(['./skills']) });

const catalog = await registry.catalog();
const skillsBlock = renderSkillCatalog(catalog);

const { text } = await generateText({
  model: anthropic('claude-opus-4-8'),
  messages: [
    {
      role: 'system',
      content: `You can trigger skills on demand.\n${skillsBlock}`,
    },
    { role: 'user', content: 'Fill out my W-2 from this data...' },
  ],
});

There is no system option on the call — system instructions are a role: 'system' message (see Messages).

Example: trigger on demand inside a tool

Expose trigger and resource as tools so the model loads a skill's full instructions only when it chooses to. The model triggers; you do not.

skill-tools.ts
import { generateText } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
import { createSkillRegistry, renderSkillCatalog } from '@deuz-sdk/core/skills';
import { nodeSkillSource } from '@deuz-sdk/core/skills/node';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
const registry = createSkillRegistry({ source: nodeSkillSource(['./skills']) });
const catalog = await registry.catalog();

const { text } = await generateText({
  model: anthropic('claude-opus-4-8'),
  maxSteps: 6,
  messages: [
    { role: 'system', content: renderSkillCatalog(catalog) },
    { role: 'user', content: 'Use the pdf-filler skill to complete the form.' },
  ],
  tools: {
    load_skill: {
      description: 'Load the full instructions for a skill by id (Level 2).',
      parameters: {
        type: 'object',
        properties: { id: { type: 'string' } },
        required: ['id'],
      },
      async execute({ id }: { id: string }) {
        const manifest = await registry.trigger(id);
        return manifest.body;
      },
    },
    read_skill_resource: {
      description: 'Read a bundled file shipped with a skill (Level 3).',
      parameters: {
        type: 'object',
        properties: { id: { type: 'string' }, path: { type: 'string' } },
        required: ['id', 'path'],
      },
      async execute({ id, path }: { id: string; path: string }) {
        const data = await registry.resource(id, path);
        return typeof data === 'string' ? data : new TextDecoder().decode(data);
      },
    },
  },
});

resource runs every path through normalizeResourcePath, so a traversal attempt (../secret, /etc/passwd) throws an InvalidRequestError before any IO. See the agentic tool loop for how maxSteps and tool execution work.

The SkillSource seam

A SkillSource is the single IO boundary. Implement three methods (the third is optional):

interface SkillSource {
  list(): Promise<SkillSourceEntry[]>; // Level 1: { id, name, description, path? }
  read(id: string): Promise<string>; // Level 2: full SKILL.md text
  readResource?(id: string, rel: string): Promise<Uint8Array | string>; // Level 3
}

The SDK ships three implementations:

SourceSubpathUse for
nodeSkillSource(dirs)@deuz-sdk/core/skills/nodeFilesystem — walks dirs for <id>/SKILL.md folders
fetchSkillSource(baseUrl, fetch?)@deuz-sdk/core/skillsEdge — catalog from ${baseUrl}/index.json, bodies/resources over fetch
staticSkillSource(map)@deuz-sdk/core/skillsIn-memory literal map (tests, static catalogs)

nodeSkillSource lazily imports node:fs/promises; calling it in an edge runtime throws a clear error. On the edge, use fetchSkillSource and serve an index.json of { id, name, description } entries.

Compose multiple sources

mergeSkillSources layers project / user / remote sources. Each layer can take a prefix that namespaces its ids (proj:shared); earlier sources win on conflict unless you pass { override: true }.

merge-sources.ts
import { mergeSkillSources, createSkillRegistry } from '@deuz-sdk/core/skills';
import { nodeSkillSource } from '@deuz-sdk/core/skills/node';
import { fetchSkillSource } from '@deuz-sdk/core/skills';

const source = mergeSkillSources([
  { source: nodeSkillSource(['./skills']), prefix: 'proj' },
  { source: fetchSkillSource('https://cdn.example.com/skills'), prefix: 'shared' },
]);

const registry = createSkillRegistry({ source });

Write your own source

Any object satisfying SkillSource works — for example, a Supabase-backed catalog. Only list and read are required.

supabase-source.ts
import type { SkillSource } from '@deuz-sdk/core/skills';
import { createClient } from '@supabase/supabase-js';

export function supabaseSkillSource(): SkillSource {
  const db = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_KEY!);
  return {
    async list() {
      const { data } = await db.from('skills').select('id, name, description');
      return data ?? [];
    },
    async read(id) {
      const { data } = await db.from('skills').select('raw').eq('id', id).single();
      if (!data) throw new Error(`Unknown skill '${id}'`);
      return data.raw;
    },
  };
}

Pruning a large catalog (matchers)

A SkillMatcher ranks catalog candidates by relevance to a query so you can render only the top few into the system prompt — useful when the full catalog would be too large. It only reorders and filters; the model still decides what to trigger.

prune.ts
import { createSkillRegistry, renderSkillCatalog } from '@deuz-sdk/core/skills';
import { nodeSkillSource } from '@deuz-sdk/core/skills/node';

const registry = createSkillRegistry({ source: nodeSkillSource(['./skills']) });

// lexicalMatcher (default): top 5 by token overlap with the user's message.
const top = await registry.match('fill out a pdf form', { topK: 5 });
const catalog = await registry.catalog();
const pruned = catalog.filter((c) => top.some((m) => m.id === c.id));

const skillsBlock = renderSkillCatalog(pruned);

match(query, opts?) accepts { topK?, threshold? } and returns SkillMatch[] ({ id, score }). Two matchers ship:

MatcherImportHow it scores
lexicalMatchernamed export (the default)Token overlap over name + description, zero-dep
embeddingMatcher(embed)named exportCosine over an injected embed(texts) => Promise<number[][]>
embedding-matcher.ts
import { createSkillRegistry, embeddingMatcher } from '@deuz-sdk/core/skills';
import { nodeSkillSource } from '@deuz-sdk/core/skills/node';
import { embedMany } from '@deuz-sdk/core';
import { createOpenAIEmbedding } from '@deuz-sdk/core/openai';

const model = createOpenAIEmbedding({ apiKey: process.env.OPENAI_API_KEY! })(
  'text-embedding-3-small',
);

const registry = createSkillRegistry({
  source: nodeSkillSource(['./skills']),
  matcher: embeddingMatcher(async (texts) => {
    const { embeddings } = await embedMany({ model, values: texts });
    return embeddings;
  }),
});

Scoping tools to a triggered skill

A skill's allowed-tools frontmatter can narrow which tools the model may use while that skill is active. scopeToolsToSkill(tools, allowedTools) intersects an active ToolSet by key — a scoped entry like Bash(python:*) matches the Bash key (the inner pattern is advisory metadata, not enforced by core). undefined means "no restriction" and passes the full set through.

scope-tools.ts
import { scopeToolsToSkill, type ToolSetLike } from '@deuz-sdk/core/skills';

const allTools: ToolSetLike = { Read: readTool, Write: writeTool, Bash: bashTool };

const manifest = await registry.trigger('pdf-filler');
const scoped = scopeToolsToSkill(allTools, manifest.allowedTools);
// => { Read, Bash } when allowedTools is ['Read', 'Bash(python:*)']

Path safety

normalizeResourcePath(rel) is the traversal guard behind every Level-3 read. It rewrites backslashes to /, strips a leading ./, and throws InvalidRequestError on any .. segment or leading /. Both fetchSkillSource and nodeSkillSource route resource paths through it, so bundled-file reads can never escape the skill directory.

normalizeResourcePath('forms\\w2.json'); // 'forms/w2.json'
normalizeResourcePath('../secret'); // throws InvalidRequestError
normalizeResourcePath('/abs'); // throws InvalidRequestError

Low-level parsing exports

When you need the parser without the registry, @deuz-sdk/core/skills exposes the building blocks directly:

ExportSignaturePurpose
parseSkill(raw, opts?) => { manifest, issues }Parse a SKILL.md string; never throws
splitFrontmatter(raw) => { frontmatter, body }Split the leading --- fence only
validateSkillName(name) => SkillValidationIssue[]Name constraint check
validateSkillDescription(description) => SkillValidationIssue[]Description constraint check
renderSkillCatalog(candidates) => string<available_skills> system-prompt block
normalizeResourcePath(rel) => stringTraversal guard
scopeToolsToSkill(tools, allowedTools) => ToolSetIntersect a ToolSet by tool key

Skill proposals from a run (2.4, experimental)

proposeSkillFromRun({ run, model, budget, toolSummaries?, maxOutputTokens?, maxSourceChars?, instructions?, signal?, deps? }) from @deuz-sdk/core/skills/learning drafts a SKILL.md from one native run (a 2.3 or V3 record). It is edge-safe. The result is a SkillProposal: plain JSON with status: 'proposed', evaluation: 'not-evaluated', skillMd, manifest, steps (each with its sources), limitations, an optional allowedTools, source (runId, scope, revision and the projection's digest), cost and createdAt.

  • Eligible runs only. The run must be terminal, result.status: 'completed' and verification: 'verified'; anything else, or a run with nothing to project, throws SkillLearningError with code source_not_eligible before any model call. The verification describes the source task, never the skill.
  • What the model reads. A numbered projection (S1, S2, ...) of the run's user-facing turns: no system messages, reasoning parts, provider metadata, images, compiled context blocks or compaction summaries. Raw tool results never reach the model; a result reads omitted unless toolSummaries gives a summary for its tool call id. For a run without a transcript, user turns after the caller's input (the SDK's finalization, repair and verifier turns) are left out, and a multi-turn input stays the task. A projection longer than maxSourceChars (default 32 000) throws source_too_large; it is never cut.
  • One call. One request with no tools and no transport retry, at most maxOutputTokens (default 2048), with instructions added to the system prompt. It is charged to a fresh ledger built from budget (scope 'skill-proposal'), apart from the run's accounting. budget is required and needs a tokens or usd bound (a TypeError otherwise); a budget too small for the call rejects with BudgetLedgerError code budget_exceeded before the request. cost.costUsd is set only when the model has a price. A repeated call costs again.
  • Checked draft. The draft is refused with invalid_proposal when it is not one JSON object, a step has no text or cites no item of the projection, allowedTools names a tool the run did not use (a tool counts only for a call the run made itself, with a receipt recording the call completed and an answer without an error; a call only in the input history never counts), the name or description fails parseSkill, or the answer was cut off (finishReason: 'length') or filtered. Only whitespace is cleaned up.
  • Sources. Each step's sources carry the runId, the messageId (the id the message keeps in the run's chat projection, so an input message carried over from the stored chat keeps that chat's id; otherwise <runId>:<index>), the toolCallId and modelStep when known, and the digest of the cited item. The SKILL.md adds deuz-status: proposed, deuz-evaluation: not-evaluated and deuz-source-digest to its frontmatter, and ## Method, ## Limitations and ## Sources sections to its body.

The helper writes no file, changes no registry, and never evaluates or activates the skill: storing and reviewing the draft is yours. Its model call shows up in observers as an ordinary generate-text run.

See also generateText, Messages, and Errors.

本页内容