UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

84 lines (83 loc) • 5.25 kB
/** * RB-410 件1 — the Agent Skills (`SKILL.md`) directory loader. * * The open skills format (published at agentskills.io, reference implementation `anthropics/skills`) is * a directory per skill: `skill-name/SKILL.md` holding a `---` frontmatter block plus a Markdown body, * with optional `scripts/` / `references/` / `assets/` resource directories beside it. sema's * {@link SkillSpec} is already the same shape — frontmatter → `name`/`description`, body → `content`, * sibling resources → `files[]` — so the only missing piece was this reader. No engine mechanism * changes: the output is an ordinary `SkillSpec[]` a deployment hands to `TaskSpec.skills`. * * **Deployment-half helper, called BEFORE a task starts.** It reads the filesystem synchronously and is * NOT reachable from the run loop — the engine still never resolves a skill path at tool time, and never * writes. An embedder serving skills from a remote/sandboxed filesystem should read through its own * `ExecutionEnv` instead and construct `SkillSpec` objects directly; this helper is the local-disk case. * * **Trust contract is unchanged and is the caller's:** a skill IS instructions, so everything read here * (body and attachments alike) enters the model's context as CALLER-TRUSTED material. Pointing this at a * directory third parties can write to is the same as pasting their text into the system prompt. The * loader validates SHAPE (spec conformance), never intent. * * **`allowed-tools` / `disallowed-tools` are COPIED, never judged.** Each frontmatter list reaches * {@link SkillSpec.allowedTools} / {@link SkillSpec.disallowedTools} entry by entry as the author wrote it — no * intersection with a roster, no projection onto a tool name, no refusal, no de-duplication. What an entry means * (a tool, a namespace, a command rule, a path, a wildcard that grants nothing) is read in ONE place, the run's * grant table, which announces what it cannot read; a loader that also judged would be a second reader of the grammar. * * Frontmatter is parsed by a deliberately closed subset reader (no YAML dependency — the runtime * dependency budget is a standing constraint): top-level `key: value` scalars, optional surrounding * quotes, `#` comment lines, block scalars (`|` / `>` with their indentation + chomping indicators), * flow sequences (`[a, b]`, including the multi-line form), and indented mapping bodies skipped * wholesale. That covers every field the spec defines, and the same posture the memory-engine * frontmatter reader already takes. */ import type { SkillSpec } from "./types.js"; /** Why one skill directory (or one of its parts) was not taken as-declared. */ export type SkillsDirectoryWarningCode = /** The directory holds no `SKILL.md`. */ "no_skill_file" /** `SKILL.md` has no well-formed `---` fenced frontmatter block. */ | "no_frontmatter" /** Required `name` absent. */ | "missing_name" /** `name` breaks the charset/length rule. */ | "invalid_name" /** `name` disagrees with the directory name (the spec requires they match). */ | "name_mismatch" /** Required `description` absent. */ | "missing_description" /** `description` exceeds the spec cap. */ | "description_too_long" /** An attachment was left out (non-text bytes, over budget, unreadable, or resolving outside the skill). */ | "attachment_skipped" /** A file or directory entry existed but could not be read/resolved. */ | "read_failed"; /** One non-fatal diagnostic from a load. Safe to log: no file contents, only names/reasons. */ export interface SkillsDirectoryWarning { code: SkillsDirectoryWarningCode; /** Directory name of the skill this concerns, relative to the scanned root. */ skill: string; /** Human-readable detail (names the offending field/file and what was done about it). */ detail: string; } export interface SkillsDirectoryOptions { /** Sink for non-fatal diagnostics. Absent ⇒ warnings are dropped and the load still proceeds. */ onWarning?: (warning: SkillsDirectoryWarning) => void; /** * Total decoded-text budget for ONE skill's attachments, in bytes (default 256 KiB). Files are taken in * path order until the budget is spent; the rest are skipped with a disclosed warning. Attachments also * ride a separate delivery cap at invoke time, so a generous value here does not enlarge the prompt. */ maxAttachmentBytes?: number; } /** * Load every `SKILL.md`-bearing subdirectory of `dir` into a {@link SkillSpec}, ready for * `TaskSpec.skills`. Results are ordered by directory name (deterministic across filesystems). * * Per-skill problems are NON-FATAL: a directory whose frontmatter breaks the spec (missing/invalid * `name`, missing/over-long `description`, no fenced block, no `SKILL.md`) is SKIPPED and reported * through {@link SkillsDirectoryOptions.onWarning}, so one malformed skill cannot take down a whole * library. The root itself is different — an unreadable/non-directory root THROWS, because silently * returning `[]` for a mistyped path would look exactly like "this deployment has no skills". */ export declare function createSkillsFromDirectory(dir: string, options?: SkillsDirectoryOptions): SkillSpec[];